import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { buildSuite, bundleRoot } from "./app-system-test-flows.mjs"; import { installBundle, upgradeBundle, makeReport, caseResults } from "./app-system-tests.mjs"; import { applyCatalogOverrides } from "./catalog-overrides.mjs"; import { hasCapability } from "../src/main/capabilities/index.ts"; import { runFlow } from "../src/main/engine/runtime.ts"; import { validateFlow } from "../src/renderer/src/validation/flowValidation.ts"; const containers = new Set(["logic_control_try_catch", "logic_control_if_else"]); const catalog = JSON.parse(fs.readFileSync("resources/catalog.json", "utf8")); for (const c of catalog.components) c.implemented = containers.has(c.id) || hasCapability(c.id) || hasCapability(c.functionName); const componentMap = new Map(catalog.components.map(c => [c.id, c])); const temporary = []; afterEach(() => { for (const dir of temporary.splice(0)) fs.rmSync(dir, { recursive: true, force: true }); }); async function execute(number, change) { const group = buildSuite()[number - 1]; change?.(group.flow); const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-appsystem-unit-")); temporary.push(dir); const logs = []; const result = await runFlow(group.flow, { runId: `unit-${group.flow.id}`, evidenceDir: dir, signal: new AbortController().signal, emit: () => {}, log: (message) => logs.push(message), componentOf: node => componentMap.get(node.componentId) }); return { group, result, logs }; } describe("应用与系统测试流程包", () => { it("覆盖真实注册的全部 49 项,13 组无重复归属,不覆盖待接入组件", () => { const suite = buildSuite(); expect(suite).toHaveLength(13); const covered = suite.flatMap(group => group.entry.components).sort(); const executable = catalog.components.filter(c => c.group === "app" && c.implemented).map(c => c.id).sort(); expect(covered).toHaveLength(49); expect(new Set(covered).size).toBe(49); expect(covered).toEqual(executable); for (const { flow, entry } of suite) { expect(new Set(flow.nodes.map(node => node.id)).size).toBe(flow.nodes.length); expect(new Set(entry.cases.map(c => c.componentId))).toEqual(new Set(entry.components)); const reached = new Set([flow.nodes[0].id]); for (let n = 0; n < flow.nodes.length; n++) { for (const edge of flow.edges) if (reached.has(edge.source)) reached.add(edge.target); for (const node of flow.nodes) if (reached.has(node.parentId)) reached.add(node.id); } expect(reached.size, flow.name).toBe(flow.nodes.length); expect(entry.cases.every(c => flow.nodes.some(n => n.id === c.targetNodeId && n.componentId === c.componentId))).toBe(true); expect(JSON.parse(fs.readFileSync(path.join(bundleRoot, entry.file), "utf8"))).toEqual(flow); } }); it("目录修正可重复应用,前端类型选项都有实际判断与转换能力", async () => { expect(applyCatalogOverrides(structuredClone(catalog))).toEqual(catalog); for (const id of ["Basic_JudgeDataType", "Basic_DataTypeConversion"]) { const component = componentMap.get(id); const options = component.fields.find(field => field.key === "into_type").options; expect(options.length).toBeGreaterThanOrEqual(4); for (const option of options) { const output = await import("../src/main/engine/registry.ts").then(({ getHandler }) => getHandler(id)({}, { id: "type-test", componentId: id }, { val: 42, into_type: option.value }, component)); if (id === "Basic_JudgeDataType") expect(typeof output).toBe("boolean"); } } const wake = componentMap.get("ScreenControl_ScreenUnlock"); expect(wake.name).toContain("非登录解锁"); expect(wake.fields).toHaveLength(0); expect(componentMap.get("System_ScreenSaverShowV1").fields).toHaveLength(0); }); it("只保留真实参数校验阻塞,不出现断链、未知变量或未接入节点", () => { const blocked = []; for (const { flow } of buildSuite()) { const issues = validateFlow(flow, catalog); expect(issues.every(issue => issue.code === "required"), flow.name).toBe(true); if (issues.length) blocked.push(flow.id); } expect(blocked).toEqual(["test_appsystem_08", "test_appsystem_12"]); const pidOnly = buildSuite()[0].flow; const killV1 = pidOnly.nodes.find(n => n.componentId === "App_KillV1"); killV1.values.p_name = ""; expect(validateFlow(pidOnly, catalog).filter(issue => issue.nodeId === killV1.id)).toEqual([]); }); it("真实执行转换流程,显式类型选项下完整核对全部用例", async () => { const { group, result, logs } = await execute(2); expect(result.status).toBe("succeeded"); expect(result.variables.failures).toBe(0); expect(result.variables.blocked).toBe(0); expect(group.entry.cases.map(c => result.variables[c.resultVariable][2])).toEqual(Array(10).fill("PASS")); expect(logs.filter(log => log.startsWith("用例结果:"))).toHaveLength(10); }); it("注入错误返回类型时断言会失败,且前面的用例继续留有证据", async () => { // 只改变内存中的测试副本,不把现有组件缺陷固化成测试的预期行为。 const { result } = await execute(3, flow => { const target = flow.nodes.find(n => n.componentId === "Basic_JudgeDataType"); target.componentId = "Basic_SetVariable"; target.values = { value: "number" }; }); expect(result.variables.failures).toBe(1); expect(result.variables.case_12[2]).toBe("FAIL"); expect(result.variables.case_12[5]).toBe("number"); expect(result.variables.case_11[2]).toBe("PASS"); }); it("变量、深复制、配置、流程信息和计数能够真实通过", async () => { const { result } = await execute(4); expect(result.status).toBe("succeeded"); expect(result.variables.failures).toBe(0); expect(result.variables.case_10[5]).toBe(14); }); it("手动任务默认保护有效,不调用输入法、远程连接或断连操作", async () => { for (const number of [9, 13]) { const { group, result } = await execute(number); expect(result.status).toBe("failed"); expect(result.variables.pending).toBe(group.entry.cases.length); for (const c of group.entry.cases) { expect(result.nodes.some(n => n.nodeId === c.targetNodeId)).toBe(false); expect(result.variables[c.resultVariable][2]).toBe("MANUAL"); } } }); it("字符串 false 或 true 不会误开启手动保护", async () => { for (const value of ["false", "true"]) { const { group, result } = await execute(13, flow => { flow.nodes.find(n => n.options.outputVar === "manualEnabled").values.value = value; }); for (const c of group.entry.cases) expect(result.nodes.some(n => n.nodeId === c.targetNodeId)).toBe(false); } }); it("只新增 13 个任务,重复安装保留用户已有修改且不改无关文件", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-appsystem-install-")); temporary.push(dir); const first = installBundle(dir); expect(first.installed).toHaveLength(13); const file = path.join(dir, "flows/test_appsystem_01.json"); fs.writeFileSync(file, "用户自行编辑的内容"); fs.writeFileSync(path.join(dir, "flows/other.json"), "其他任务"); const second = installBundle(dir); expect(second.installed).toHaveLength(0); expect(second.skipped).toHaveLength(13); expect(fs.readFileSync(file, "utf8")).toBe("用户自行编辑的内容"); expect(fs.readFileSync(path.join(dir, "flows/other.json"), "utf8")).toBe("其他任务"); }); it("升级仅覆盖匹配旧模板的测试任务,并保存原文件备份", () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rpa-appsystem-upgrade-")); temporary.push(dir); installBundle(dir); const baseline = path.join(dir, "baseline"), backup = path.join(dir, "backup"); fs.mkdirSync(baseline); for (const { entry, flow } of buildSuite()) { const old = { ...flow, description: "旧测试模板" }; fs.writeFileSync(path.join(baseline, entry.file), JSON.stringify(old)); fs.writeFileSync(path.join(dir, "flows", entry.file), JSON.stringify(old)); } const changed = path.join(dir, "flows/test_appsystem_01.json"); const userFlow = JSON.parse(fs.readFileSync(changed, "utf8")); userFlow.description = "用户已修改"; fs.writeFileSync(changed, JSON.stringify(userFlow)); const result = upgradeBundle(dir, baseline, backup); expect(result.upgraded).toHaveLength(12); expect(result.skipped).toContainEqual({ id: "test_appsystem_01", reason: "用户已修改,保留现有版本" }); expect(fs.readFileSync(changed, "utf8")).toBe(JSON.stringify(userFlow)); expect(JSON.parse(fs.readFileSync(path.join(backup, "test_appsystem_02.json"), "utf8")).description).toBe("旧测试模板"); }); it("报告按最差用例聚合,不把任务成功或缺失的记录冒充组件通过", () => { const group = buildSuite()[0]; const variables = Object.fromEntries(group.entry.cases.map(c => [c.resultVariable, [c.id, c.componentId, "PASS", c.input, c.expected, 42]])); variables.case_8[2] = "FAIL"; const report = makeReport({ runs: [{ flowId: group.flow.id, status: "succeeded", result: { variables } }] }); expect(report.components.find(c => c.id === "App_GetPid").status).toBe("FAIL"); expect(report.components.find(c => c.id === "Basic_ConvertInt").status).toBe("UNRUN"); expect(report.components.find(c => c.id === "System_PlaySound").status).toBe("MANUAL"); expect(caseResults(group.entry, { status: "BLOCKED" }).every(c => c.status === "BLOCKED")).toBe(true); }); });