在准备环境前提交次全部更改。
This commit is contained in:
125
apps/admin-web/src/__tests__/flowLayers.test.ts
Normal file
125
apps/admin-web/src/__tests__/flowLayers.test.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Flow 层级与任务兼容性测试
|
||||
*
|
||||
* **Validates: Requirements 2.2**
|
||||
*
|
||||
* Property 21: 对任意 Flow 类型和任务定义,当 Flow 包含的层不包含该任务所属层时,
|
||||
* 该任务不应出现在可选列表中;当 Flow 包含该任务所属层时,该任务应出现在可选列表中。
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getFlowLayers } from "../pages/TaskConfig";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 预期的 Flow 定义(来自设计文档) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
const EXPECTED_FLOWS: Record<string, string[]> = {
|
||||
api_ods: ["ODS"],
|
||||
api_ods_dwd: ["ODS", "DWD"],
|
||||
api_full: ["ODS", "DWD", "DWS", "INDEX"],
|
||||
ods_dwd: ["DWD"],
|
||||
dwd_dws: ["DWS"],
|
||||
dwd_dws_index: ["DWS", "INDEX"],
|
||||
dwd_index: ["INDEX"],
|
||||
};
|
||||
|
||||
describe("getFlowLayers — Flow 层级与任务兼容性", () => {
|
||||
/* ---- 1. 每个已知 Flow 返回正确的层列表 ---- */
|
||||
it.each(Object.entries(EXPECTED_FLOWS))(
|
||||
"Flow '%s' 应返回 %j",
|
||||
(flowId, expectedLayers) => {
|
||||
expect(getFlowLayers(flowId)).toEqual(expectedLayers);
|
||||
},
|
||||
);
|
||||
|
||||
/* ---- 2. 未知 Flow ID 返回空数组 ---- */
|
||||
it("未知 Flow ID 应返回空数组", () => {
|
||||
expect(getFlowLayers("unknown_flow")).toEqual([]);
|
||||
expect(getFlowLayers("")).toEqual([]);
|
||||
expect(getFlowLayers("API_FULL")).toEqual([]); // 大小写敏感
|
||||
});
|
||||
|
||||
/* ---- 3. 所有 7 种 Flow 都有定义 ---- */
|
||||
it("应定义全部 7 种 Flow", () => {
|
||||
const allFlowIds = Object.keys(EXPECTED_FLOWS);
|
||||
expect(allFlowIds).toHaveLength(7);
|
||||
for (const flowId of allFlowIds) {
|
||||
expect(getFlowLayers(flowId).length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- 4. 层级互斥性验证 ---- */
|
||||
describe("层级互斥性", () => {
|
||||
it("api_ods 不包含 DWD / DWS / INDEX", () => {
|
||||
const layers = getFlowLayers("api_ods");
|
||||
expect(layers).not.toContain("DWD");
|
||||
expect(layers).not.toContain("DWS");
|
||||
expect(layers).not.toContain("INDEX");
|
||||
});
|
||||
|
||||
it("ods_dwd 只包含 DWD,不包含 ODS / DWS / INDEX", () => {
|
||||
const layers = getFlowLayers("ods_dwd");
|
||||
expect(layers).not.toContain("ODS");
|
||||
expect(layers).not.toContain("DWS");
|
||||
expect(layers).not.toContain("INDEX");
|
||||
});
|
||||
|
||||
it("dwd_dws 只包含 DWS,不包含 ODS / DWD / INDEX", () => {
|
||||
const layers = getFlowLayers("dwd_dws");
|
||||
expect(layers).not.toContain("ODS");
|
||||
expect(layers).not.toContain("DWD");
|
||||
expect(layers).not.toContain("INDEX");
|
||||
});
|
||||
|
||||
it("dwd_index 只包含 INDEX,不包含 ODS / DWD / DWS", () => {
|
||||
const layers = getFlowLayers("dwd_index");
|
||||
expect(layers).not.toContain("ODS");
|
||||
expect(layers).not.toContain("DWD");
|
||||
expect(layers).not.toContain("DWS");
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- 5. 任务兼容性:模拟任务按层过滤 ---- */
|
||||
describe("任务兼容性过滤", () => {
|
||||
// 模拟任务定义
|
||||
const mockTasks = [
|
||||
{ code: "FETCH_ORDERS", layer: "ODS" },
|
||||
{ code: "LOAD_DWD_ORDERS", layer: "DWD" },
|
||||
{ code: "AGG_DAILY_REVENUE", layer: "DWS" },
|
||||
{ code: "CALC_WBI_INDEX", layer: "INDEX" },
|
||||
];
|
||||
|
||||
/**
|
||||
* 根据 Flow 包含的层过滤任务(与 TaskSelector 组件逻辑一致)
|
||||
*/
|
||||
function filterTasksByFlow(flowId: string) {
|
||||
const layers = getFlowLayers(flowId);
|
||||
return mockTasks.filter((t) => layers.includes(t.layer));
|
||||
}
|
||||
|
||||
it("api_ods 只显示 ODS 任务", () => {
|
||||
const visible = filterTasksByFlow("api_ods");
|
||||
expect(visible.map((t) => t.code)).toEqual(["FETCH_ORDERS"]);
|
||||
});
|
||||
|
||||
it("api_full 显示所有层的任务", () => {
|
||||
const visible = filterTasksByFlow("api_full");
|
||||
expect(visible).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("dwd_dws_index 显示 DWS 和 INDEX 任务", () => {
|
||||
const visible = filterTasksByFlow("dwd_dws_index");
|
||||
const codes = visible.map((t) => t.code);
|
||||
expect(codes).toContain("AGG_DAILY_REVENUE");
|
||||
expect(codes).toContain("CALC_WBI_INDEX");
|
||||
expect(codes).not.toContain("FETCH_ORDERS");
|
||||
expect(codes).not.toContain("LOAD_DWD_ORDERS");
|
||||
});
|
||||
|
||||
it("未知 Flow 不显示任何任务", () => {
|
||||
const visible = filterTasksByFlow("nonexistent");
|
||||
expect(visible).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
169
apps/admin-web/src/__tests__/logFilter.test.ts
Normal file
169
apps/admin-web/src/__tests__/logFilter.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
/**
|
||||
* 日志过滤正确性测试
|
||||
*
|
||||
* **Validates: Requirements 9.2**
|
||||
*
|
||||
* Property 19: 对任意日志行集合和过滤关键词,过滤后的结果应只包含
|
||||
* 含有该关键词的日志行,且不遗漏任何匹配行。
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { filterLogLines } from "../pages/LogViewer";
|
||||
|
||||
describe("filterLogLines — 日志过滤正确性", () => {
|
||||
/* ---- 1. 空关键词返回所有行 ---- */
|
||||
it("空关键词返回所有行", () => {
|
||||
const lines = ["INFO 启动", "ERROR 失败", "DEBUG 调试"];
|
||||
expect(filterLogLines(lines, "")).toEqual(lines);
|
||||
});
|
||||
|
||||
/* ---- 2. 空格关键词返回所有行 ---- */
|
||||
it("空格关键词返回所有行", () => {
|
||||
const lines = ["行1", "行2", "行3"];
|
||||
expect(filterLogLines(lines, " ")).toEqual(lines);
|
||||
expect(filterLogLines(lines, "\t")).toEqual(lines);
|
||||
});
|
||||
|
||||
/* ---- 3. 匹配的行被保留 ---- */
|
||||
it("匹配的行被保留", () => {
|
||||
const lines = ["INFO 启动成功", "ERROR 连接失败", "INFO 处理完成"];
|
||||
expect(filterLogLines(lines, "INFO")).toEqual([
|
||||
"INFO 启动成功",
|
||||
"INFO 处理完成",
|
||||
]);
|
||||
});
|
||||
|
||||
/* ---- 4. 不匹配的行被过滤掉 ---- */
|
||||
it("不匹配的行被过滤掉", () => {
|
||||
const lines = ["INFO ok", "ERROR fail", "WARN slow"];
|
||||
const result = filterLogLines(lines, "ERROR");
|
||||
expect(result).not.toContain("INFO ok");
|
||||
expect(result).not.toContain("WARN slow");
|
||||
});
|
||||
|
||||
/* ---- 5. 大小写不敏感匹配 ---- */
|
||||
it("大小写不敏感匹配", () => {
|
||||
const lines = ["Error occurred", "error found", "ERROR critical"];
|
||||
const result = filterLogLines(lines, "error");
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toEqual(lines);
|
||||
});
|
||||
|
||||
/* ---- 6. 空行数组返回空数组 ---- */
|
||||
it("空行数组返回空数组", () => {
|
||||
expect(filterLogLines([], "anything")).toEqual([]);
|
||||
});
|
||||
|
||||
/* ---- 7. 所有行都匹配时返回全部 ---- */
|
||||
it("所有行都匹配时返回全部", () => {
|
||||
const lines = ["log: a", "log: b", "log: c"];
|
||||
expect(filterLogLines(lines, "log")).toEqual(lines);
|
||||
});
|
||||
|
||||
/* ---- 8. 没有行匹配时返回空数组 ---- */
|
||||
it("没有行匹配时返回空数组", () => {
|
||||
const lines = ["hello", "world", "foo"];
|
||||
expect(filterLogLines(lines, "zzz")).toEqual([]);
|
||||
});
|
||||
|
||||
/* ---- 9. 关键词在行首/行中/行尾都能匹配 ---- */
|
||||
describe("关键词位置匹配", () => {
|
||||
const keyword = "target";
|
||||
|
||||
it("行首匹配", () => {
|
||||
expect(filterLogLines(["target is here"], keyword)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("行中匹配", () => {
|
||||
expect(filterLogLines(["the target found"], keyword)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("行尾匹配", () => {
|
||||
expect(filterLogLines(["found the target"], keyword)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- 10. 特殊字符关键词正常工作 ---- */
|
||||
it("特殊字符关键词正常工作", () => {
|
||||
const lines = [
|
||||
"path: /api/v1/users",
|
||||
"regex: [a-z]+",
|
||||
"price: $100.00",
|
||||
"normal line",
|
||||
];
|
||||
// 包含 '/' 的关键词
|
||||
expect(filterLogLines(lines, "/api")).toEqual(["path: /api/v1/users"]);
|
||||
// 包含 '[' 的关键词
|
||||
expect(filterLogLines(lines, "[a-z]")).toEqual(["regex: [a-z]+"]);
|
||||
// 包含 '$' 的关键词
|
||||
expect(filterLogLines(lines, "$100")).toEqual(["price: $100.00"]);
|
||||
});
|
||||
|
||||
/* ---- 11. Property: 过滤结果是原始数组的子集 ---- */
|
||||
it("过滤结果是原始数组的子集", () => {
|
||||
const lines = ["alpha", "beta", "gamma", "delta", "epsilon"];
|
||||
const keywords = ["a", "eta", "xyz", ""];
|
||||
|
||||
for (const kw of keywords) {
|
||||
const result = filterLogLines(lines, kw);
|
||||
// 结果中的每一行都必须存在于原始数组中
|
||||
for (const line of result) {
|
||||
expect(lines).toContain(line);
|
||||
}
|
||||
// 结果长度不超过原始数组
|
||||
expect(result.length).toBeLessThanOrEqual(lines.length);
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- 12. Property: 过滤结果中每一行都包含关键词 ---- */
|
||||
it("过滤结果中每一行都包含关键词", () => {
|
||||
const lines = [
|
||||
"2024-01-01 INFO 启动",
|
||||
"2024-01-01 ERROR 数据库连接失败",
|
||||
"2024-01-01 WARN 内存不足",
|
||||
"2024-01-01 INFO 处理完成",
|
||||
"2024-01-01 DEBUG SQL: SELECT *",
|
||||
];
|
||||
const keywords = ["INFO", "error", "SQL", "2024", "不存在的关键词"];
|
||||
|
||||
for (const kw of keywords) {
|
||||
const result = filterLogLines(lines, kw);
|
||||
const lower = kw.toLowerCase();
|
||||
for (const line of result) {
|
||||
expect(line.toLowerCase()).toContain(lower);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- 13. Property: 原始数组中包含关键词的行都在结果中(不遗漏) ---- */
|
||||
it("原始数组中包含关键词的行都在结果中(不遗漏)", () => {
|
||||
const lines = [
|
||||
"INFO 启动",
|
||||
"ERROR 失败",
|
||||
"INFO 完成",
|
||||
"WARN 超时",
|
||||
"INFO 关闭",
|
||||
];
|
||||
const keyword = "INFO";
|
||||
const result = filterLogLines(lines, keyword);
|
||||
const lower = keyword.toLowerCase();
|
||||
|
||||
// 手动找出所有应匹配的行
|
||||
const expected = lines.filter((l) => l.toLowerCase().includes(lower));
|
||||
expect(result).toEqual(expected);
|
||||
|
||||
// 确认没有遗漏:原始数组中每一行如果包含关键词,就必须在结果中
|
||||
for (const line of lines) {
|
||||
if (line.toLowerCase().includes(lower)) {
|
||||
expect(result).toContain(line);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ---- 补充:保持原始顺序 ---- */
|
||||
it("过滤结果保持原始顺序", () => {
|
||||
const lines = ["c-match", "a-match", "b-no", "d-match"];
|
||||
const result = filterLogLines(lines, "match");
|
||||
expect(result).toEqual(["c-match", "a-match", "d-match"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user