diff --git a/scripts/e2e/lib/release-user-journey/assertions.mjs b/scripts/e2e/lib/release-user-journey/assertions.mjs index 1c75648890e5..85143bdc6e84 100644 --- a/scripts/e2e/lib/release-user-journey/assertions.mjs +++ b/scripts/e2e/lib/release-user-journey/assertions.mjs @@ -8,6 +8,11 @@ import { import { readBoundedResponseText as readBoundedResponseTextWithLimit } from "../bounded-response-text.mjs"; import { applyMockOpenAiModelConfig } from "../fixtures/mock-openai-config.mjs"; import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs"; +import { readTextFileTail } from "../text-file-utils.mjs"; + +const SCAN_CHUNK_BYTES = 64 * 1024; +const SCAN_CARRY_CHARS = 256; +const ERROR_DETAIL_TAIL_BYTES = 16 * 1024; function clickClackHttpTimeoutMs() { return readPositiveInt(process.env.OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS, 5000); @@ -33,6 +38,41 @@ function readPositiveInt(raw, fallback) { return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback; } +function fileContainsText(file, needle) { + let stat; + try { + stat = fs.statSync(file); + } catch { + return false; + } + if (!stat.isFile() || stat.size <= 0) { + return false; + } + + const fd = fs.openSync(file, "r"); + try { + const buffer = Buffer.alloc(Math.min(SCAN_CHUNK_BYTES, stat.size)); + let carry = ""; + let offset = 0; + while (offset < stat.size) { + const bytesToRead = Math.min(buffer.length, stat.size - offset); + const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, offset); + if (bytesRead <= 0) { + break; + } + offset += bytesRead; + const text = carry + buffer.subarray(0, bytesRead).toString("utf8"); + if (text.includes(needle)) { + return true; + } + carry = text.slice(-Math.max(SCAN_CARRY_CHARS, needle.length - 1)); + } + return false; + } finally { + fs.closeSync(fd); + } +} + async function withClickClackFixtureResponse(url, init, consume, options = {}) { const timeoutMs = options.timeoutMs ?? clickClackHttpTimeoutMs(); const controller = new AbortController(); @@ -134,8 +174,10 @@ function assertAgentTurn() { function assertFileContains() { const file = process.argv[3]; const needle = process.argv[4]; - const raw = fs.readFileSync(file, "utf8"); - assert(raw.includes(needle), `${file} did not contain ${needle}. Output: ${raw}`); + assert( + fileContainsText(file, needle), + `${file} did not contain ${needle}. Output tail: ${readTextFileTail(file, ERROR_DETAIL_TAIL_BYTES)}`, + ); } function rememberPluginInstallPath() { diff --git a/test/scripts/release-user-journey-assertions.test.ts b/test/scripts/release-user-journey-assertions.test.ts index a31f552ad8cc..4cce0df528fa 100644 --- a/test/scripts/release-user-journey-assertions.test.ts +++ b/test/scripts/release-user-journey-assertions.test.ts @@ -87,6 +87,55 @@ async function startTcpFixtureServer(handler: (socket: Socket) => void): Promise } describe("release user journey assertions", () => { + it("scans large files when checking release user journey output text", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-")); + const home = path.join(root, "home"); + const outputPath = path.join(root, "output.log"); + + try { + const needlePrefix = "journey-plugin"; + writeFileSync( + outputPath, + `${"x".repeat(64 * 1024 - needlePrefix.length)}${needlePrefix}-a:pong\n`, + "utf8", + ); + + const result = runAssertion(home, [ + "assert-file-contains", + outputPath, + "journey-plugin-a:pong", + ]); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }); + + it("bounds release user journey output assertion diagnostics", () => { + const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-")); + const home = path.join(root, "home"); + const outputPath = path.join(root, "output.log"); + + try { + writeFileSync( + outputPath, + `DO_NOT_DUMP_OLD_OUTPUT${"x".repeat(70 * 1024)}\nrecent output tail\n`, + "utf8", + ); + + const result = runAssertion(home, ["assert-file-contains", outputPath, "missing"]); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("Output tail:"); + expect(result.stderr).toContain("recent output tail"); + expect(result.stderr).not.toContain("DO_NOT_DUMP_OLD_OUTPUT"); + } finally { + rmSync(root, { force: true, recursive: true }); + } + }); + it("fails when uninstall leaves the managed plugin directory behind", () => { const root = mkdtempSync(path.join(tmpdir(), "openclaw-release-user-assertions-")); const home = path.join(root, "home");