diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index 513de73eb8f..82d986c5337 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -397,6 +397,7 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ ["scripts/docker-e2e-timings.mjs", ["test/scripts/docker-e2e-helper-cli.test.ts"]], ["scripts/kova-ci-summary.mjs", ["test/scripts/kova-ci-summary.test.ts"]], ["scripts/test-extension-batch.mjs", ["test/scripts/test-extension.test.ts"]], + ["scripts/zai-fallback-repro.ts", ["test/scripts/zai-fallback-repro.test.ts"]], ["scripts/lib/extension-test-plan.mjs", ["test/scripts/test-extension.test.ts"]], ["scripts/lib/vitest-batch-runner.mjs", ["test/scripts/test-extension.test.ts"]], ["scripts/lib/ci-node-test-plan.mjs", ["test/scripts/ci-node-test-plan.test.ts"]], @@ -461,6 +462,7 @@ const TOOLING_TEST_TARGETS = new Map([ "test/scripts/vitest-local-scheduling.test.ts", ["test/scripts/vitest-local-scheduling.test.ts"], ], + ["test/scripts/zai-fallback-repro.test.ts", ["test/scripts/zai-fallback-repro.test.ts"]], ]); const GROUP_VISIBLE_REPLY_TEST_TARGETS = [ "src/auto-reply/reply/dispatch-acp.test.ts", diff --git a/scripts/zai-fallback-repro.ts b/scripts/zai-fallback-repro.ts index 0585764e173..87945c73021 100644 --- a/scripts/zai-fallback-repro.ts +++ b/scripts/zai-fallback-repro.ts @@ -13,6 +13,11 @@ type RunResult = { stderr: string; }; +type OutputCapture = { + text: string; + truncatedChars: number; +}; + type PnpmCommand = { args: string[]; command: string; @@ -29,11 +34,33 @@ type ResolvePnpmCommandOptions = { platform?: NodeJS.Platform; }; +const COMMAND_OUTPUT_MAX_CHARS = 512 * 1024; + function resolveEnvValue(env: NodeJS.ProcessEnv, name: string): string | undefined { const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase()); return key === undefined ? undefined : env[key]; } +export function appendBoundedReproOutput( + capture: OutputCapture, + chunk: unknown, + maxChars = COMMAND_OUTPUT_MAX_CHARS, +): OutputCapture { + const nextText = capture.text + String(chunk); + if (nextText.length <= maxChars) { + return { text: nextText, truncatedChars: capture.truncatedChars }; + } + const truncatedChars = capture.truncatedChars + nextText.length - maxChars; + return { text: nextText.slice(-maxChars), truncatedChars }; +} + +function formatBoundedReproOutput(capture: OutputCapture): string { + if (capture.truncatedChars === 0) { + return capture.text; + } + return `[output truncated ${capture.truncatedChars} chars; showing tail]\n${capture.text}`; +} + export function resolveZaiFallbackPnpmCommand( args: string[], options: ResolvePnpmCommandOptions = {}, @@ -83,25 +110,31 @@ async function runCommand( stdio: ["ignore", "pipe", "pipe"], windowsVerbatimArguments: command.windowsVerbatimArguments, }); - let stdout = ""; - let stderr = ""; + let stdout: OutputCapture = { text: "", truncatedChars: 0 }; + let stderr: OutputCapture = { text: "", truncatedChars: 0 }; child.stdout.on("data", (chunk) => { const text = String(chunk); - stdout += text; + stdout = appendBoundedReproOutput(stdout, text); process.stdout.write(text); }); child.stderr.on("data", (chunk) => { const text = String(chunk); - stderr += text; + stderr = appendBoundedReproOutput(stderr, text); process.stderr.write(text); }); child.on("error", (err) => reject(err)); child.on("close", (code, signal) => { + const result = { + code, + signal, + stdout: formatBoundedReproOutput(stdout), + stderr: formatBoundedReproOutput(stderr), + }; if (code === 0) { - resolve({ code, signal, stdout, stderr }); + resolve(result); return; } - resolve({ code, signal, stdout, stderr }); + resolve(result); const summary = signal ? `${label} exited with signal ${signal}` : `${label} exited with code ${code}`; diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 70ec10801fd..c434c19d159 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -214,6 +214,13 @@ describe("scripts/test-projects changed-target routing", () => { }); }); + it("routes Z.AI fallback repro script changes through its regression test", () => { + expect(resolveChangedTestTargetPlan(["scripts/zai-fallback-repro.ts"])).toEqual({ + mode: "targets", + targets: ["test/scripts/zai-fallback-repro.test.ts"], + }); + }); + it("routes group visible reply config changes through channel delivery regressions", () => { expect( resolveChangedTestTargetPlan([ diff --git a/test/scripts/zai-fallback-repro.test.ts b/test/scripts/zai-fallback-repro.test.ts index c56c8d7a010..bb19367ea7e 100644 --- a/test/scripts/zai-fallback-repro.test.ts +++ b/test/scripts/zai-fallback-repro.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { resolveZaiFallbackPnpmCommand } from "../../scripts/zai-fallback-repro.ts"; +import { + appendBoundedReproOutput, + resolveZaiFallbackPnpmCommand, +} from "../../scripts/zai-fallback-repro.ts"; describe("zai fallback repro command resolution", () => { it("wraps Windows pnpm.cmd without Node shell argv", () => { @@ -24,4 +27,12 @@ describe("zai fallback repro command resolution", () => { windowsVerbatimArguments: true, }); }); + + it("keeps only a bounded child output tail", () => { + const first = appendBoundedReproOutput({ text: "", truncatedChars: 0 }, "abcdef", 5); + const second = appendBoundedReproOutput(first, "ghij", 5); + + expect(first).toEqual({ text: "bcdef", truncatedChars: 1 }); + expect(second).toEqual({ text: "fghij", truncatedChars: 5 }); + }); });