fix(scripts): bound gateway watch log capture

This commit is contained in:
Vincent Koc
2026-05-28 08:45:18 +02:00
parent 00fb15253c
commit 0ade360da5
2 changed files with 75 additions and 13 deletions

View File

@@ -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,

View File

@@ -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({