

























@@ -847,6 +847,19 @@ function spawnDaemon(params: {
847847return child.pid;
848848}
849849850+function sleep(ms: number) {
851+return new Promise((resolve) => {
852+setTimeout(resolve, ms);
853+});
854+}
855+856+function waitForChildExit(child: ChildProcess) {
857+return new Promise<number | null>((resolve, reject) => {
858+child.once("error", reject);
859+child.once("exit", resolve);
860+});
861+}
862+850863export function readLogTail(logPath: string, maxBytes = LOG_READY_TAIL_BYTES): string {
851864 let stat: fs.Stats;
852865try {
@@ -1011,50 +1024,122 @@ function writeSutConfig(params: {
10111024return { configPath, stateDir, tempRoot, workspace };
10121025}
101310261014-async function startLocalSut(params: {
1015- gatewayPort: number;
1016- groupId: string;
1017- mockResponseText: string;
1018- mockPort: number;
1019- outputDir: string;
1020- sutToken: string;
1021- testerId: string;
1022- repoRoot: string;
1027+type StartLocalSutDeps = {
1028+createGatewaySpawnSpec?: typeof createOpenClawGatewaySpawnSpec;
1029+drainUpdates?: typeof drainSutUpdates;
1030+spawnLoggedCommand?: typeof spawnLogged;
1031+waitForOutputReady?: typeof waitForOutput;
1032+writeConfig?: typeof writeSutConfig;
1033+};
1034+1035+export async function startLocalSut(
1036+params: {
1037+ gatewayPort: number;
1038+ groupId: string;
1039+ mockResponseText: string;
1040+ mockPort: number;
1041+ outputDir: string;
1042+ sutToken: string;
1043+ testerId: string;
1044+ repoRoot: string;
1045+},
1046+ deps: StartLocalSutDeps = {},
1047+) {
1048+const drainUpdates = deps.drainUpdates ?? drainSutUpdates;
1049+const writeConfig = deps.writeConfig ?? writeSutConfig;
1050+const spawnLoggedCommand = deps.spawnLoggedCommand ?? spawnLogged;
1051+const waitForOutputReady = deps.waitForOutputReady ?? waitForOutput;
1052+const createGatewaySpawnSpec = deps.createGatewaySpawnSpec ?? createOpenClawGatewaySpawnSpec;
1053+let gateway: ReturnType<typeof spawnLogged> | undefined;
1054+let mock: ReturnType<typeof spawnLogged> | undefined;
1055+try {
1056+const drained = await drainUpdates(params.sutToken);
1057+const config = writeConfig(params);
1058+const requestLog = path.join(params.outputDir, "mock-openai-requests.ndjson");
1059+mock = spawnLoggedCommand("node", ["scripts/e2e/mock-openai-server.mjs"], {
1060+cwd: params.repoRoot,
1061+env: mockServerEnv({ ...params, requestLog }),
1062+});
1063+await waitForOutputReady(
1064+mock.child,
1065+/mock-openai listening/u,
1066+() => mock.output,
1067+"mock-openai",
1068+10_000,
1069+);
1070+const gatewaySpec = createGatewaySpawnSpec({
1071+env: gatewayEnv({ ...config, sutToken: params.sutToken }),
1072+gatewayPort: params.gatewayPort,
1073+repoRoot: params.repoRoot,
1074+});
1075+gateway = spawnLoggedCommand(gatewaySpec.command, gatewaySpec.args, gatewaySpec.options);
1076+await waitForOutputReady(
1077+gateway.child,
1078+/\[gateway\] ready/u,
1079+() => gateway.output,
1080+"gateway",
1081+60_000,
1082+);
1083+return {
1084+ ...config,
1085+ drained,
1086+gateway: gateway.child,
1087+get gatewayLog() {
1088+return gateway.output;
1089+},
1090+mock: mock.child,
1091+get mockLog() {
1092+return mock.output;
1093+},
1094+ requestLog,
1095+};
1096+} catch (error) {
1097+killTree(gateway?.child);
1098+killTree(mock?.child);
1099+throw error;
1100+}
1101+}
1102+1103+export async function recordProbeVideo(params: {
1104+ crabboxBin: string;
1105+ cwd: string;
1106+ durationSeconds: number;
1107+ leaseId: string;
1108+ outputPath: string;
1109+ provider: string;
1110+ runProbe: () => Promise<void>;
1111+startDelayMs?: number;
1112+ target: string;
10231113}) {
1024-const drained = await drainSutUpdates(params.sutToken);
1025-const config = writeSutConfig(params);
1026-const requestLog = path.join(params.outputDir, "mock-openai-requests.ndjson");
1027-const mock = spawnLogged("node", ["scripts/e2e/mock-openai-server.mjs"], {
1028-cwd: params.repoRoot,
1029-env: mockServerEnv({ ...params, requestLog }),
1030-});
1031-await waitForOutput(
1032-mock.child,
1033-/mock-openai listening/u,
1034-() => mock.output,
1035-"mock-openai",
1036-10_000,
1037-);
1038-const gatewaySpec = createOpenClawGatewaySpawnSpec({
1039-env: gatewayEnv({ ...config, sutToken: params.sutToken }),
1040-gatewayPort: params.gatewayPort,
1041-repoRoot: params.repoRoot,
1042-});
1043-const gateway = spawnLogged(gatewaySpec.command, gatewaySpec.args, gatewaySpec.options);
1044-await waitForOutput(gateway.child, /\[gateway\] ready/u, () => gateway.output, "gateway", 60_000);
1045-return {
1046- ...config,
1047- drained,
1048-gateway: gateway.child,
1049-get gatewayLog() {
1050-return gateway.output;
1051-},
1052-mock: mock.child,
1053-get mockLog() {
1054-return mock.output;
1055-},
1056- requestLog,
1057-};
1114+ let recording: ChildProcess | undefined;
1115+try {
1116+recording = spawn(
1117+params.crabboxBin,
1118+[
1119+"artifacts",
1120+"video",
1121+"--provider",
1122+params.provider,
1123+"--target",
1124+params.target,
1125+"--id",
1126+params.leaseId,
1127+"--duration",
1128+`${params.durationSeconds}s`,
1129+"--output",
1130+params.outputPath,
1131+],
1132+{ cwd: params.cwd, stdio: "inherit" },
1133+);
1134+await sleep(params.startDelayMs ?? 3_000);
1135+await params.runProbe();
1136+const recordCode = await waitForChildExit(recording);
1137+if (recordCode !== 0) {
1138+throw new Error(`Crabbox recording failed with exit code ${recordCode ?? "unknown"}.`);
1139+}
1140+} finally {
1141+killTree(recording);
1142+}
10581143}
1059114410601145async function startLocalSutDaemon(params: {
@@ -2512,34 +2597,18 @@ async function main() {
25122597await sshRun(root, inspect, `bash ${REMOTE_ROOT}/authorize-desktop.sh`);
25132598await sshRun(root, inspect, `bash ${REMOTE_ROOT}/select-desktop-chat.sh`);
25142599const videoPath = path.join(outputDir, "telegram-user-crabbox-proof.mp4");
2515-const recording = spawn(
2516-opts.crabboxBin,
2517-[
2518-"artifacts",
2519-"video",
2520-"--provider",
2521-opts.provider,
2522-"--target",
2523-opts.target,
2524-"--id",
2525-leaseId,
2526-"--duration",
2527-`${opts.recordSeconds}s`,
2528-"--output",
2529-videoPath,
2530-],
2531-{ cwd: root, stdio: "inherit" },
2532-);
2533-await new Promise((resolve) => {
2534-setTimeout(resolve, 3_000);
2535-});
2536-await sshRun(root, inspect, `bash ${REMOTE_ROOT}/remote-probe.sh`);
2537-const recordCode = await new Promise<number | null>((resolve) => {
2538-recording.on("exit", resolve);
2600+await recordProbeVideo({
2601+crabboxBin: opts.crabboxBin,
2602+cwd: root,
2603+durationSeconds: opts.recordSeconds,
2604+ leaseId,
2605+outputPath: videoPath,
2606+provider: opts.provider,
2607+runProbe: async () => {
2608+await sshRun(root, inspect, `bash ${REMOTE_ROOT}/remote-probe.sh`);
2609+},
2610+target: opts.target,
25392611});
2540-if (recordCode !== 0) {
2541-throw new Error(`Crabbox recording failed with exit code ${recordCode ?? "unknown"}.`);
2542-}
25432612const motionVideoPath = path.join(outputDir, "telegram-user-crabbox-proof-motion.mp4");
25442613const motionGifPath = path.join(outputDir, "telegram-user-crabbox-proof-motion.gif");
25452614summary.mediaPreview = await createMotionPreview({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。