diff --git a/scripts/check-gateway-watch-regression.mjs b/scripts/check-gateway-watch-regression.mjs index 0dd7f864d46..285538a6618 100644 --- a/scripts/check-gateway-watch-regression.mjs +++ b/scripts/check-gateway-watch-regression.mjs @@ -42,6 +42,33 @@ const WATCH_GATEWAY_SKIP_ENV = { NODE_ENV: "test", }; +export const WATCH_LOG_CAPTURE_MAX_CHARS = 2 * 1024 * 1024; +const WATCH_BUILD_DETECTION_MAX_CHARS = 4096; + +export function appendBoundedWatchLog(current, chunk, maxChars = WATCH_LOG_CAPTURE_MAX_CHARS) { + const next = `${current}${String(chunk)}`; + if (next.length <= maxChars) { + return { text: next, truncated: false }; + } + return { text: next.slice(-maxChars), truncated: true }; +} + +function formatCapturedWatchLog(text, truncated) { + return truncated ? `[openclaw] log truncated to last ${WATCH_LOG_CAPTURE_MAX_CHARS} chars\n${text}` : text; +} + +export function updateWatchBuildDetection(state, chunk) { + const combined = `${state.buffer ?? ""}${String(chunk)}`; + const next = appendBoundedWatchLog("", combined, WATCH_BUILD_DETECTION_MAX_CHARS); + const reason = detectWatchBuildReason(combined, ""); + const triggered = state.triggered || combined.includes("Building TypeScript (dist is stale"); + return { + buffer: next.text, + triggered, + reason: state.reason ?? reason, + }; +} + function parseArgs(argv) { const options = { ...DEFAULTS }; for (let i = 0; i < argv.length; i += 1) { @@ -464,11 +491,20 @@ async function runTimedWatch(options, outputDir) { let stdout = ""; let stderr = ""; + let stdoutTruncated = false; + let stderrTruncated = false; + let buildDetection = { buffer: "", triggered: false, reason: null }; child.stdout?.on("data", (chunk) => { - stdout += String(chunk); + const next = appendBoundedWatchLog(stdout, chunk); + stdout = next.text; + stdoutTruncated ||= next.truncated; + buildDetection = updateWatchBuildDetection(buildDetection, chunk); }); child.stderr?.on("data", (chunk) => { - stderr += String(chunk); + const next = appendBoundedWatchLog(stderr, chunk); + stderr = next.text; + stderrTruncated ||= next.truncated; + buildDetection = updateWatchBuildDetection(buildDetection, chunk); }); let watchPid = null; @@ -492,8 +528,8 @@ async function runTimedWatch(options, outputDir) { const idleCpuEndMs = watchPid ? readProcessTreeCpuMs(watchPid) : null; const exit = await stopTimedWatchChild(child, watchPid, options); - fs.writeFileSync(stdoutPath, stdout, "utf8"); - fs.writeFileSync(stderrPath, stderr, "utf8"); + fs.writeFileSync(stdoutPath, formatCapturedWatchLog(stdout, stdoutTruncated), "utf8"); + fs.writeFileSync(stderrPath, formatCapturedWatchLog(stderr, stderrTruncated), "utf8"); const timing = fs.existsSync(timeFilePath) ? parseTimingFile(timeFilePath) : { userSeconds: Number.NaN, sysSeconds: Number.NaN, elapsedSeconds: Number.NaN }; @@ -509,6 +545,8 @@ async function runTimedWatch(options, outputDir) { stdoutPath, stderrPath, timeFilePath, + watchTriggeredBuild: buildDetection.triggered, + watchBuildReason: buildDetection.reason, }; } @@ -706,15 +744,8 @@ async function main() { (watchResult.timing.userSeconds + watchResult.timing.sysSeconds) * 1000, ); const cpuMs = watchResult.idleCpuMs ?? totalCpuMs; - const watchTriggeredBuild = - fs - .readFileSync(watchResult.stderrPath, "utf8") - .includes("Building TypeScript (dist is stale") || - fs.readFileSync(watchResult.stdoutPath, "utf8").includes("Building TypeScript (dist is stale"); - const watchBuildReason = detectWatchBuildReason( - fs.readFileSync(watchResult.stdoutPath, "utf8"), - fs.readFileSync(watchResult.stderrPath, "utf8"), - ); + const watchTriggeredBuild = watchResult.watchTriggeredBuild; + const watchBuildReason = watchResult.watchBuildReason; const summary = { windowMs: options.windowMs, diff --git a/test/scripts/check-gateway-watch-regression.test.ts b/test/scripts/check-gateway-watch-regression.test.ts index 015d37d3d99..2f4766fbd75 100644 --- a/test/scripts/check-gateway-watch-regression.test.ts +++ b/test/scripts/check-gateway-watch-regression.test.ts @@ -4,9 +4,12 @@ import os from "node:os"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { + appendBoundedWatchLog, hasGatewayReadyLog, shouldRefreshBuildStampForRestoredArtifacts, stopTimedWatchChild, + updateWatchBuildDetection, + WATCH_LOG_CAPTURE_MAX_CHARS, writeBuildAndRuntimePostBuildStamps, } from "../../scripts/check-gateway-watch-regression.mjs"; import { @@ -21,6 +24,34 @@ describe("check-gateway-watch-regression", () => { expect(hasGatewayReadyLog("[gateway] starting HTTP server...")).toBe(false); }); + it("bounds in-memory watch output capture while keeping the newest logs", () => { + const first = appendBoundedWatchLog("abc", "def", 8); + expect(first).toEqual({ text: "abcdef", truncated: false }); + + const second = appendBoundedWatchLog(first.text, "ghijkl", 8); + expect(second).toEqual({ text: "efghijkl", truncated: true }); + expect(second.text).toHaveLength(8); + expect(WATCH_LOG_CAPTURE_MAX_CHARS).toBeGreaterThan(1024); + }); + + it("keeps build-regression detection after diagnostic logs truncate", () => { + const detected = updateWatchBuildDetection( + { buffer: "", triggered: false, reason: null }, + "Building TypeScript (dist is stale: source_mtime_newer)\n", + ); + const afterNoise = updateWatchBuildDetection(detected, "x".repeat(10_000)); + + expect(afterNoise.triggered).toBe(true); + expect(afterNoise.reason).toBe("source_mtime_newer"); + + const coalesced = updateWatchBuildDetection( + { buffer: "", triggered: false, reason: null }, + `Building TypeScript (dist is stale: config_newer)\n${"x".repeat(10_000)}`, + ); + expect(coalesced.triggered).toBe(true); + expect(coalesced.reason).toBe("config_newer"); + }); + it("refreshes restored build stamps only for skip-build config mtime drift", () => { expect( shouldRefreshBuildStampForRestoredArtifacts({