fix: ConvertBool数字语义对齐(数字1为true), 凭据读写改经ps强制UTF8解码中文并返回整条凭据块, 补应用与系统冒烟回归10项
This commit is contained in:
parent
d5d7df8880
commit
d4de0ec245
157
src/main/capabilities/app-system-smoke.test.ts
Normal file
157
src/main/capabilities/app-system-smoke.test.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getHandler } from "../engine/registry";
|
||||
import type { CatalogComponent, FlowNode } from "../../shared/types";
|
||||
import type { RunContext } from "../engine/types";
|
||||
import "./data";
|
||||
import "./desktop";
|
||||
import "./system-extended";
|
||||
import "./utility-extended";
|
||||
import "./common-final";
|
||||
import "./control";
|
||||
import "./windows-system";
|
||||
import "./basic-encryption";
|
||||
|
||||
const ctx = {
|
||||
signal: new AbortController().signal,
|
||||
log: () => {},
|
||||
vars: {},
|
||||
evidenceDir: "D:/office-test2",
|
||||
runId: "smoke-run",
|
||||
flow: {
|
||||
id: "smoke-flow", name: "冒烟测试流程", description: "", enabled: true,
|
||||
trigger: null, meta: { appVersion: "dev", createdAt: "", updatedAt: "" },
|
||||
nodes: []
|
||||
}
|
||||
} as unknown as RunContext;
|
||||
|
||||
const node = { id: "smoke-app", componentId: "smoke", position: { x: 0, y: 0 }, values: {}, options: {} } as unknown as FlowNode;
|
||||
|
||||
async function run(componentId: string, args: Record<string, unknown>, ctxOverride?: RunContext): Promise<unknown> {
|
||||
const handler = getHandler(componentId);
|
||||
if (!handler) throw new Error(`组件 ${componentId} 未注册运行时`);
|
||||
const n = { ...node, componentId } as unknown as FlowNode;
|
||||
return handler(ctxOverride ?? ctx, n, args, {} as CatalogComponent);
|
||||
}
|
||||
|
||||
describe("应用与系统组件冒烟", () => {
|
||||
it("数据类型判断族", async () => {
|
||||
expect(await run("Basic_IsNumber", { val: "12.5" })).toBe(true);
|
||||
expect(await run("Basic_IsNumber", { val: "abc" })).toBe(false);
|
||||
expect(await run("Basic_IsNull", { val: "" })).toBe(true);
|
||||
expect(await run("Basic_IsNull", { val: "x" })).toBe(false);
|
||||
expect(await run("Basic_IsArray", { val: [1, 2] })).toBe(true);
|
||||
expect(await run("Basic_IsDictionary", { val: { a: 1 } })).toBe(true);
|
||||
expect(await run("Basic_Type", { val: [1] })).toBe("list");
|
||||
expect(await run("Basic_Type", { val: "s" })).toBe("string");
|
||||
expect(await run("Basic_JudgeDataType", { val: 5, into_type: "int" })).toBe(true);
|
||||
expect(await run("Basic_JudgeDataType", { val: 5.5, into_type: "int" })).toBe(false);
|
||||
console.log("[数据判断族] 通过");
|
||||
});
|
||||
|
||||
it("数据转换族", async () => {
|
||||
expect(await run("Basic_ConvertString", { val: 123 })).toBe("123");
|
||||
expect(await run("Basic_ConvertInt", { val: "12.9" })).toBe(12);
|
||||
expect(await run("Basic_ConvertBool", { val: 1 })).toBe(true);
|
||||
expect(await run("Basic_ConvertNumber", { val: "3.14" })).toBe(3.14);
|
||||
expect(await run("Basic_DataTypeConversion", { val: "是", into_type: "bool" })).toBe(true);
|
||||
expect(await run("Basic_DataTypeConversion", { val: 8, into_type: "string" })).toBe("8");
|
||||
console.log("[数据转换族] 通过");
|
||||
});
|
||||
|
||||
it("复制/随机/密文", async () => {
|
||||
const src = { a: [1, { b: 2 }] };
|
||||
const cloned = (await run("Basic_Clone", { val: src })) as typeof src;
|
||||
expect(cloned).toEqual(src);
|
||||
expect(cloned).not.toBe(src);
|
||||
const ri = (await run("Basic_RandomInt", { min: 5, max: 5 })) as number;
|
||||
expect(ri).toBe(5);
|
||||
const rnd = (await run("Basic_Rnd", {})) as number;
|
||||
expect(rnd).toBeGreaterThanOrEqual(0);
|
||||
expect(rnd).toBeLessThan(1);
|
||||
expect(await run("Basic_Dodecryption", { words: Buffer.from("hello-世界", "utf8").toString("base64") })).toBe("hello-世界");
|
||||
console.log("[复制/随机/密文] 通过");
|
||||
});
|
||||
|
||||
it("延迟/调试输出/设置变量", async () => {
|
||||
const logs: string[] = [];
|
||||
const ctxLog = { ...ctx, log: (m: string) => logs.push(m), vars: {} } as unknown as RunContext;
|
||||
const t0 = Date.now();
|
||||
expect(await run("Basic_Delay", { mileSecond: 100 })).toBe(0.1);
|
||||
expect(Date.now() - t0).toBeGreaterThanOrEqual(95);
|
||||
await run("Basic_DebugOutput", { msg: "冒烟输出" }, ctxLog);
|
||||
expect(logs.join()).toContain("冒烟输出");
|
||||
const setNode = { ...node, options: { outputVar: "v_test" } } as unknown as FlowNode;
|
||||
const handler = getHandler("Basic_SetVariable_V1")!;
|
||||
expect(await handler(ctxLog, setNode, { value: "42", variableType: "rpa_int" }, {} as CatalogComponent)).toBe(42);
|
||||
expect((ctxLog as unknown as { vars: Record<string, unknown> }).vars.v_test).toBe(42);
|
||||
console.log("[延迟/输出/变量] 通过");
|
||||
});
|
||||
|
||||
it("自定义配置与业务计数", async () => {
|
||||
const cfg = (await run("CustomConfig_GetConfig", {
|
||||
jsonlist: JSON.stringify([{ name: "host", value: "127.0.0.1" }]),
|
||||
configName: "host"
|
||||
})) as string;
|
||||
expect(cfg).toBe("127.0.0.1");
|
||||
const ctxCount = { ...ctx, vars: {} } as unknown as RunContext;
|
||||
expect(await run("Basic_BusinessOrderCount", { start_count: 100, count: 3 }, ctxCount)).toBe(103);
|
||||
expect(await run("Basic_BusinessOrderCount", { start_count: 100, count: 3 }, ctxCount)).toBe(106);
|
||||
console.log("[配置/计数] 通过");
|
||||
});
|
||||
|
||||
it("系统路径与环境变量", async () => {
|
||||
expect(String(await run("System_GetTempPath", {}))).toMatch(/Temp|tmp/i);
|
||||
expect(String(await run("System_GetHomePath", {}))).toMatch(/Users|home/i);
|
||||
expect(String(await run("System_GetSystemPath", { path_name: "desktop" }))).toMatch(/Desktop/);
|
||||
expect(await run("System_GetEnv", { name: "PATH" })).toBeTruthy();
|
||||
expect(await run("System_SetEnv", { name: "RPA_SMOKE", value: "ok" })).toBe("ok");
|
||||
expect(await run("System_GetEnv", { name: "RPA_SMOKE" })).toBe("ok");
|
||||
expect(await run("Basic_GetProcessInfo", {})).toBeTruthy();
|
||||
console.log("[系统路径/环境变量] 通过");
|
||||
});
|
||||
|
||||
it("输入法状态获取", async () => {
|
||||
const st = (await run("System_GetInputMethodState", {})) as { culture: string; layout: string };
|
||||
console.log("[输入法]", JSON.stringify(st));
|
||||
expect(st.culture).toBeTruthy();
|
||||
}, 60000);
|
||||
|
||||
it("执行命令行两种组件", async () => {
|
||||
const out1 = String(await run("System_ExecuteCommand", { command: "echo rpa-smoke-123" }));
|
||||
expect(out1).toContain("rpa-smoke-123");
|
||||
const out2 = String(await run("System_ExecuteCommandV1", { command_type: "1", dos_command: "echo dos-ok-456" }));
|
||||
expect(out2).toContain("dos-ok-456");
|
||||
const out3 = String(await run("System_ExecuteCommandV1", { command_type: "3", ps_command: "Write-Output 'ps-ok-789'" }));
|
||||
expect(out3).toContain("ps-ok-789");
|
||||
console.log("[执行命令] 通过");
|
||||
}, 120000);
|
||||
|
||||
it("应用生命周期: 启动→查PID→关闭", async () => {
|
||||
const pid = (await run("App_Run", { executeable: "notepad.exe" })) as number;
|
||||
expect(Number.isInteger(pid)).toBe(true);
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
// Win11 notepad.exe 为应用别名:Start-Process 返回的启动器 PID 会立即退出,真实进程另获新 PID,
|
||||
// 因此不比较具体 PID,只验证「能查到运行中的 notepad 进程」
|
||||
const single = (await run("App_GetPid", { pattern: "notepad" })) as number | null;
|
||||
expect(single).toBeTruthy();
|
||||
const all = (await run("App_GetPids", { p_name: "notepad" })) as number[];
|
||||
expect(all.length).toBeGreaterThan(0);
|
||||
const killed = await run("App_KillV1", { close_type: "name", p_name: "notepad" });
|
||||
console.log("[应用生命周期] 启动PID:", pid, "查询到:", all, "关闭:", JSON.stringify(killed));
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
const after = (await run("App_GetPids", { pattern: "notepad" })) as number[];
|
||||
expect(after.length).toBe(0);
|
||||
}, 120000);
|
||||
|
||||
it("Windows凭据: 添加→读取→删除", async () => {
|
||||
const target = "RPA_SMOKETEST_GENERIC";
|
||||
expect((await run("System_AddCredential", { application: target, name: "smoke-user", password: "p@ss" })) as { added: boolean }).toEqual({ added: true, target });
|
||||
const read = (await run("System_ReadCredential", { application: target })) as { exists: boolean; detail: string };
|
||||
expect(read.exists).toBe(true);
|
||||
expect(read.detail).toContain("smoke-user");
|
||||
expect((await run("System_DeleteCredential", { application: target })) as { deleted: boolean }).toEqual({ deleted: true, target });
|
||||
const readAfter = (await run("System_ReadCredential", { application: target })) as { exists: boolean };
|
||||
expect(readAfter.exists).toBe(false);
|
||||
console.log("[凭据闭环] 通过");
|
||||
}, 120000);
|
||||
});
|
||||
@ -481,7 +481,11 @@ register(["CSV_Write", "CSV_WriteNew"], async (_ctx, node, args) => {
|
||||
register(["Basic_ConvertNumber", "Basic_StringToNum"], (_ctx, _node, args) => num(first(args, ["val"])));
|
||||
register("Basic_ConvertString", (_ctx, _node, args) => str(first(args, ["val"])));
|
||||
register("Basic_ConvertInt", (_ctx, _node, args) => Math.trunc(num(first(args, ["val"]))));
|
||||
register("Basic_ConvertBool", (_ctx, _node, args) => bool(first(args, ["val"])));
|
||||
register("Basic_ConvertBool", (_ctx, _node, args) => {
|
||||
const value = first(args, ["val"]);
|
||||
if (typeof value === "number") return value !== 0;
|
||||
return bool(value);
|
||||
});
|
||||
register("Basic_IsNull", (_ctx, _node, args) => {
|
||||
const value = first(args, ["val"]);
|
||||
return value === null || value === undefined || value === "";
|
||||
|
||||
@ -5,6 +5,13 @@ import { promisify } from "node:util";
|
||||
|
||||
const run = promisify(execFile);
|
||||
const ps = (script: string) => run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 60000 });
|
||||
// cmdkey 在中文 Windows 上输出 GBK,直接 execFile 按 UTF-8 解码会乱码;统一经 PowerShell 强制 UTF-8 控制台编码捕获
|
||||
const cmdkey = async (args: string[]): Promise<string> => {
|
||||
const quoted = args.map((item) => "'" + item.replace(/'/g, "''") + "'");
|
||||
const script = "[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); & cmdkey " + quoted.join(" ");
|
||||
const { stdout } = await run("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", script], { windowsHide: true, timeout: 60000 });
|
||||
return stdout;
|
||||
};
|
||||
|
||||
function reqText(args: Record<string, unknown>, keys: string[], label: string, nodeId: string): string {
|
||||
for (const key of keys) {
|
||||
@ -42,10 +49,16 @@ register("System_ScreenSaverStop", async () => {
|
||||
|
||||
register("System_ReadCredential", async (ctx, node, args) => {
|
||||
const app = reqText(args, ["application"], "凭据目标", node.id);
|
||||
const { stdout } = await run("cmdkey", ["/list"], { windowsHide: true });
|
||||
const stdout = await cmdkey(["/list"]);
|
||||
const exists = stdout.includes(app);
|
||||
const userLine = stdout.split(/\r?\n/).find(l => l.includes(app));
|
||||
return { target: app, exists, detail: exists ? (userLine ?? "").trim() : "" };
|
||||
// cmdkey /list 每条凭据是「目标/类型/用户」多行块(空行分隔);取整块才有用户名等有效信息
|
||||
const lines = stdout.split(/\r?\n/);
|
||||
const idx = lines.findIndex((l) => l.includes(app));
|
||||
const block: string[] = [];
|
||||
if (idx >= 0) {
|
||||
for (let i = idx; i < lines.length && lines[i].trim(); i++) block.push(lines[i].trim());
|
||||
}
|
||||
return { target: app, exists, detail: exists ? block.join(" | ") : "" };
|
||||
});
|
||||
|
||||
register("System_AddCredential", async (ctx, node, args) => {
|
||||
@ -53,13 +66,13 @@ register("System_AddCredential", async (ctx, node, args) => {
|
||||
const user = reqText(args, ["name"], "用户名", node.id);
|
||||
const pwd = String(args.password ?? "");
|
||||
const psArgs = pwd ? ["/generic:" + app, "/user:" + user, "/pass:" + pwd] : ["/generic:" + app, "/user:" + user];
|
||||
await run("cmdkey", psArgs, { windowsHide: true });
|
||||
await cmdkey(psArgs);
|
||||
return { added: true, target: app };
|
||||
});
|
||||
|
||||
register("System_DeleteCredential", async (ctx, node, args) => {
|
||||
const app = reqText(args, ["application"], "凭据目标", node.id);
|
||||
await run("cmdkey", ["/delete:" + app], { windowsHide: true });
|
||||
await cmdkey(["/delete:" + app]);
|
||||
return { deleted: true, target: app };
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user