PRA-only-demo/packages/rpa-core/src/capabilities/utility-extended.test.ts
yu caa8e19332 阶段1-rpa-core建包: engine+31个纯Node能力迁入packages/rpa-core, 领域类型单一源化, createEngine工厂收拢模块级状态
- engine/(registry/types/runtime/expression)物理迁入包, 解耦shared/types(域类型归包, 宿主type-only re-export)
- 31个纯Node能力+27个测试迁入包; 宿主留守Electron/浏览器/AI依赖能力, index.ts经@rpa/core/capabilities拉起
- 新增createEngine()/defaultEngine: excel缓存/Excel句柄/db连接/FTP客户端/OCR实例/拟真设置挂引擎实例
- remaining-core子流程依赖改为host-bridge注入, 包内不再import宿主flows
- 宿主main/index.ts启动时注入子流程源; electron-vite/vitest/tsconfig配@rpa/core源码别名
- 验证: typecheck绿, 快照909个id diff为零, 332测试过(2个桌面冒烟因会话无交互SetForegroundWindow被拒, 环境阻塞)
2026-11-04 12:36:03 +08:00

131 lines
7.3 KiB
TypeScript

import { mkdtemp, readFile, rm, writeFile } 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, FlowNode } from "../types";
import type { RunContext } from "../engine/types";
import { getHandler } from "../engine/registry";
import "./utility-extended";
const ids = [
"Array_Reshape2Dim", "Time_GetDatetimeObj", "Time_TimeTypeConversion_V1", "Basic_SetVariable_V1",
"Basic_GetProcessInfo", "System_ExecuteCommandV1", "DataService_Diff", "DataService_WorkingDay",
"Databook_Clear", "Databook_GetRowAndColCount", "Databook_WriteContent", "Databook_ReadContent",
"Databook_DeleteRow", "Databook_DeleteColumn"
] as const;
const directories: string[] = [];
const flow: FlowDefinition = {
schemaVersion: 1, id: "flow-1", name: "本地工具测试", description: "utility",
variables: [], nodes: [], edges: [], meta: { createdAt: "2026-10-27", updatedAt: "2026-10-27", appVersion: "0.1.0" }
};
const context = { vars: {}, flow, runId: "run-1" } as unknown as RunContext;
const component = {} as CatalogComponent;
const node = (id: string, outputVar?: string): FlowNode => ({
id: `utility_${id}`, componentId: id, position: { x: 0, y: 0 }, values: {}, options: outputVar ? { outputVar } : {}
});
async function run(id: string, args: Record<string, unknown> = {}, outputVar?: string): Promise<unknown> {
const handler = getHandler(id);
expect(handler, `${id} should be registered`).toBeTruthy();
return handler!(context, node(id, outputVar), args, component);
}
afterEach(async () => {
context.vars = {};
await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
describe("utility extended capabilities", () => {
it("registers only real catalog component ids", async () => {
const catalog = JSON.parse(await readFile("resources/catalog.json", "utf8")) as Catalog;
const catalogIds = new Set(catalog.components.map((item) => item.id));
expect(ids.length).toBeGreaterThanOrEqual(10);
for (const id of ids) {
expect(catalogIds.has(id), `${id} should exist in catalog`).toBe(true);
expect(getHandler(id), `${id} should be registered`).toBeTruthy();
}
});
it("reshapes arrays and rejects incompatible dimensions", async () => {
await expect(run("Array_Reshape2Dim", { arr: "[1,2,3,4,5,6]", x: 2, y: 3 }))
.resolves.toEqual([[1, 2, 3], [4, 5, 6]]);
await expect(run("Array_Reshape2Dim", { arr: [1, 2, 3], x: 2, y: 2 }))
.rejects.toMatchObject({ code: "validation_error" });
});
it("returns datetime details and formats Python-style patterns", async () => {
const detail = await run("Time_GetDatetimeObj", { date_input: "2026-10-27 08:09:10" }) as Record<string, unknown>;
expect(detail).toMatchObject({ year: 2026, month: 10, day: 27, hour: 8, minute: 9, second: 10 });
await expect(run("Time_TimeTypeConversion_V1", { datetime: "2026-10-27 08:09:10", get_type: "%Y/%m/%d %H:%M:%S" }))
.resolves.toBe("2026/10/27 08:09:10");
await expect(run("Time_TimeTypeConversion_V1", { datetime: "12:34", get_type: "%H:%M:%S" }))
.resolves.toBe("12:34:00");
await expect(run("Time_TimeTypeConversion_V1", { datetime: "2026-02-30" }))
.rejects.toMatchObject({ code: "validation_error" });
});
it("converts variable types and exposes current flow information", async () => {
await expect(run("Basic_SetVariable_V1", { variableType: "rpa_int", value: "42" }, "answer")).resolves.toBe(42);
expect(context.vars.answer).toBe(42);
await expect(run("Basic_SetVariable_V1", { variableType: "rpa_list", value: "[1,2]" })).resolves.toEqual([1, 2]);
await expect(run("Basic_GetProcessInfo")).resolves.toMatchObject({ id: "flow-1", name: "本地工具测试", runId: "run-1" });
});
it("executes a local command and returns stdout", async () => {
const args = process.platform === "win32"
? { command_type: "1", dos_command: "echo utility-ok", get_ret: true }
: { command_type: "1", dos_command: "printf utility-ok", get_ret: true };
await expect(run("System_ExecuteCommandV1", args)).resolves.toContain("utility-ok");
});
it("executes BAT files whose path contains spaces and Chinese characters", async () => {
if (process.platform !== "win32") return;
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-utility-中文 空格-"));
directories.push(directory);
const file = path.join(directory, "测试 脚本.bat");
await writeFile(file, "@echo RPA-BAT-PATH-OK\r\n", "ascii");
await expect(run("System_ExecuteCommandV1", { command_type: "2", bat_file: file, get_ret: "1" })).resolves.toBe("RPA-BAT-PATH-OK");
});
it("keeps PowerShell command output in UTF-8", async () => {
if (process.platform !== "win32") return;
await expect(run("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output '中文输出测试'", get_ret: "1" })).resolves.toBe("中文输出测试");
});
it("compares local text files with structured line changes", async () => {
const directory = await mkdtemp(path.join(os.tmpdir(), "rpa-utility-"));
directories.push(directory);
const source = path.join(directory, "source.txt");
const target = path.join(directory, "target.txt");
await writeFile(source, "A\nB\nC", "utf8");
await writeFile(target, "A\nD\nC", "utf8");
const result = await run("DataService_Diff", { sourceFilePath: source, targetFilePath: target, showTable: true }) as Record<string, unknown>;
expect(result).toMatchObject({ identical: false, addedCount: 1, removedCount: 1, sourceLineCount: 3, targetLineCount: 3 });
});
it("evaluates weekends and local holiday/workday overrides", async () => {
await expect(run("DataService_WorkingDay", { day: "2026-10-27" })).resolves.toBe(true);
await expect(run("DataService_WorkingDay", { day: "2026-10-31" })).resolves.toBe(false);
await expect(run("DataService_WorkingDay", { day: "2026-10-31", working_cal: '{"workdays":["2026-10-31"]}' })).resolves.toBe(true);
await expect(run("DataService_WorkingDay", { day: "2026-10-27", working_cal: '["2026-10-27"]' })).resolves.toBe(false);
});
it("writes, reads, counts, and clears the in-run databook", async () => {
await run("Databook_WriteContent", { write_type: "region", column_name: "A", row_num: 1, content: [["name", "score"], ["Alice", 90], ["Bob", 80]] });
await expect(run("Databook_ReadContent", { read_type: "cell", column_name: "B", row_num: 2 })).resolves.toBe(90);
await expect(run("Databook_ReadContent", { read_type: "column", column_name: "A" })).resolves.toEqual(["name", "Alice", "Bob"]);
await expect(run("Databook_ReadContent", { read_type: "region", column_name: "A", row_num: 2, end_column_name: "B", end_row_num: 3 }))
.resolves.toEqual([["Alice", 90], ["Bob", 80]]);
await expect(run("Databook_GetRowAndColCount")).resolves.toMatchObject({ rowCount: 3, columnCount: 2 });
await expect(run("Databook_Clear")).resolves.toEqual([]);
});
it("deletes selected databook rows and columns", async () => {
context.vars.__rpaDatabook = [["A", "B", "C"], [1, 2, 3], [4, 5, 6]];
await run("Databook_DeleteRow", { row_nums: [2] });
await run("Databook_DeleteColumn", { column_names: ["B"] });
expect(context.vars.__rpaDatabook).toEqual([["A", "C"], [4, 6]]);
});
});