From a2bf7f5e6534005462e9369525b39b45fa99c50d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 06:18:21 +0000 Subject: [PATCH 1/3] Initial plan From dd6848c9c7a528a64967d12ddbbdbef5f8e13e27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 06:28:38 +0000 Subject: [PATCH 2/3] fix: extract shared process_runner.cjs from claude and copilot harnesses Agent-Logs-Url: https://github.com/github/gh-aw/sessions/ed8c699b-18bb-4843-9921-683c3fb4ad61 Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/claude_harness.cjs | 106 +----------- actions/setup/js/copilot_harness.cjs | 111 +------------ actions/setup/js/process_runner.cjs | 131 +++++++++++++++ actions/setup/js/process_runner.test.cjs | 195 +++++++++++++++++++++++ 4 files changed, 332 insertions(+), 211 deletions(-) create mode 100644 actions/setup/js/process_runner.cjs create mode 100644 actions/setup/js/process_runner.test.cjs diff --git a/actions/setup/js/claude_harness.cjs b/actions/setup/js/claude_harness.cjs index f0b65fe269..eaf3522653 100644 --- a/actions/setup/js/claude_harness.cjs +++ b/actions/setup/js/claude_harness.cjs @@ -32,8 +32,8 @@ "use strict"; -const { spawn } = require("child_process"); const fs = require("fs"); +const { runProcess, formatDuration, sleep } = require("./process_runner.cjs"); const { AWF_API_PROXY_REFLECT_URL, AWF_REFLECT_OUTPUT_PATH, @@ -110,108 +110,6 @@ function isMaxTurnsExit(output) { return MAX_TURNS_EXIT_PATTERN.test(output); } -/** - * Sleep for a specified duration - * @param {number} ms - Duration in milliseconds - * @returns {Promise} - */ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * Format elapsed milliseconds as a human-readable string (e.g. "3m 12s"). - * @param {number} ms - * @returns {string} - */ -function formatDuration(ms) { - const totalSeconds = Math.floor(ms / 1000); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - if (minutes > 0) { - return `${minutes}m ${seconds}s`; - } - return `${seconds}s`; -} - -/** - * Run a command with the given arguments, transparently forwarding stdin/stdout/stderr. - * Also collects output for error pattern detection. - * - * @param {string} command - The executable to run - * @param {string[]} args - Arguments to pass to the command - * @param {number} attempt - Current attempt index (0-based), used for logging - * @param {string[]} [logArgs] - Safe arg list used only for logging; defaults to `args`. - * Pass a redacted copy to avoid leaking prompt content into logs. - * @returns {Promise<{exitCode: number, output: string, hasOutput: boolean, durationMs: number}>} - */ -function runProcess(command, args, attempt, logArgs) { - return new Promise(resolve => { - const startTime = Date.now(); - const argsForLog = logArgs ?? args; - log(`attempt ${attempt + 1}: spawning: ${command} ${argsForLog.join(" ").substring(0, 200)}`); - - const child = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - env: process.env, - }); - - log(`attempt ${attempt + 1}: process started (pid=${child.pid ?? "unknown"})`); - - let collectedOutput = ""; - let hasOutput = false; - let stdoutBytes = 0; - let stderrBytes = 0; - - child.stdout.on( - "data", - /** @param {Buffer} data */ data => { - hasOutput = true; - stdoutBytes += data.length; - collectedOutput += data.toString(); - process.stdout.write(data); - } - ); - - child.stderr.on( - "data", - /** @param {Buffer} data */ data => { - hasOutput = true; - stderrBytes += data.length; - collectedOutput += data.toString(); - process.stderr.write(data); - } - ); - - child.on("exit", (code, signal) => { - log(`attempt ${attempt + 1}: process exit event` + ` exitCode=${code ?? 1}` + (signal ? ` signal=${signal}` : "")); - }); - - // Resolve on 'close', not 'exit', to ensure stdio streams are fully drained. - child.on("close", (code, signal) => { - const durationMs = Date.now() - startTime; - const exitCode = code ?? 1; - log(`attempt ${attempt + 1}: process closed` + ` exitCode=${exitCode}` + (signal ? ` signal=${signal}` : "") + ` duration=${formatDuration(durationMs)}` + ` stdout=${stdoutBytes}B stderr=${stderrBytes}B hasOutput=${hasOutput}`); - resolve({ exitCode, output: collectedOutput, hasOutput, durationMs }); - }); - - child.on("error", err => { - const durationMs = Date.now() - startTime; - // prettier-ignore - const errno = /** @type {NodeJS.ErrnoException} */ (err); - const errCode = errno.code ?? "unknown"; - const errSyscall = errno.syscall ?? "unknown"; - log(`attempt ${attempt + 1}: failed to start process '${command}': ${err.message}` + ` (code=${errCode} syscall=${errSyscall})`); - resolve({ - exitCode: 1, - output: collectedOutput, - hasOutput, - durationMs, - }); - }); - }); -} - /** * Resolve --prompt-file arguments for the initial Claude run. * Strips the --prompt-file pair from args and appends the file content @@ -347,7 +245,7 @@ async function main() { log(`retry ${attempt}/${MAX_RETRIES}: woke up, next delay cap will be ${Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS)}ms`); } - const result = await runProcess(command, currentArgs, attempt, logArgs); + const result = await runProcess({ command, args: currentArgs, attempt, log, logArgs }); lastExitCode = result.exitCode; // Success — stop retrying diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index d189c6d2b9..ac2bda4406 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -38,9 +38,9 @@ "use strict"; -const { spawn } = require("child_process"); const fs = require("fs"); const path = require("path"); +const { runProcess, formatDuration, sleep } = require("./process_runner.cjs"); const { AWF_API_PROXY_REFLECT_URL, AWF_REFLECT_OUTPUT_PATH, @@ -163,15 +163,6 @@ function isNullTypeToolCallError(output) { return NULL_TYPE_TOOL_CALL_PATTERN.test(output); } -/** - * Sleep for a specified duration - * @param {number} ms - Duration in milliseconds - * @returns {Promise} - */ -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - /** * Build a structured report_incomplete payload for infrastructure failures. * @param {string} details @@ -247,102 +238,6 @@ async function checkCommandAccessible(command) { } } -/** - * Format elapsed milliseconds as a human-readable string (e.g. "3m 12s"). - * @param {number} ms - * @returns {string} - */ -function formatDuration(ms) { - const totalSeconds = Math.floor(ms / 1000); - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - if (minutes > 0) { - return `${minutes}m ${seconds}s`; - } - return `${seconds}s`; -} - -/** - * Run a command with the given arguments, transparently forwarding stdin/stdout/stderr. - * Also collects output for error pattern detection. - * - * @param {string} command - The executable to run - * @param {string[]} args - Arguments to pass to the command - * @param {number} attempt - Current attempt index (0-based), used for logging - * @returns {Promise<{exitCode: number, output: string, hasOutput: boolean, durationMs: number}>} - */ -function runProcess(command, args, attempt) { - return new Promise(resolve => { - const startTime = Date.now(); - // Redact --prompt value from logs to avoid leaking prompt content - const safeArgs = args.map((arg, i) => (args[i - 1] === "--prompt" || args[i - 1] === "-p" ? "" : arg)); - log(`attempt ${attempt + 1}: spawning: ${command} ${safeArgs.join(" ")}`); - - const child = spawn(command, args, { - stdio: ["inherit", "pipe", "pipe"], - env: process.env, - }); - - log(`attempt ${attempt + 1}: process started (pid=${child.pid ?? "unknown"})`); - - let collectedOutput = ""; - let hasOutput = false; - let stdoutBytes = 0; - let stderrBytes = 0; - - child.stdout.on( - "data", - /** @param {Buffer} data */ data => { - hasOutput = true; - stdoutBytes += data.length; - collectedOutput += data.toString(); - process.stdout.write(data); - } - ); - - child.stderr.on( - "data", - /** @param {Buffer} data */ data => { - hasOutput = true; - stderrBytes += data.length; - collectedOutput += data.toString(); - process.stderr.write(data); - } - ); - - child.on("exit", (code, signal) => { - // Log the exit event early; the promise is resolved in 'close' (see below) once stdio - // streams are fully drained so that collectedOutput and hasOutput are complete. - log(`attempt ${attempt + 1}: process exit event` + ` exitCode=${code ?? 1}` + (signal ? ` signal=${signal}` : "")); - }); - - // Resolve on 'close', not 'exit'. 'close' fires after stdio streams are fully drained, - // guaranteeing that collectedOutput and hasOutput are complete before we make the retry - // decision and that the final exit code is faithfully propagated. - child.on("close", (code, signal) => { - const durationMs = Date.now() - startTime; - const exitCode = code ?? 1; - log(`attempt ${attempt + 1}: process closed` + ` exitCode=${exitCode}` + (signal ? ` signal=${signal}` : "") + ` duration=${formatDuration(durationMs)}` + ` stdout=${stdoutBytes}B stderr=${stderrBytes}B hasOutput=${hasOutput}`); - resolve({ exitCode, output: collectedOutput, hasOutput, durationMs }); - }); - - child.on("error", err => { - const durationMs = Date.now() - startTime; - // prettier-ignore - const errno = /** @type {NodeJS.ErrnoException} */ (err); - const errCode = errno.code ?? "unknown"; - const errSyscall = errno.syscall ?? "unknown"; - log(`attempt ${attempt + 1}: failed to start process '${command}': ${err.message}` + ` (code=${errCode} syscall=${errSyscall})`); - resolve({ - exitCode: 1, - output: collectedOutput, - hasOutput, - durationMs, - }); - }); - }); -} - /** * Build a compact fallback prompt that asks the agent to read instructions from disk. * @param {string} promptFile @@ -439,7 +334,9 @@ async function main() { log(`retry ${attempt}/${MAX_RETRIES}: woke up, next delay cap will be ${Math.min(delay * BACKOFF_MULTIPLIER, MAX_DELAY_MS)}ms`); } - const result = await runProcess(command, currentArgs, attempt); + // Redact --prompt / -p value from logs to avoid leaking prompt content + const safeArgs = currentArgs.map((arg, i) => (currentArgs[i - 1] === "--prompt" || currentArgs[i - 1] === "-p" ? "" : arg)); + const result = await runProcess({ command, args: currentArgs, attempt, log, logArgs: safeArgs }); lastExitCode = result.exitCode; // Success — record exit code and stop retrying diff --git a/actions/setup/js/process_runner.cjs b/actions/setup/js/process_runner.cjs new file mode 100644 index 0000000000..3d2253cfe5 --- /dev/null +++ b/actions/setup/js/process_runner.cjs @@ -0,0 +1,131 @@ +// @ts-check + +/** + * Shared process runner utilities for agent harnesses. + * + * Provides a common runProcess helper used by both the Claude and Copilot + * harnesses to spawn child processes, forward stdin/stdout/stderr, collect + * output for retry decisions, track byte counts, and surface spawn errors. + * + * Each harness retains its own logging prefix and argument-redaction logic; + * the caller passes a log function and an optional logArgs array so that + * sensitive values (e.g. prompt text) are never written to logs. + */ + +"use strict"; + +const { spawn } = require("child_process"); + +/** + * Format elapsed milliseconds as a human-readable string (e.g. "3m 12s"). + * @param {number} ms + * @returns {string} + */ +function formatDuration(ms) { + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + if (minutes > 0) { + return `${minutes}m ${seconds}s`; + } + return `${seconds}s`; +} + +/** + * Sleep for a specified duration. + * @param {number} ms - Duration in milliseconds + * @returns {Promise} + */ +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Run a command with the given arguments, transparently forwarding stdin/stdout/stderr. + * Also collects combined stdout+stderr output for error pattern detection. + * + * @param {{ + * command: string, + * args: string[], + * attempt: number, + * log: (message: string) => void, + * logArgs?: string[] + * }} options + * - command - The executable to run + * - args - Arguments to pass to the command + * - attempt - Current attempt index (0-based), used for logging + * - log - Caller-supplied logging function (harness-specific prefix) + * - logArgs - Safe arg list used only for logging; defaults to `args`. + * Pass a redacted copy to avoid leaking sensitive values. + * @returns {Promise<{exitCode: number, output: string, hasOutput: boolean, durationMs: number}>} + */ +function runProcess({ command, args, attempt, log, logArgs }) { + return new Promise(resolve => { + const startTime = Date.now(); + const argsForLog = logArgs ?? args; + log(`attempt ${attempt + 1}: spawning: ${command} ${argsForLog.join(" ").substring(0, 200)}`); + + const child = spawn(command, args, { + stdio: ["inherit", "pipe", "pipe"], + env: process.env, + }); + + log(`attempt ${attempt + 1}: process started (pid=${child.pid ?? "unknown"})`); + + let collectedOutput = ""; + let hasOutput = false; + let stdoutBytes = 0; + let stderrBytes = 0; + + child.stdout.on( + "data", + /** @param {Buffer} data */ data => { + hasOutput = true; + stdoutBytes += data.length; + collectedOutput += data.toString(); + process.stdout.write(data); + } + ); + + child.stderr.on( + "data", + /** @param {Buffer} data */ data => { + hasOutput = true; + stderrBytes += data.length; + collectedOutput += data.toString(); + process.stderr.write(data); + } + ); + + child.on("exit", (code, signal) => { + log(`attempt ${attempt + 1}: process exit event` + ` exitCode=${code ?? 1}` + (signal ? ` signal=${signal}` : "")); + }); + + // Resolve on 'close', not 'exit', to ensure stdio streams are fully drained. + child.on("close", (code, signal) => { + const durationMs = Date.now() - startTime; + const exitCode = code ?? 1; + log(`attempt ${attempt + 1}: process closed` + ` exitCode=${exitCode}` + (signal ? ` signal=${signal}` : "") + ` duration=${formatDuration(durationMs)}` + ` stdout=${stdoutBytes}B stderr=${stderrBytes}B hasOutput=${hasOutput}`); + resolve({ exitCode, output: collectedOutput, hasOutput, durationMs }); + }); + + child.on("error", err => { + const durationMs = Date.now() - startTime; + // prettier-ignore + const errno = /** @type {NodeJS.ErrnoException} */ (err); + const errCode = errno.code ?? "unknown"; + const errSyscall = errno.syscall ?? "unknown"; + log(`attempt ${attempt + 1}: failed to start process '${command}': ${err.message}` + ` (code=${errCode} syscall=${errSyscall})`); + resolve({ + exitCode: 1, + output: collectedOutput, + hasOutput, + durationMs, + }); + }); + }); +} + +if (typeof module !== "undefined" && module.exports) { + module.exports = { runProcess, formatDuration, sleep }; +} diff --git a/actions/setup/js/process_runner.test.cjs b/actions/setup/js/process_runner.test.cjs new file mode 100644 index 0000000000..6b640e1601 --- /dev/null +++ b/actions/setup/js/process_runner.test.cjs @@ -0,0 +1,195 @@ +import { describe, it, expect, vi } from "vitest"; +import { createRequire } from "module"; + +const require = createRequire(import.meta.url); +const { runProcess, formatDuration, sleep } = require("./process_runner.cjs"); + +describe("process_runner.cjs", () => { + describe("formatDuration", () => { + it("formats zero milliseconds as 0s", () => { + expect(formatDuration(0)).toBe("0s"); + }); + + it("formats sub-minute durations as seconds only", () => { + expect(formatDuration(1000)).toBe("1s"); + expect(formatDuration(45000)).toBe("45s"); + expect(formatDuration(59999)).toBe("59s"); + }); + + it("formats exactly one minute", () => { + expect(formatDuration(60000)).toBe("1m 0s"); + }); + + it("formats minutes and seconds", () => { + expect(formatDuration(192000)).toBe("3m 12s"); + expect(formatDuration(125500)).toBe("2m 5s"); + }); + + it("truncates sub-second precision", () => { + expect(formatDuration(1999)).toBe("1s"); + }); + }); + + describe("sleep", () => { + it("resolves after the given delay", async () => { + const start = Date.now(); + await sleep(50); + expect(Date.now() - start).toBeGreaterThanOrEqual(40); + }); + + it("resolves immediately for 0ms", async () => { + await expect(sleep(0)).resolves.toBeUndefined(); + }); + }); + + describe("runProcess", () => { + it("resolves with exitCode 0 for a successful command", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(0)"], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.exitCode).toBe(0); + expect(result.durationMs).toBeGreaterThanOrEqual(0); + }); + + it("resolves with the actual non-zero exit code on failure", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(42)"], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.exitCode).toBe(42); + }); + + it("collects stdout output and sets hasOutput", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", 'process.stdout.write("hello stdout"); process.exit(0)'], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.hasOutput).toBe(true); + expect(result.output).toContain("hello stdout"); + }); + + it("collects stderr output and sets hasOutput", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", 'process.stderr.write("hello stderr"); process.exit(1)'], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.hasOutput).toBe(true); + expect(result.output).toContain("hello stderr"); + }); + + it("sets hasOutput false when no output is produced", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(1)"], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.hasOutput).toBe(false); + expect(result.output).toBe(""); + }); + + it("logs spawning with logArgs instead of args when provided", async () => { + const logs = []; + await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(0)"], + attempt: 0, + log: msg => logs.push(msg), + logArgs: [""], + }); + const spawnLog = logs.find(l => l.includes("spawning")); + expect(spawnLog).toContain(""); + expect(spawnLog).not.toContain("-e"); + }); + + it("falls back to args for logging when logArgs is not provided", async () => { + const logs = []; + await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(0)"], + attempt: 0, + log: msg => logs.push(msg), + }); + const spawnLog = logs.find(l => l.includes("spawning")); + expect(spawnLog).toContain("-e"); + }); + + it("uses the attempt number in log messages", async () => { + const logs = []; + await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(0)"], + attempt: 2, + log: msg => logs.push(msg), + }); + expect(logs.some(l => l.includes("attempt 3"))).toBe(true); + }); + + it("resolves with exitCode 1 and hasOutput false when command is not found", async () => { + const logs = []; + const result = await runProcess({ + command: "/nonexistent-binary-xyz", + args: [], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.exitCode).toBe(1); + const errorLog = logs.find(l => l.includes("failed to start process")); + expect(errorLog).toBeTruthy(); + }); + + it("collects combined stdout and stderr in output", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", 'process.stdout.write("out"); process.stderr.write("err"); process.exit(0)'], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(result.output).toContain("out"); + expect(result.output).toContain("err"); + }); + + it("resolves with durationMs as a non-negative number", async () => { + const logs = []; + const result = await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(0)"], + attempt: 0, + log: msg => logs.push(msg), + }); + expect(typeof result.durationMs).toBe("number"); + expect(result.durationMs).toBeGreaterThanOrEqual(0); + }); + + it("truncates logArgs to 200 chars in spawn log", async () => { + const logs = []; + const longArg = "x".repeat(300); + await runProcess({ + command: process.execPath, + args: ["-e", "process.exit(0)"], + attempt: 0, + log: msg => logs.push(msg), + logArgs: [longArg], + }); + const spawnLog = logs.find(l => l.includes("spawning")); + // Extract the args portion from the log line (everything after "spawning: ") + const afterCommand = spawnLog?.replace(/.*spawning: \S+ /, "") ?? ""; + expect(afterCommand.length).toBeLessThanOrEqual(200); + }); + }); +}); From 9810f54d75c1bf6598937bd193b7f30610e5cfca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 08:19:18 +0000 Subject: [PATCH 3/3] fix: address PR review comments on process_runner.cjs Agent-Logs-Url: https://github.com/github/gh-aw/sessions/9fc604ee-8594-45bc-aaa2-7361445da690 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/process_runner.cjs | 15 +++++++++++++-- actions/setup/js/process_runner.test.cjs | 20 +++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/actions/setup/js/process_runner.cjs b/actions/setup/js/process_runner.cjs index 3d2253cfe5..f77998247a 100644 --- a/actions/setup/js/process_runner.cjs +++ b/actions/setup/js/process_runner.cjs @@ -62,6 +62,17 @@ function sleep(ms) { function runProcess({ command, args, attempt, log, logArgs }) { return new Promise(resolve => { const startTime = Date.now(); + // Guard against the promise being settled more than once. On some systems Node + // emits 'close' after 'error' (or vice-versa); only the first terminal event should + // log and resolve so callers receive a deterministic result. + let settled = false; + /** @param {{exitCode: number, output: string, hasOutput: boolean, durationMs: number}} result */ + function settle(result) { + if (settled) return; + settled = true; + resolve(result); + } + const argsForLog = logArgs ?? args; log(`attempt ${attempt + 1}: spawning: ${command} ${argsForLog.join(" ").substring(0, 200)}`); @@ -106,7 +117,7 @@ function runProcess({ command, args, attempt, log, logArgs }) { const durationMs = Date.now() - startTime; const exitCode = code ?? 1; log(`attempt ${attempt + 1}: process closed` + ` exitCode=${exitCode}` + (signal ? ` signal=${signal}` : "") + ` duration=${formatDuration(durationMs)}` + ` stdout=${stdoutBytes}B stderr=${stderrBytes}B hasOutput=${hasOutput}`); - resolve({ exitCode, output: collectedOutput, hasOutput, durationMs }); + settle({ exitCode, output: collectedOutput, hasOutput, durationMs }); }); child.on("error", err => { @@ -116,7 +127,7 @@ function runProcess({ command, args, attempt, log, logArgs }) { const errCode = errno.code ?? "unknown"; const errSyscall = errno.syscall ?? "unknown"; log(`attempt ${attempt + 1}: failed to start process '${command}': ${err.message}` + ` (code=${errCode} syscall=${errSyscall})`); - resolve({ + settle({ exitCode: 1, output: collectedOutput, hasOutput, diff --git a/actions/setup/js/process_runner.test.cjs b/actions/setup/js/process_runner.test.cjs index 6b640e1601..2349391a26 100644 --- a/actions/setup/js/process_runner.test.cjs +++ b/actions/setup/js/process_runner.test.cjs @@ -31,10 +31,15 @@ describe("process_runner.cjs", () => { }); describe("sleep", () => { - it("resolves after the given delay", async () => { - const start = Date.now(); - await sleep(50); - expect(Date.now() - start).toBeGreaterThanOrEqual(40); + it("returns a promise that resolves after the given delay", async () => { + vi.useFakeTimers(); + try { + const promise = sleep(1000); + vi.advanceTimersByTime(1000); + await expect(promise).resolves.toBeUndefined(); + } finally { + vi.useRealTimers(); + } }); it("resolves immediately for 0ms", async () => { @@ -187,9 +192,10 @@ describe("process_runner.cjs", () => { logArgs: [longArg], }); const spawnLog = logs.find(l => l.includes("spawning")); - // Extract the args portion from the log line (everything after "spawning: ") - const afterCommand = spawnLog?.replace(/.*spawning: \S+ /, "") ?? ""; - expect(afterCommand.length).toBeLessThanOrEqual(200); + // logArgs is a single arg made entirely of 'x' characters. After truncation to 200 + // chars the spawn log line must end with at most 200 consecutive x's. + const trailingXs = spawnLog?.match(/x+$/)?.[0] ?? ""; + expect(trailingXs.length).toBeLessThanOrEqual(200); }); }); });