diff --git a/scripts/e2e/lib/plugin-update/probe.mjs b/scripts/e2e/lib/plugin-update/probe.mjs index 1e2b86638b63..4198ab392dbe 100644 --- a/scripts/e2e/lib/plugin-update/probe.mjs +++ b/scripts/e2e/lib/plugin-update/probe.mjs @@ -10,6 +10,9 @@ import { } from "../plugin-index-sqlite.mjs"; const home = os.homedir(); +const OUTPUT_TAIL_BYTES = 64 * 1024; +const OUTPUT_TAIL_LINES = 120; +const OUTPUT_SCAN_WINDOW_BYTES = 8 * 1024; const readJson = (file) => { try { @@ -106,15 +109,51 @@ function assertSnapshot(beforePath) { } } -function assertOutput(logPath) { - const output = fs.readFileSync(logPath, "utf8"); - const failure = output.includes("Downloading @example/lossless-claw") +function appendBufferTail(tail, chunk, maxBytes) { + if (chunk.length >= maxBytes) { + return chunk.subarray(chunk.length - maxBytes); + } + if (tail.length + chunk.length <= maxBytes) { + return Buffer.concat([tail, chunk]); + } + return Buffer.concat([tail, chunk]).subarray(tail.length + chunk.length - maxBytes); +} + +async function readOutputEvidence(logPath) { + let outputTail = Buffer.alloc(0); + let scanWindow = ""; + let sawDownload = false; + let sawUpToDate = false; + for await (const chunk of fs.createReadStream(logPath)) { + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + const text = buffer.toString("utf8"); + const searchable = `${scanWindow}${text}`; + outputTail = appendBufferTail(outputTail, buffer, OUTPUT_TAIL_BYTES); + sawDownload ||= searchable.includes("Downloading @example/lossless-claw"); + sawUpToDate ||= searchable.includes("lossless-claw is up to date (0.9.0)."); + scanWindow = searchable.slice(-OUTPUT_SCAN_WINDOW_BYTES); + } + return { + outputTail: outputTail + .toString("utf8") + .split(/\r?\n/u) + .slice(-OUTPUT_TAIL_LINES) + .join("\n") + .trimEnd(), + sawDownload, + sawUpToDate, + }; +} + +async function assertOutput(logPath) { + const evidence = await readOutputEvidence(logPath); + const failure = evidence.sawDownload ? "Unexpected npm download/reinstall path" - : !output.includes("lossless-claw is up to date (0.9.0).") + : !evidence.sawUpToDate ? "Expected up-to-date output missing" : ""; if (failure) { - throw new Error(`${failure}\n${output}`); + throw new Error(`${failure}\nOutput tail:\n${evidence.outputTail}`); } } diff --git a/scripts/e2e/lib/plugin-update/unchanged-scenario.sh b/scripts/e2e/lib/plugin-update/unchanged-scenario.sh index cb6d6ed53127..bb613011e170 100644 --- a/scripts/e2e/lib/plugin-update/unchanged-scenario.sh +++ b/scripts/e2e/lib/plugin-update/unchanged-scenario.sh @@ -23,7 +23,7 @@ trap 'openclaw_e2e_stop_process "${registry_pid:-}"' EXIT if ! node "$probe" wait-registry; then echo "Local npm metadata registry failed to start" - cat /tmp/openclaw-e2e-registry.log || true + openclaw_e2e_print_log /tmp/openclaw-e2e-registry.log exit 1 fi @@ -42,9 +42,9 @@ set -e if [ "$plugin_update_status" -ne 0 ]; then echo "Plugin update command failed or timed out after ${plugin_update_timeout_seconds}s (status ${plugin_update_status})" echo "--- plugin update output ---" - cat /tmp/plugin-update-output.log || true + openclaw_e2e_print_log /tmp/plugin-update-output.log echo "--- local registry output ---" - cat /tmp/openclaw-e2e-registry.log || true + openclaw_e2e_print_log /tmp/openclaw-e2e-registry.log exit "$plugin_update_status" fi @@ -52,11 +52,11 @@ if [ -n "$before_config_hash" ]; then after_config_hash="$(sha256sum "$OPENCLAW_CONFIG_PATH" | awk '{print $1}')" if [ "$before_config_hash" != "$after_config_hash" ]; then echo "Config changed unexpectedly for modern package $package_version" - cat /tmp/plugin-update-output.log + openclaw_e2e_print_log /tmp/plugin-update-output.log exit 1 fi fi node "$probe" assert-snapshot /tmp/plugin-update-before.json node "$probe" assert-output /tmp/plugin-update-output.log -cat /tmp/plugin-update-output.log +openclaw_e2e_print_log /tmp/plugin-update-output.log diff --git a/test/scripts/plugin-update-unchanged-docker.test.ts b/test/scripts/plugin-update-unchanged-docker.test.ts index 35e7dcc7164f..62e29adbaf58 100644 --- a/test/scripts/plugin-update-unchanged-docker.test.ts +++ b/test/scripts/plugin-update-unchanged-docker.test.ts @@ -47,6 +47,17 @@ function runProbeStatus( } } +function runProbeFileStatus( + command: string, + filePath: string, +): { status: number | null; stderr: string } { + const result = spawnSync("node", [PLUGIN_UPDATE_PROBE_SCRIPT, command, filePath], { + encoding: "utf8", + stdio: "pipe", + }); + return { status: result.status, stderr: result.stderr }; +} + describe("plugin update unchanged Docker E2E", () => { it("seeds current plugin install ledger state before checking config stability", () => { const runner = readFileSync(PLUGIN_UPDATE_DOCKER_SCRIPT, "utf8"); @@ -75,6 +86,56 @@ describe("plugin update unchanged Docker E2E", () => { ); expect(script).toContain('"--- plugin update output ---"'); expect(script).toContain('"--- local registry output ---"'); + expect(script).toContain("openclaw_e2e_print_log /tmp/plugin-update-output.log"); + expect(script).toContain("openclaw_e2e_print_log /tmp/openclaw-e2e-registry.log"); + expect(script).not.toContain("cat /tmp/plugin-update-output.log"); + expect(script).not.toContain("cat /tmp/openclaw-e2e-registry.log"); + }); + + it("bounds assert-output diagnostics to the saved command log tail", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-update-probe-")); + const logPath = path.join(root, "plugin-update-output.log"); + try { + writeFileSync( + logPath, + `DO_NOT_PRINT_OLD_PLUGIN_UPDATE_LOG\n${"filler line\n".repeat(12 * 1024)}missing marker tail`, + "utf8", + ); + + const result = runProbeFileStatus("assert-output", logPath); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("Expected up-to-date output missing"); + expect(result.stderr).toContain("Output tail:"); + expect(result.stderr).toContain("missing marker tail"); + expect(result.stderr).not.toContain("DO_NOT_PRINT_OLD_PLUGIN_UPDATE_LOG"); + expect(result.stderr.length).toBeLessThan(80 * 1024); + } finally { + rmSync(root, { recursive: true, force: true }); + } + }); + + it("detects unexpected download output before a large log tail", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-plugin-update-probe-")); + const logPath = path.join(root, "plugin-update-output.log"); + try { + writeFileSync( + logPath, + [ + "Downloading @example/lossless-claw", + "filler line\n".repeat(12 * 1024), + "lossless-claw is up to date (0.9.0).", + ].join("\n"), + "utf8", + ); + + const result = runProbeFileStatus("assert-output", logPath); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("Unexpected npm download/reinstall path"); + } finally { + rmSync(root, { recursive: true, force: true }); + } }); it("waits for the local registry process during cleanup", () => {