import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; export const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export const bundleRoot = path.join(projectRoot, "resources/test-flows/app-system"); const ref = (expression) => `\${${expression}}`; const literal = (value) => JSON.stringify(value); const epoch = "2026-10-30T00:00:00.000Z"; // 只生成现有引擎支持的节点,不注册测试专用执行器。 export class TestFlow { constructor(number, title, components, manual = false) { this.number = String(number).padStart(2, "0"); this.flow = { schemaVersion: 1, id: `test_appsystem_${this.number}`, name: `组件测试 ${this.number} · ${title}${manual ? "【手动】" : ""}`, description: manual ? "手动测试:先阅读测试说明。默认不开启执行开关,不能把组件返回成功当作功能通过。" : "查看运行日志中的用例结果;有失败时任务最终显示失败,其他用例仍继续。", enabled: true, trigger: { type: "manual", label: "手动运行" }, variables: [], nodes: [], edges: [], meta: { createdAt: epoch, updatedAt: epoch, appVersion: "0.1.0" } }; this.entry = { id: this.flow.id, file: `${this.flow.id}.json`, name: this.flow.name, manual, components, cases: [] }; this.seq = 0; this.top = []; this.add("Trigger.Manual", { entryName: "开始组件测试" }, undefined, "人工发起"); this.set("failures", 0, "初始化失败计数"); this.set("pending", 0, "初始化待确认计数"); this.set("blocked", 0, "初始化阻塞计数"); if (manual) this.set("manualEnabled", false, "手动执行开关:确认环境后才可改为 true"); } add(componentId, values = {}, outputVar, label, parent) { const id = `t${this.number}_${++this.seq}`; const node = { id, componentId, label: label || componentId, position: { x: 220, y: 80 + this.top.length * 180 }, values, options: { timeoutMs: 15000, retryCount: 0, logException: false, ...(outputVar ? { outputVar } : {}) } }; if (parent) { node.parentId = parent.id; node.slot = parent.slot; } else { if (this.top.length) this.flow.edges.push({ id: `e_${id}`, source: this.top.at(-1), target: id }); this.top.push(id); } this.flow.nodes.push(node); return node; } set(name, value, label, parent) { return this.add("Basic_SetVariable", { value }, name, label, parent); } log(message, parent) { return this.add("Log_Info", { msg: message }, undefined, "输出测试结果", parent); } command(script, outputVar, label, parent) { return this.add("System_ExecuteCommand", { action: script }, outputVar, label, parent); } case(componentId, values, expected, predicate, options = {}) { const index = this.entry.cases.length + 1; const caseId = `${this.number}-${String(index).padStart(2, "0")}`; const out = options.output || `actual_${index}`; const result = `case_${index}`; const container = this.add("logic_control_try_catch", {}, undefined, `${caseId} · ${options.title || componentId}`); const run = { id: container.id, slot: "try_fn" }; const caught = { id: container.id, slot: "except_fn" }; let work = run; if (this.entry.manual) { const gate = this.add("logic_control_if_else", { condition: "${manualEnabled === true}" }, undefined, "手动执行保护", run); work = { id: gate.id, slot: "statement_if" }; this.add("ExceptionUtil_Throws", { msg: "待人工确认:执行开关未开启" }, undefined, "默认不执行系统状态操作", { id: gate.id, slot: "statement_else" }); } for (const step of options.before || []) this.add(step.componentId, step.values, step.output, step.label, work); const target = this.add(componentId, values, out, options.title, work); if (options.timeoutMs) target.options.timeoutMs = options.timeoutMs; for (const step of options.after || []) this.add(step.componentId, step.values, step.output, step.label, work); const actual = options.actual || out; const manualResult = options.observe === true; const status = options.blocked ? '"BLOCKED"' : manualResult ? '"MANUAL"' : `(${predicate}) ? "PASS" : "FAIL"`; // 数组是表达式引擎原生支持的结构;对象交给 json/parseJson 处理。 const prefix = [caseId, componentId]; const input = options.input || JSON.stringify(values); this.set(result, ref(`[${literal(prefix[0])}, ${literal(prefix[1])}, ${status}, ${literal(input)}, ${literal(expected)}, ${actual}]`), "记录实际值与判定", work); this.set("failures", ref(`failures + (${result}[2] === "FAIL" ? 1 : 0)`), "累计失败数", work); this.set("pending", ref(`pending + (${result}[2] === "MANUAL" ? 1 : 0)`), "累计待人工确认数", work); if (options.blocked) this.set("blocked", ref("blocked + 1"), options.blocked, work); const errorStatus = this.entry.manual ? 'manualEnabled === true ? "FAIL" : "MANUAL"' : '"FAIL"'; this.set(result, ref(`[${literal(caseId)}, ${literal(componentId)}, ${errorStatus}, ${literal(input)}, ${literal(expected)}, error.reason]`), "记录异常结果", caught); this.set("failures", ref(`failures + (${result}[2] === "FAIL" ? 1 : 0)`), "异常累计失败数", caught); this.set("pending", ref(`pending + (${result}[2] === "MANUAL" ? 1 : 0)`), "异常累计待确认数", caught); this.log(`用例结果:\${json(${result})}`); this.entry.cases.push({ id: caseId, componentId, targetNodeId: target.id, resultVariable: result, input, expected, observe: manualResult, ...(options.blocked ? { blockedReason: options.blocked } : {}) }); return out; } finish() { this.log("本组汇总:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。"); const gate = this.add("logic_control_if_else", { condition: "${failures > 0 || pending > 0 || blocked > 0}" }, undefined, "汇总判定"); this.add("ExceptionUtil_Throws", { msg: "组件测试未全部通过:失败 ${failures} 项,阻塞 ${blocked} 项,待人工确认 ${pending} 项。请查看日志。" }, undefined, "如实标记未通过", { id: gate.id, slot: "statement_if" }); this.log("本组全部通过。", { id: gate.id, slot: "statement_else" }); let y = 80; for (const id of this.top) { const node = this.flow.nodes.find(item => item.id === id); node.position.y = y; const children = this.flow.nodes.filter(item => item.parentId === id); y += children.length ? 320 + children.length * 46 : 142; } return { flow: this.flow, entry: this.entry }; } } export function buildSuite() { const groups = []; groups.push(processFlow()); const conversion = new TestFlow(2, "数据转换", ["Basic_StringToNum", "Basic_ConvertNumber", "Basic_ConvertInt", "Basic_ConvertBool", "Basic_ConvertString", "Basic_DataTypeConversion"]); conversion.case("Basic_StringToNum", { val: "123.5" }, "数字 123.5", "actual_1 === 123.5"); conversion.case("Basic_ConvertNumber", { val: "-42.25" }, "数字 -42.25", "actual_2 === -42.25"); conversion.case("Basic_ConvertInt", { val: -3.9 }, "整数 -3(向零截断)", "actual_3 === -3"); conversion.case("Basic_ConvertBool", { val: "true" }, "布尔 true", "actual_4 === true"); conversion.case("Basic_ConvertBool", { val: "false" }, "布尔 false", "actual_5 === false"); conversion.case("Basic_ConvertString", { val: 42 }, '文本 "42"', 'actual_6 === "42"'); conversion.case("Basic_DataTypeConversion", { val: "42", into_type: "number" }, "数值 42,严格区分文本和数值", "actual_7 === 42"); conversion.case("Basic_DataTypeConversion", { val: "-3.9", into_type: "int" }, "整数 -3", "actual_8 === -3"); conversion.case("Basic_DataTypeConversion", { val: "false", into_type: "bool" }, "布尔 false", "actual_9 === false"); conversion.case("Basic_DataTypeConversion", { val: 42, into_type: "string" }, '文本 "42"', 'actual_10 === "42"'); groups.push(conversion.finish()); const judge = new TestFlow(3, "数据判断", ["Basic_IsDictionary", "Basic_IsNumber", "Basic_IsNull", "Basic_IsArray", "Basic_Type", "Basic_JudgeDataType"]); for (const [id, val, expected] of [ ["Basic_IsDictionary", { name: "测试" }, true], ["Basic_IsDictionary", [], false], ["Basic_IsNumber", "12.5", true], ["Basic_IsNumber", "中文", false], ["Basic_IsNull", null, true], ["Basic_IsNull", "", true], ["Basic_IsNull", 0, false], ["Basic_IsArray", [], true], ["Basic_IsArray", "[]", false], ["Basic_Type", [1, 2], "list"], ["Basic_Type", null, "null"] ]) { const out = `actual_${judge.entry.cases.length + 1}`; judge.case(id, { val }, JSON.stringify(expected), `${out} === ${literal(expected)}`); } judge.case("Basic_JudgeDataType", { val: 42, into_type: "number" }, "数字 42 属于数值类型,返回布尔 true", "actual_12 === true"); groups.push(judge.finish()); const vars = new TestFlow(4, "变量与配置", ["Basic_SetVariable", "Basic_SetVariable_V1", "Basic_Clone", "Basic_GetProcessInfo", "Basic_DebugOutput", "CustomConfig_GetConfig", "Basic_BusinessOrderCount"]); vars.case("Basic_SetVariable", { value: "中文变量往返" }, "原值及输出变量均保持中文文本", 'actual_1 === "中文变量往返"'); vars.case("Basic_SetVariable_V1", { variableType: "rpa_int", value: "42" }, "数字 42", "actual_2 === 42"); vars.case("Basic_SetVariable_V1", { variableType: "rpa_list", value: "[1,2]" }, "数组 [1,2]", 'json(actual_3) === "[1,2]"'); vars.set("original", { title: "中文", nested: { n: 1 } }, "初始化复制源"); vars.case("Basic_Clone", { val: "${original}" }, "深复制内容相同,外层和嵌套对象均不共用引用", "json(actual_4) === json(original) && actual_4 !== original && actual_4.nested !== original.nested"); vars.case("Basic_GetProcessInfo", {}, "当前流程 ID、名称、运行 ID 和节点数一致", `actual_5.id === ${literal(vars.flow.id)} && actual_5.name === ${literal(vars.flow.name)} && len(actual_5.runId) > 0 && actual_5.nodeCount > 0`); vars.case("Basic_DebugOutput", { msg: "中文日志测试:${actual_2}" }, '日志及返回值为“中文日志测试:42”', 'actual_6 === "中文日志测试:42"'); vars.case("CustomConfig_GetConfig", { jsonlist: '[{"name":"timeout","value":30}]', configName: "timeout" }, "数字 30", "actual_7 === 30"); vars.case("CustomConfig_GetConfig", { jsonlist: "[]", configName: "missing" }, "未命中返回 null", "actual_8 === null"); vars.case("Basic_BusinessOrderCount", { start_count: 10, count: 2 }, "首次计数 12", "actual_9 === 12"); vars.case("Basic_BusinessOrderCount", { start_count: 10, count: 2 }, "同一流程再次计数 14", "actual_10 === 14"); groups.push(vars.finish()); groups.push(timingFlow()); const system = new TestFlow(6, "系统路径与环境变量", ["System_GetTempPath", "System_GetHomePath", "System_GetSystemPath", "System_GetEnv", "System_SetEnv"]); system.command("@{temp=[IO.Path]::GetTempPath().TrimEnd('\\'); home=[Environment]::GetFolderPath('UserProfile'); desktop=[Environment]::GetFolderPath('Desktop')} | ConvertTo-Json -Compress", "pathOracle", "独立读取 Windows 系统路径"); system.set("paths", "${parseJson(pathOracle)}", "解析独立预期路径"); system.case("System_GetTempPath", {}, "与系统临时目录一致(忽略尾部分隔符)", 'actual_1 === paths.temp || actual_1 === paths.temp + "\\\\"'); system.case("System_GetHomePath", {}, "与 Windows 用户文件夹一致", "actual_2 === paths.home"); system.case("System_GetSystemPath", { path_name: "desktop" }, "与 Windows 实际桌面目录一致(包含重定向)", "actual_3 === paths.desktop"); system.case("System_GetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE" }, "读取测试专用变量的原始值,作为恢复依据", "true", { output: "originalEnv" }); system.case("System_SetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE", value: "中文-测试-42" }, "当前应用进程内设置成功", 'actual_5 === "中文-测试-42"'); system.case("System_GetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE" }, "能读回中文-测试-42", 'actual_6 === "中文-测试-42"'); system.case("System_SetEnv", { name: "RPA_APP_SYSTEM_TEST_VALUE", value: "${originalEnv}" }, "恢复测试前读取值", "actual_7 === originalEnv"); groups.push(system.finish()); groups.push(commandFlow()); groups.push(...manualFlows()); return groups; } function processFlow() { const f = new TestFlow(1, "应用与 PID", ["App_Run", "App_GetPid", "App_GetPids", "App_Kill", "App_KillV1"]); const csharp = "using System; using System.Threading; public static class RpaFixture { public static void Main() { Thread.Sleep(90000); } }"; const setup = `$ErrorActionPreference='Stop'; $tag=[Guid]::NewGuid().ToString('N'); $dir=Join-Path ([IO.Path]::GetTempPath()) ('RPA-AppSystem-Tests\\'+$tag); [IO.Directory]::CreateDirectory($dir) | Out-Null; $name='rpatest_'+$tag.Substring(0,12); $exe=Join-Path $dir ($name+'.exe'); Add-Type -TypeDefinition '${csharp}' -OutputAssembly $exe -OutputType ConsoleApplication; @{folder=$dir;exe=$exe;name=$name} | ConvertTo-Json -Compress`; f.command(setup, "fixtureJson", "准备唯一测试程序(90 秒自动退出)"); f.set("fixture", "${parseJson(fixtureJson)}", "读取夹具路径与专用进程名"); f.case("App_Run", { executeable: "${fixture.exe}" }, "第一个测试进程 PID 大于 0", "pid1 > 0", { output: "pid1" }); f.case("App_Run", { executeable: "${fixture.exe}" }, "第二个 PID 大于 0,且与第一个不同", "pid2 > 0 && pid2 !== pid1", { output: "pid2" }); f.case("App_GetPid", { pattern: "${fixture.name}.exe" }, "返回本次创建的任一 PID", "actual_3 === pid1 || actual_3 === pid2"); f.case("App_GetPids", { pattern: "${fixture.name}.exe" }, "列表恰含本次创建的两个 PID", "len(actual_4) === 2 && ((actual_4[0] === pid1 && actual_4[1] === pid2) || (actual_4[0] === pid2 && actual_4[1] === pid1))"); f.case("App_Kill", { pid: "${pid1}" }, "结束第一个夹具 PID,第二个仍在运行", "parseJson(alive1) === false && parseJson(alive2) === true", { after: [processProbe("pid1", "alive1"), processProbe("pid2", "alive2")], actual: "[actual_5, parseJson(alive1), parseJson(alive2)]" }); f.case("App_KillV1", { close_type: "id", pid: "${pid2}", kill_all: "0" }, "只填写 PID 即可结束第二个夹具进程", "parseJson(aliveAfterV1) === false", { after: [processProbe("pid2", "aliveAfterV1")], actual: "[actual_6, parseJson(aliveAfterV1)]" }); f.case("App_GetPids", { pattern: "${fixture.name}.exe" }, "所有夹具结束后应返回空数组 []", "len(actual_7) === 0"); f.case("App_GetPid", { pattern: "${fixture.name}.exe" }, "无匹配时应返回 null", "actual_8 === null"); const cleanup = "$ErrorActionPreference='Stop'; $fixture='${fixture.exe}'; if ((Split-Path $fixture -Leaf) -notmatch '^rpatest_[0-9a-f]{12}\\.exe$') { throw '拒绝清理非测试进程' }; Get-Process -Name '${fixture.name}' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $fixture } | Stop-Process -Force; '测试进程清理完成'"; f.command(cleanup, "cleanupResult", "收尾:仅清理本次专用测试进程"); return f.finish(); } function processProbe(pid, output) { return { componentId: "System_ExecuteCommand", values: { action: `if (Get-Process -Id \${${pid}} -ErrorAction SilentlyContinue) { 'true' } else { 'false' }` }, output, label: "独立核对 PID 是否存活" }; } function timingFlow() { const f = new TestFlow(5, "等待与随机数", ["Basic_Delay_V2", "Basic_Delay_V1", "Basic_Delay", "Basic_RandomInt", "Basic_Rnd"]); for (const [id, values, low, high] of [ ["Basic_Delay_V2", { is_random: "0", second: "0.15", min_second: "0.1", max_second: "0.2" }, 120, 1800], ["Basic_Delay_V2", { is_random: "1", second: "0.15", min_second: "0.1", max_second: "0.2" }, 80, 1800], ["Basic_Delay_V1", { second: "0.15" }, 120, 1800], ["Basic_Delay", { mileSecond: "3000" }, 2800, 6500] ]) { const step = (output) => ({ componentId: "Time_GetDatetimeObj", values: { date_input: "${now()}" }, output, label: "独立采集毫秒时间" }); f.case(id, values, `实际等待耗时 ${low}~${high} ms`, `endTime.timestamp - startTime.timestamp >= ${low} && endTime.timestamp - startTime.timestamp <= ${high}`, { before: [step("startTime")], after: [step("endTime")], actual: "endTime.timestamp - startTime.timestamp" }); } f.case("Basic_RandomInt", { min: 7, max: 7 }, "最小值等于最大值时返回整数 7", "actual_5 === 7"); f.case("Basic_RandomInt", { min: -2, max: 2 }, "返回 -2~2 的整数", "actual_6 >= -2 && actual_6 <= 2 && actual_6 % 1 === 0"); f.case("Basic_Rnd", {}, "随机小数满足 0 ≤ x < 1", "actual_7 >= 0 && actual_7 < 1"); return f.finish(); } function commandFlow() { const f = new TestFlow(7, "命令行", ["System_ExecuteCommand", "System_ExecuteCommandV1"]); f.case("System_ExecuteCommand", { action: "Write-Output 'RPA-PS-中文-OK'" }, "RPA-PS-中文-OK", 'actual_1 === "RPA-PS-中文-OK"'); f.case("System_ExecuteCommandV1", { command_type: "1", dos_command: "echo RPA-DOS-OK", get_ret: "1" }, "RPA-DOS-OK", 'actual_2 === "RPA-DOS-OK"'); f.case("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output 'RPA-PS-V1-OK'", get_ret: "1" }, "RPA-PS-V1-OK", 'actual_3 === "RPA-PS-V1-OK"'); f.command("$dir=Join-Path ([IO.Path]::GetTempPath()) ('RPA-AppSystem-Tests\\'+[Guid]::NewGuid().ToString('N')); [IO.Directory]::CreateDirectory($dir) | Out-Null; $bat=Join-Path $dir 'echo-test.bat'; [IO.File]::WriteAllText($bat, '@echo RPA-BAT-OK', [Text.Encoding]::ASCII); $bat", "batPath", "创建仅输出标记的 BAT 夹具"); f.case("System_ExecuteCommandV1", { command_type: "2", bat_file: "${batPath}", get_ret: "1" }, "RPA-BAT-OK", 'actual_4 === "RPA-BAT-OK"'); f.case("System_ExecuteCommand", { action: "Write-Output 'RPA-ERROR-PROBE'; exit 9" }, "非零退出码应抛出异常", "false", { title: "非零返回码检查" }); // 最后一例是预期异常:由正常 Catch 记录 PASS,仍保留实际异常证据。 const last = f.entry.cases.at(-1); const caught = f.flow.nodes.find(node => node.options.outputVar === last.resultVariable && node.slot === "except_fn"); caught.values.value = ref(`[${literal(last.id)}, ${literal(last.componentId)}, "PASS", ${literal(last.input)}, ${literal(last.expected)}, error.reason]`); return f.finish(); } function manualFlows() { const result = []; const credential = new TestFlow(8, "Windows 凭据", ["System_AddCredential", "System_ReadCredential", "System_DeleteCredential"], true); credential.set("credentialTarget", "RPA-AppSystem-Tests-${now()}", "只允许使用新的测试凭据目标"); credential.case("System_AddCredential", { application: "${credentialTarget}", name: "", password: "" }, "安全输入机制准备好后,在专用目标添加测试凭据", "false", { observe: true }); credential.case("System_ReadCredential", { application: "${credentialTarget}" }, "仅验证测试凭据存在,不读取或打印密码", "false", { observe: true }); credential.case("System_DeleteCredential", { application: "${credentialTarget}" }, "删除刚创建的测试目标,之后人工确认不存在", "false", { observe: true }); result.push(credential.finish()); const ime = new TestFlow(9, "输入法", ["System_GetInputMethodState", "System_SetInputMethodState"], true); ime.case("System_GetInputMethodState", {}, "读取前台窗口的真实输入法状态,人工对照任务栏", "false", { observe: true, output: "originalIme" }); ime.case("System_SetInputMethodState", { mode: "en-US" }, "前台编辑器应切换到英文模式;仅子进程返回 set:true 不能算通过", "false", { observe: true }); ime.case("System_SetInputMethodState", { mode: "${originalIme.culture}" }, "恢复原输入法;人工确认前台窗口的实际状态", "false", { observe: true }); result.push(ime.finish()); const sound = new TestFlow(10, "音频", ["System_PlaySound"], true); const wav = fs.readFileSync(path.join(bundleRoot, "fixtures/tone.wav")).toString("base64"); sound.command(`$dir=Join-Path ([IO.Path]::GetTempPath()) ('RPA-AppSystem-Tests\\'+[Guid]::NewGuid().ToString('N')); [IO.Directory]::CreateDirectory($dir) | Out-Null; $wav=Join-Path $dir 'tone.wav'; [IO.File]::WriteAllBytes($wav,[Convert]::FromBase64String('${wav}')); $wav`, "audioPath", "准备 0.3 秒低音量测试音"); sound.case("System_PlaySound", { audio: "${audioPath}" }, "听到一次短提示音;没有声音不能算通过", "false", { observe: true }); result.push(sound.finish()); const saver = new TestFlow(11, "屏保", ["System_ScreenSaverShow", "System_ScreenSaverShowV1", "System_ScreenSaverStop"], true); saver.case("System_ScreenSaverShow", { text: "RPA 测试", status: "1" }, "屏幕实际进入屏保;无配置时记录阻塞", "false", { observe: true }); saver.case("System_ScreenSaverStop", {}, "屏保实际关闭,可正常操作", "false", { observe: true }); saver.case("System_ScreenSaverShowV1", {}, "开启 Windows 已配置的屏保,不要求输入密码;人工确认屏幕实际变化", "false", { observe: true }); saver.case("System_ScreenSaverStop", {}, "第二种屏保实际关闭", "false", { observe: true }); result.push(saver.finish()); const lock = new TestFlow(12, "锁屏与解锁", ["ScreenControl_Screenlock", "ScreenControl_ScreenUnlock"], true); lock.case("ScreenControl_Screenlock", { username: "", pwd: "", width: "", height: "", scale_factor: "100" }, "实际锁屏;需要人在机器旁用 Windows 登录凭据恢复,不改变账户密码", "false", { observe: true }); lock.case("ScreenControl_ScreenUnlock", {}, "仅尝试唤醒屏幕,loginUnlockSupported 必须为 false;Windows 登录仍需人工完成", "false", { observe: true }); result.push(lock.finish()); const remote = new TestFlow(13, "远程桌面", ["System_ConnectRemoteDesktop", "System_CloseRemoteDesktop"], true); remote.set("remoteAddress", "", "填写用户自己的测试远程地址"); remote.set("allowDisconnect", false, "断连单独开关:可能断开当前 Windows 会话,必须另行确认"); remote.case("System_ConnectRemoteDesktop", { ip: "${remoteAddress}", port: "3389" }, "人工完成登录,并确认已连接到正确测试机器;启动 mstsc 不等于连接成功", "false", { observe: true }); const disconnect = remote.case("System_CloseRemoteDesktop", {}, "确认目标会话及恢复方式后才能执行;当前实现会调用 tsdiscon", "false", { observe: true }); const target = remote.flow.nodes.find(node => node.options.outputVar === disconnect); const parent = { id: target.parentId, slot: target.slot }; const guard = remote.add("logic_control_if_else", { condition: "${allowDisconnect === true}" }, undefined, "断连操作单独保护", parent); // 子节点按数组顺序执行,保护节点必须在目标前。 remote.flow.nodes.splice(remote.flow.nodes.indexOf(guard), 1); remote.flow.nodes.splice(remote.flow.nodes.indexOf(target), 0, guard); target.parentId = guard.id; target.slot = "statement_if"; remote.add("ExceptionUtil_Throws", { msg: "阻塞:断连开关未开启;执行可能中断当前会话。" }, undefined, "阻止未授权断连", { id: guard.id, slot: "statement_else" }); result.push(remote.finish()); return result; } export function writeSuite() { const groups = buildSuite(); fs.mkdirSync(bundleRoot, { recursive: true }); for (const { flow, entry } of groups) fs.writeFileSync(path.join(bundleRoot, entry.file), JSON.stringify(flow, null, 2) + "\n"); const manifest = { schemaVersion: 1, generatedAt: epoch, groups: groups.map(item => item.entry) }; fs.writeFileSync(path.join(bundleRoot, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n"); return manifest; }