阶段0-基线与护栏: 修复不可移植/抖动测试全绿(341过), vitest串行执行防桌面竞争, 新增运行时能力注册快照(909个id)
This commit is contained in:
1 parent
f35f0ac979
commit
4139400dbd
9 files changed
+1030
-36
No files matched your search
@@ -1,9 +1,8 @@
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { afterAll, describe, expect, it } from "vitest";
|
||||
import { getHandler } from "../engine/registry";
|
||||
import { capabilityIds, getHandler } from "../engine/registry";
|
||||
import { RuntimeError } from "../engine/types";
|
||||
import type { CatalogComponent, FlowNode } from "../../shared/types";
|
||||
import type { RunContext } from "../engine/types";
|
||||
@@ -195,10 +194,10 @@ describe("数据处理/文件/编排 冒烟", () => {
|
||||
expect(await run("File_FileExists", { path: copy })).toBe(false);
|
||||
const renamed = await run("File_Rename", { pathSrc: moved, name: "renamed.txt" });
|
||||
expect(String(renamed)).toContain("renamed.txt");
|
||||
const zip = await run("File_CompressFile", { pathSrc: created, pathDest: path.join(sandbox, "pack.zip") });
|
||||
const zip = await run("File_CompressFile", { pathSrc: created, pathDest: path.join(sandbox, "pack.zip"), direct_type: "0" });
|
||||
expect(String(zip)).toContain("pack.zip");
|
||||
const unzipDir = path.join(sandbox, "unpacked");
|
||||
await run("File_DecompressFile", { path: path.join(sandbox, "pack.zip"), target: unzipDir });
|
||||
await run("File_DecompressFile", { path: path.join(sandbox, "pack.zip"), target: unzipDir, direct_type: "0" });
|
||||
expect(await run("File_FileExists", { path: path.join(unzipDir, "a.txt") })).toBe(true);
|
||||
expect(await run("File_WaitFile", { file_path: created, time_out: 1 })).toBe(true);
|
||||
await expect(run("File_WaitFile", { file_path: path.join(dir, "ghost.txt"), time_out: 1 })).rejects.toThrow(/超时/);
|
||||
@@ -252,9 +251,7 @@ describe("数据处理/文件/编排 冒烟", () => {
|
||||
});
|
||||
|
||||
it("全量健康检查: 已注册组件空参调用不产生非结构化崩溃", async () => {
|
||||
const ids: string[] = JSON.parse(
|
||||
readFileSync("D:/wordkb/2026-10-28-10-36-47/PRA-only-demo/live-ids.json", "utf8")
|
||||
) as string[];
|
||||
const ids: string[] = capabilityIds();
|
||||
const skip = new Set([
|
||||
"FTP_Connect", "FTP_Upload", "FTP_Download", "FTP_Delete", "FTP_Rename", "FTP_Cwd", "FTP_GetFileList", "FTP_CreateDir", "FTP_Exist", "FTP_Disconnect",
|
||||
"Database_DBConnect", "Database_SQLExecute", "Database_SingleSQLQuery", "Database_AllSQLQuery_V1", "Database_BatchSQLExecute", "Database_BatchInsertExecute", "Database_CloseDBConnect",
|
||||
|
||||
@@ -64,14 +64,29 @@ describe("键盘输入组件真实冒烟", () => {
|
||||
const { promisify } = await import("node:util");
|
||||
const runPs = promisify(execFile);
|
||||
const ps = (cmd: string) => runPs("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", cmd], { windowsHide: true, timeout: 30000 });
|
||||
// Win11 记事本窗口标题是「无标题 - Notepad」; 沙盒内 Start-Process notepad 别名解析失败, 用完整路径
|
||||
await ps("Start-Process $env:SystemRoot\\System32\\notepad.exe; $diag=''; $ok=$false; foreach($i in 1..24){ $procs=Get-Process Notepad -ErrorAction SilentlyContinue; if($procs){ $diag=\"procs:$($procs.Count) titles:$(($procs|ForEach-Object{$_.MainWindowTitle}) -join '|')\"; $p=Get-Process | Where-Object {$_.MainWindowTitle -like '*Notepad*'}; if($p){$ok=$true; break} } else { $diag='no-process' }; Start-Sleep -Milliseconds 500 }; if(-not $ok){ throw \"notepad not visible: $diag\" }");
|
||||
// 窗口标题随系统语言变化(英文 Notepad / 中文 记事本), 不硬编码, 启动后动态取实际标题
|
||||
await ps("Start-Process $env:SystemRoot\\System32\\notepad.exe; $diag=''; $ok=$false; foreach($i in 1..24){ $procs=Get-Process Notepad -ErrorAction SilentlyContinue; if($procs){ $p=$procs | Where-Object {$_.MainWindowTitle}; if($p){$ok=$true; break} } else { $diag='no-process' }; Start-Sleep -Milliseconds 500 }; if(-not $ok){ throw \"notepad not visible: $diag\" }");
|
||||
try {
|
||||
await invoke("Keyboard_Send_V1", { words: "目标窗口输入'测试", interval: "5", fuctype: "1", window_title: "Notepad" });
|
||||
const titlePs = "[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); (Get-Process Notepad | Where-Object { $_.MainWindowTitle } | Select-Object -First 1).MainWindowTitle";
|
||||
const title = (await ps(titlePs)).stdout.trim();
|
||||
expect(title.length).toBeGreaterThan(0);
|
||||
// GUI 焦点抢占受系统前台锁约束, 偶发失败属正常抖动, 有界重试
|
||||
let lastErr: unknown;
|
||||
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
||||
try {
|
||||
await invoke("Keyboard_Send_V1", { words: "目标窗口输入'测试", interval: "5", fuctype: "1", window_title: title });
|
||||
lastErr = undefined;
|
||||
break;
|
||||
} catch (error) {
|
||||
lastErr = error;
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
}
|
||||
}
|
||||
if (lastErr) throw lastErr;
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
// activateWindowScript 内部已校验前台句柄=目标窗口, 这里再确认记事本进程窗口标题匹配
|
||||
const fg = await ps("[Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); (Get-Process Notepad | Where-Object { $_.MainWindowTitle } | Select-Object -First 1).MainWindowTitle");
|
||||
expect(fg.stdout).toContain("Notepad");
|
||||
// activateWindowScript 内部已校验前台句柄=目标窗口, 这里再确认前台标题仍是记事本(输入后标题会带 * 脏标记前缀)
|
||||
const fg = await ps(titlePs);
|
||||
expect(fg.stdout.trim()).toContain(title);
|
||||
console.log("[键盘输入] 目标窗口激活 ✓ 前台:", fg.stdout.trim());
|
||||
} finally {
|
||||
await ps("Get-Process notepad* -ErrorAction SilentlyContinue | Stop-Process -Force").catch(() => {});
|
||||
|
||||
@@ -30,6 +30,17 @@ async function cursorPos(): Promise<[number, number]> {
|
||||
return stdout.trim().split(",").map(Number) as [number, number];
|
||||
}
|
||||
|
||||
/** 共享桌面环境下光标可能被其他进程短暂挪动,轮询等待落位(容差 px),超时返回最后一次读数 */
|
||||
async function waitForCursor(target: [number, number], tolerance = 0, attempts = 10, gapMs = 100): Promise<[number, number]> {
|
||||
let pos = await cursorPos();
|
||||
for (let i = 0; i < attempts; i += 1) {
|
||||
if (Math.abs(pos[0] - target[0]) <= tolerance && Math.abs(pos[1] - target[1]) <= tolerance) return pos;
|
||||
await new Promise((r) => setTimeout(r, gapMs));
|
||||
pos = await cursorPos();
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
async function invoke(componentId: string, args: Record<string, unknown>): Promise<unknown> {
|
||||
const handler = getHandler(componentId);
|
||||
expect(handler).toBeTypeOf("function");
|
||||
@@ -47,8 +58,7 @@ describe("移动鼠标组件真实冒烟", () => {
|
||||
// 用户屏幕框选生成的浮点对象坐标(此前直接报「鼠标坐标无效」)
|
||||
const out = await invoke("Mouse_MoveToV2", { location: { x: 1397, y: 227.60000610351562 }, position: "0", move_type: "2", speed: 100 }) as { x: number; y: number };
|
||||
expect(out).toEqual({ x: 1397, y: 228 });
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
const pos = await cursorPos();
|
||||
const pos = await waitForCursor([1397, 228]);
|
||||
expect(pos[0]).toBe(1397);
|
||||
expect(pos[1]).toBe(228);
|
||||
console.log("[移动鼠标] 对象坐标解析 + 瞬间移动落位 ✓ 实际光标:", pos);
|
||||
@@ -58,8 +68,7 @@ describe("移动鼠标组件真实冒烟", () => {
|
||||
original = await cursorPos();
|
||||
const out = await invoke("Mouse_MoveToV2", { location: "[300,300]", position: "0", move_type: "0", speed: 60 }) as { x: number; y: number };
|
||||
expect(out).toEqual({ x: 300, y: 300 });
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
const pos = await cursorPos();
|
||||
const pos = await waitForCursor([300, 300], 2);
|
||||
expect(Math.abs(pos[0] - 300)).toBeLessThanOrEqual(2);
|
||||
expect(Math.abs(pos[1] - 300)).toBeLessThanOrEqual(2);
|
||||
console.log("[移动鼠标] 模拟人工移动 ✓ 实际光标:", pos);
|
||||
@@ -69,7 +78,7 @@ describe("移动鼠标组件真实冒烟", () => {
|
||||
original = await cursorPos();
|
||||
const out = await invoke("Mouse_MoveToV2", { location: "[500,400]", position: "0", move_type: "1", speed: 80 }) as { x: number; y: number };
|
||||
expect(out).toEqual({ x: 500, y: 400 });
|
||||
const pos = await cursorPos();
|
||||
const pos = await waitForCursor([500, 400]);
|
||||
expect(pos[0]).toBe(500);
|
||||
expect(pos[1]).toBe(400);
|
||||
console.log("[移动鼠标] 匀速直线 + 字符串坐标 ✓ 实际光标:", pos);
|
||||
|
||||
@@ -121,21 +121,30 @@ describe("网络与消息组件冒烟", () => {
|
||||
|
||||
it("Http_Http_Request: PUT方法+Cookie+保存响应", async () => {
|
||||
const { server, port, requests } = await startHttpServer();
|
||||
const saveTo = "D:/office-test2/http-smoke-response.json";
|
||||
await run("Http_Http_Request", {
|
||||
url: `http://127.0.0.1:${port}/update`,
|
||||
method: "PUT",
|
||||
body: "raw-payload",
|
||||
cookies: JSON.stringify({ session: "s-77", lang: "zh" }),
|
||||
file_path: saveTo
|
||||
});
|
||||
server.close();
|
||||
expect(requests[0].method).toBe("PUT");
|
||||
expect(requests[0].headers.cookie).toBe("session=s-77; lang=zh");
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const saved = JSON.parse(readFileSync(saveTo, "utf8")) as { echo: string };
|
||||
expect(saved.echo).toBe("raw-payload");
|
||||
console.log("[Http_Http_Request] 通过: PUT+Cookie+响应落盘均正确");
|
||||
// 保存路径用系统临时目录,不硬编码盘符路径(可移植 + 沙盒可写)
|
||||
const { tmpdir } = await import("node:os");
|
||||
const { join } = await import("node:path");
|
||||
const { mkdtempSync, rmSync } = await import("node:fs");
|
||||
const saveDir = mkdtempSync(join(tmpdir(), "rpa-http-smoke-"));
|
||||
const saveTo = join(saveDir, "http-smoke-response.json");
|
||||
try {
|
||||
await run("Http_Http_Request", {
|
||||
url: `http://127.0.0.1:${port}/update`,
|
||||
method: "PUT",
|
||||
body: "raw-payload",
|
||||
cookies: JSON.stringify({ session: "s-77", lang: "zh" }),
|
||||
file_path: saveTo
|
||||
});
|
||||
server.close();
|
||||
expect(requests[0].method).toBe("PUT");
|
||||
expect(requests[0].headers.cookie).toBe("session=s-77; lang=zh");
|
||||
const { readFileSync } = await import("node:fs");
|
||||
const saved = JSON.parse(readFileSync(saveTo, "utf8")) as { echo: string };
|
||||
expect(saved.echo).toBe("raw-payload");
|
||||
console.log("[Http_Http_Request] 通过: PUT+Cookie+响应落盘均正确");
|
||||
} finally {
|
||||
rmSync(saveDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("Http 错误路径: 5xx 明确报错 + 超时报错", async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createServer, type Server } from "node:http";
|
||||
import { promises as fs } from "node:fs";
|
||||
import { mkdtempSync, promises as fs, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import type { CatalogComponent, FlowNode } from "../../shared/types";
|
||||
@@ -44,7 +45,8 @@ const PAGE_DISABLED = `<button id="disabledBtn" disabled>不可用</button>`;
|
||||
|
||||
let server: Server;
|
||||
let baseUrl = "";
|
||||
const outDir = "D:/office-test2/web-test";
|
||||
// 输出目录用系统临时目录,不硬编码盘符路径(可移植 + 沙盒可写)
|
||||
const outDir = mkdtempSync(path.join(tmpdir(), "rpa-web-smoke-"));
|
||||
const uploadFile = path.join(outDir, "upload-src.txt");
|
||||
let browser: BrowserLike;
|
||||
let ctx: RunContext;
|
||||
@@ -93,7 +95,8 @@ beforeAll(async () => {
|
||||
|
||||
afterAll(async () => {
|
||||
try { await browser?.close(); } catch { /* 进程退出兜底 */ }
|
||||
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||
await new Promise<void>((resolve) => { server.close(() => resolve()); });
|
||||
rmSync(outDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("网页自动化真实冒烟(Chrome 无头 + CDP)", () => {
|
||||
|
||||
Reference in new issue
Block a user