refactor(rpa-core): 测试文件迁至 test/ 目录(对面 workspace 规范:产品源码树禁含测试);修 platform-groups 测试预载顺序耦合
This commit is contained in:
1 parent
62652b4fca
commit
4f1fbc07dc
44 files changed
+196
-187
No files matched your search
@@ -0,0 +1,313 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type {
|
||||
Catalog,
|
||||
CatalogComponent,
|
||||
FlowDefinition,
|
||||
FlowEdge,
|
||||
FlowNode,
|
||||
RunEvent
|
||||
} from "../../src/types";
|
||||
import "../../src/capabilities";
|
||||
import { register } from "../../src/engine/registry";
|
||||
import { runFlow } from "../../src/engine/runtime";
|
||||
|
||||
const catalog = JSON.parse(
|
||||
readFileSync(path.resolve(process.cwd(), "resources/catalog.json"), "utf8")
|
||||
) as Catalog;
|
||||
const components = new Map(catalog.components.map((component) => [component.id, component]));
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
const baseOptions = {
|
||||
timeoutMs: 5_000,
|
||||
retryCount: 0,
|
||||
retryIntervalMs: 1_000,
|
||||
skipOnError: false
|
||||
};
|
||||
|
||||
function node(
|
||||
id: string,
|
||||
componentId: string,
|
||||
values: Record<string, unknown> = {},
|
||||
options: FlowNode["options"] = {},
|
||||
extra: Partial<FlowNode> = {}
|
||||
): FlowNode {
|
||||
return {
|
||||
id,
|
||||
componentId,
|
||||
position: { x: 0, y: 0 },
|
||||
values,
|
||||
options: { ...baseOptions, ...options },
|
||||
...extra
|
||||
};
|
||||
}
|
||||
|
||||
function edge(source: string, target: string): FlowEdge {
|
||||
return { id: `${source}->${target}`, source, target };
|
||||
}
|
||||
|
||||
function flow(id: string, nodes: FlowNode[], edges: FlowEdge[] = []): FlowDefinition {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
id,
|
||||
name: id,
|
||||
variables: [],
|
||||
nodes,
|
||||
edges,
|
||||
meta: { createdAt: now, updatedAt: now, appVersion: "test" }
|
||||
};
|
||||
}
|
||||
|
||||
function testComponent(id: string): CatalogComponent {
|
||||
return {
|
||||
id,
|
||||
name: id,
|
||||
kind: "native",
|
||||
group: "test",
|
||||
categories: ["test"],
|
||||
categorySignature: "test",
|
||||
description: "runtime integration test component",
|
||||
messageTemplate: "",
|
||||
baseType: id,
|
||||
functionName: id,
|
||||
functionCalls: [],
|
||||
version: "1",
|
||||
tags: [],
|
||||
platformTag: null,
|
||||
icon: "",
|
||||
risk: "low",
|
||||
fields: [],
|
||||
output: { label: "", key: "", suggestedName: "", valueTypes: [], fields: [] },
|
||||
slots: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function tempDir(): Promise<string> {
|
||||
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-runtime-integration-"));
|
||||
tempDirs.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
async function execute(
|
||||
definition: FlowDefinition,
|
||||
variables: Record<string, unknown> = {},
|
||||
extraComponents: CatalogComponent[] = []
|
||||
) {
|
||||
const directory = await tempDir();
|
||||
const events: Array<Omit<RunEvent, "runId">> = [];
|
||||
const logs: Array<{ message: string; level?: "info" | "warn" | "error"; nodeId?: string }> = [];
|
||||
const componentMap = new Map(components);
|
||||
for (const component of extraComponents) componentMap.set(component.id, component);
|
||||
|
||||
const result = await runFlow(definition, {
|
||||
runId: `run-${definition.id}`,
|
||||
evidenceDir: directory,
|
||||
signal: new AbortController().signal,
|
||||
variables,
|
||||
emit: (event) => events.push(event),
|
||||
log: (message, level, nodeId) => logs.push({ message, level, nodeId }),
|
||||
componentOf: (current) => componentMap.get(current.componentId)
|
||||
});
|
||||
|
||||
return { result, events, logs, directory };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
describe("runFlow integration", () => {
|
||||
it("follows edges in a straight line and passes variables between real capabilities", async () => {
|
||||
const definition = flow(
|
||||
"straight-variable-flow",
|
||||
[
|
||||
node("debug", "Basic_DebugOutput", { msg: "${upper}" }, {}, { position: { x: 0, y: 10 } }),
|
||||
node("upper", "StrUtil_UpperCase", { string: "${source}" }, { outputVar: "upper" }),
|
||||
node("trigger", "Trigger.Manual", {}, {}, { position: { x: 0, y: 300 } }),
|
||||
node("set", "Basic_SetVariable", { value: "hello rpa" }, { outputVar: "source" })
|
||||
],
|
||||
[edge("trigger", "set"), edge("set", "upper"), edge("upper", "debug")]
|
||||
);
|
||||
|
||||
const { result, events, logs } = await execute(definition);
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(result.variables).toMatchObject({ source: "hello rpa", upper: "HELLO RPA" });
|
||||
expect(result.nodes.map((state) => state.nodeId)).toEqual(["trigger", "set", "upper", "debug"]);
|
||||
expect(events.filter((event) => event.type === "node.started").map((event) => event.nodeId)).toEqual([
|
||||
"trigger",
|
||||
"set",
|
||||
"upper",
|
||||
"debug"
|
||||
]);
|
||||
expect(logs.some((entry) => entry.nodeId === "debug" && entry.message === "HELLO RPA")).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ approved: true, expected: "true", executed: "true-node", absent: "false-node" },
|
||||
{ approved: false, expected: "false", executed: "false-node", absent: "true-node" }
|
||||
])("executes only the IF $expected branch", async ({ approved, expected, executed, absent }) => {
|
||||
const branch = node("branch", "logic_control_if_else", { condition: "${approved}" });
|
||||
const definition = flow("if-branch-flow", [
|
||||
branch,
|
||||
node("true-node", "Basic_SetVariable", { value: "true" }, { outputVar: "branchResult" }, {
|
||||
parentId: branch.id,
|
||||
slot: "statement_if"
|
||||
}),
|
||||
node("false-node", "Basic_SetVariable", { value: "false" }, { outputVar: "branchResult" }, {
|
||||
parentId: branch.id,
|
||||
slot: "statement_else"
|
||||
})
|
||||
]);
|
||||
|
||||
const { result } = await execute(definition, { approved });
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(result.variables.branchResult).toBe(expected);
|
||||
expect(result.nodes.find((state) => state.nodeId === executed)?.status).toBe("succeeded");
|
||||
expect(result.nodes.some((state) => state.nodeId === absent)).toBe(false);
|
||||
expect(result.nodes.find((state) => state.nodeId === "branch")?.output).toBe(approved);
|
||||
});
|
||||
|
||||
it("runs a count loop inclusively and exposes the loop index to child nodes", async () => {
|
||||
const loop = node("loop", "logic_control_count_for", {
|
||||
count_index: "iteration",
|
||||
count_start_value: "1",
|
||||
count_end_value: "3",
|
||||
count_step: "1"
|
||||
});
|
||||
const definition = flow("count-loop-flow", [
|
||||
loop,
|
||||
node("accumulate", "Basic_SetVariable", { value: "${total + iteration}" }, { outputVar: "total" }, {
|
||||
parentId: loop.id,
|
||||
slot: "count_for_fn"
|
||||
})
|
||||
]);
|
||||
|
||||
const { result, events } = await execute(definition, { total: 0 });
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(result.variables).toMatchObject({ iteration: 3, total: 6 });
|
||||
expect(result.nodes.find((state) => state.nodeId === "loop")?.output).toBe(3);
|
||||
expect(events.filter((event) => event.type === "node.started" && event.nodeId === "accumulate")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("runs an array loop through the last item when end_index is -1", async () => {
|
||||
const loop = node("loop", "logic_control_array_for_v3", {
|
||||
array_name: "items",
|
||||
reverse: false,
|
||||
start_index: 0,
|
||||
end_index: -1
|
||||
});
|
||||
const definition = flow("array-loop-flow", [
|
||||
loop,
|
||||
node("collect", "Basic_DebugOutput", { msg: "${item}" }, {}, {
|
||||
parentId: loop.id,
|
||||
slot: "array_for_fn"
|
||||
})
|
||||
]);
|
||||
|
||||
const { result, events } = await execute(definition, { items: ["glm", "mini", "other", "kimi"] });
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(result.nodes.find((state) => state.nodeId === "loop")?.output).toBe(4);
|
||||
expect(events.filter((event) => event.type === "node.started" && event.nodeId === "collect").map((event) => event.nodeId)).toHaveLength(4);
|
||||
});
|
||||
|
||||
it("retries a transient failure and records the successful attempt", async () => {
|
||||
const component = testComponent("Test.TransientFailure");
|
||||
let calls = 0;
|
||||
register(component.id, () => {
|
||||
calls += 1;
|
||||
if (calls < 3) throw new Error(`temporary failure ${calls}`);
|
||||
return "recovered";
|
||||
});
|
||||
const definition = flow("retry-flow", [
|
||||
node("retry", component.id, {}, { outputVar: "retryResult", retryCount: 2, retryIntervalMs: 1_000 })
|
||||
]);
|
||||
|
||||
const { result, logs } = await execute(definition, {}, [component]);
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(calls).toBe(3);
|
||||
expect(result.variables.retryResult).toBe("recovered");
|
||||
expect(result.nodes[0]).toMatchObject({ status: "succeeded", attempt: 3, output: "recovered" });
|
||||
expect(logs.filter((entry) => entry.level === "warn" && entry.nodeId === "retry")).toHaveLength(2);
|
||||
}, 5_000);
|
||||
|
||||
it("continues after a skipped failure and executes the following node", async () => {
|
||||
const component = testComponent("Test.AlwaysFails");
|
||||
register(component.id, () => {
|
||||
throw new Error("expected failure");
|
||||
});
|
||||
const definition = flow(
|
||||
"skip-on-error-flow",
|
||||
[
|
||||
node("failure", component.id, {}, { skipOnError: true }),
|
||||
node("after", "Basic_SetVariable", { value: "continued" }, { outputVar: "afterFailure" })
|
||||
],
|
||||
[edge("failure", "after")]
|
||||
);
|
||||
|
||||
const { result } = await execute(definition, {}, [component]);
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(result.nodes.find((state) => state.nodeId === "failure")).toMatchObject({
|
||||
status: "skipped",
|
||||
error: { code: "unknown", reason: "expected failure" }
|
||||
});
|
||||
expect(result.nodes.find((state) => state.nodeId === "after")?.status).toBe("succeeded");
|
||||
expect(result.variables.afterFailure).toBe("continued");
|
||||
});
|
||||
|
||||
it("runs an offline file pipeline with interpolation and persisted output", async () => {
|
||||
const directory = await tempDir();
|
||||
const sourcePath = path.join(directory, "source.txt");
|
||||
const targetPath = path.join(directory, "result.txt");
|
||||
const definition = flow(
|
||||
"file-pipeline-flow",
|
||||
[
|
||||
node("write-source", "File_Write", {
|
||||
path: "${sourcePath}",
|
||||
content: "${inputText}",
|
||||
encode: "utf-8",
|
||||
append: "False"
|
||||
}),
|
||||
node("read-source", "File_Read", { path: "${sourcePath}", encode: "utf-8" }, { outputVar: "fileText" }),
|
||||
node("transform", "StrUtil_UpperCase", { string: "${fileText}" }, { outputVar: "transformed" }),
|
||||
node("write-target", "File_Write", {
|
||||
path: "${targetPath}",
|
||||
content: "${transformed}",
|
||||
encode: "utf-8",
|
||||
append: "False"
|
||||
}),
|
||||
node("read-target", "File_Read", { path: "${targetPath}", encode: "utf-8" }, { outputVar: "finalText" })
|
||||
],
|
||||
[
|
||||
edge("write-source", "read-source"),
|
||||
edge("read-source", "transform"),
|
||||
edge("transform", "write-target"),
|
||||
edge("write-target", "read-target")
|
||||
]
|
||||
);
|
||||
|
||||
const { result } = await execute(definition, {
|
||||
sourcePath,
|
||||
targetPath,
|
||||
inputText: "offline rpa pipeline"
|
||||
});
|
||||
|
||||
expect(result.status).toBe("succeeded");
|
||||
expect(result.variables).toMatchObject({
|
||||
fileText: "offline rpa pipeline",
|
||||
transformed: "OFFLINE RPA PIPELINE",
|
||||
finalText: "OFFLINE RPA PIPELINE"
|
||||
});
|
||||
await expect(readFile(targetPath, "utf8")).resolves.toBe("OFFLINE RPA PIPELINE");
|
||||
expect(result.nodes.every((state) => state.status === "succeeded")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user