From de10eca7d60740defccb2c80086345f6d028e3ca Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 18 Jun 2026 12:17:32 +0800 Subject: [PATCH] refactor(scripts): dedupe release assertion readers --- scripts/e2e/lib/release-assertion-files.mjs | 69 ++++++++++++++++++ .../e2e/lib/release-scenarios/assertions.mjs | 71 ++----------------- .../lib/release-user-journey/assertions.mjs | 71 ++----------------- scripts/test-projects.test-support.mjs | 7 ++ test/scripts/test-projects.test.ts | 34 ++++----- 5 files changed, 104 insertions(+), 148 deletions(-) create mode 100644 scripts/e2e/lib/release-assertion-files.mjs diff --git a/scripts/e2e/lib/release-assertion-files.mjs b/scripts/e2e/lib/release-assertion-files.mjs new file mode 100644 index 00000000000..bef49d249d1 --- /dev/null +++ b/scripts/e2e/lib/release-assertion-files.mjs @@ -0,0 +1,69 @@ +// Shared bounded file readers for release E2E assertion scripts. +import fs from "node:fs"; +import { readTextFileTail } from "./text-file-utils.mjs"; + +const SCAN_CHUNK_BYTES = 64 * 1024; +const SCAN_CARRY_CHARS = 256; +export const ERROR_DETAIL_TAIL_BYTES = 16 * 1024; +const JSON_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024; + +export function readJson(file, maxBytes = JSON_ARTIFACT_MAX_BYTES) { + const stat = fs.statSync(file); + if (!stat.isFile()) { + throw new Error(`${file} is not a file`); + } + if (stat.size > maxBytes) { + throw new Error( + `JSON artifact exceeded ${maxBytes} bytes: ${file} (${stat.size} bytes). Tail: ${readTextFileTail( + file, + ERROR_DETAIL_TAIL_BYTES, + )}`, + ); + } + const text = fs.readFileSync(file, "utf8"); + const bytes = Buffer.byteLength(text, "utf8"); + if (bytes > maxBytes) { + throw new Error( + `JSON artifact exceeded ${maxBytes} bytes: ${file} (${bytes} bytes). Tail: ${readTextFileTail( + file, + ERROR_DETAIL_TAIL_BYTES, + )}`, + ); + } + return JSON.parse(text); +} + +export 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); + } +} diff --git a/scripts/e2e/lib/release-scenarios/assertions.mjs b/scripts/e2e/lib/release-scenarios/assertions.mjs index 540bc6222a7..68e8f7e7662 100644 --- a/scripts/e2e/lib/release-scenarios/assertions.mjs +++ b/scripts/e2e/lib/release-scenarios/assertions.mjs @@ -12,82 +12,21 @@ import { parseMockOpenAiPort, } from "../fixtures/mock-openai-config.mjs"; import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs"; +import { + ERROR_DETAIL_TAIL_BYTES, + fileContainsText, + readJson, +} from "../release-assertion-files.mjs"; import { readTextFileTail } from "../text-file-utils.mjs"; const command = process.argv[2]; -const SCAN_CHUNK_BYTES = 64 * 1024; -const SCAN_CARRY_CHARS = 256; -const ERROR_DETAIL_TAIL_BYTES = 16 * 1024; -const JSON_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024; - function assert(condition, message) { if (!condition) { throw new Error(message); } } -function readJson(file, maxBytes = JSON_ARTIFACT_MAX_BYTES) { - const stat = fs.statSync(file); - if (!stat.isFile()) { - throw new Error(`${file} is not a file`); - } - if (stat.size > maxBytes) { - throw new Error( - `JSON artifact exceeded ${maxBytes} bytes: ${file} (${stat.size} bytes). Tail: ${readTextFileTail( - file, - ERROR_DETAIL_TAIL_BYTES, - )}`, - ); - } - const text = fs.readFileSync(file, "utf8"); - const bytes = Buffer.byteLength(text, "utf8"); - if (bytes > maxBytes) { - throw new Error( - `JSON artifact exceeded ${maxBytes} bytes: ${file} (${bytes} bytes). Tail: ${readTextFileTail( - file, - ERROR_DETAIL_TAIL_BYTES, - )}`, - ); - } - return JSON.parse(text); -} - -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); - } -} - function configPath() { return ( process.env.OPENCLAW_CONFIG_PATH ?? diff --git a/scripts/e2e/lib/release-user-journey/assertions.mjs b/scripts/e2e/lib/release-user-journey/assertions.mjs index f4bf122bee3..8de7f682726 100644 --- a/scripts/e2e/lib/release-user-journey/assertions.mjs +++ b/scripts/e2e/lib/release-user-journey/assertions.mjs @@ -12,13 +12,13 @@ import { parseMockOpenAiPort, } from "../fixtures/mock-openai-config.mjs"; import { readPluginInstallRecords } from "../plugin-index-sqlite.mjs"; +import { + ERROR_DETAIL_TAIL_BYTES, + fileContainsText, + readJson, +} from "../release-assertion-files.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; -const JSON_ARTIFACT_MAX_BYTES = 2 * 1024 * 1024; - function clickClackHttpTimeoutMs() { return readPositiveInt( process.env.OPENCLAW_RELEASE_USER_JOURNEY_HTTP_TIMEOUT_MS, @@ -35,32 +35,6 @@ function clickClackHttpBodyMaxBytes() { ); } -function readJson(file, maxBytes = JSON_ARTIFACT_MAX_BYTES) { - const stat = fs.statSync(file); - if (!stat.isFile()) { - throw new Error(`${file} is not a file`); - } - if (stat.size > maxBytes) { - throw new Error( - `JSON artifact exceeded ${maxBytes} bytes: ${file} (${stat.size} bytes). Tail: ${readTextFileTail( - file, - ERROR_DETAIL_TAIL_BYTES, - )}`, - ); - } - const text = fs.readFileSync(file, "utf8"); - const bytes = Buffer.byteLength(text, "utf8"); - if (bytes > maxBytes) { - throw new Error( - `JSON artifact exceeded ${maxBytes} bytes: ${file} (${bytes} bytes). Tail: ${readTextFileTail( - file, - ERROR_DETAIL_TAIL_BYTES, - )}`, - ); - } - return JSON.parse(text); -} - function readPositiveInt(raw, fallback, label) { const text = String(raw ?? "").trim(); if (!text) { @@ -76,41 +50,6 @@ function readPositiveInt(raw, fallback, label) { return parsed; } -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(); diff --git a/scripts/test-projects.test-support.mjs b/scripts/test-projects.test-support.mjs index ad4511a7af1..880851e1f46 100644 --- a/scripts/test-projects.test-support.mjs +++ b/scripts/test-projects.test-support.mjs @@ -1030,6 +1030,13 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([ "test/scripts/release-user-journey-assertions.test.ts", ], ], + [ + "scripts/e2e/lib/release-assertion-files.mjs", + [ + "test/scripts/release-scenarios-assertions.test.ts", + "test/scripts/release-user-journey-assertions.test.ts", + ], + ], ["scripts/e2e/lib/skills/clawhub-install-proof.sh", ["test/scripts/e2e-shell-tempfiles.test.ts"]], [ "scripts/e2e/lib/update-channel-switch/assertions.mjs", diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index 50d3ec3c94f..7f89ac53486 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -365,6 +365,13 @@ describe("scripts/test-projects changed-target routing", () => { "scripts/e2e/lib/release-user-journey/assertions.mjs", ["test/scripts/release-user-journey-assertions.test.ts"], ], + [ + "scripts/e2e/lib/release-assertion-files.mjs", + [ + "test/scripts/release-scenarios-assertions.test.ts", + "test/scripts/release-user-journey-assertions.test.ts", + ], + ], [ "scripts/e2e/lib/openai-chat-tools/write-config.mjs", ["test/scripts/openai-chat-tools-client.test.ts"], @@ -375,10 +382,7 @@ describe("scripts/test-projects changed-target routing", () => { ], [ "scripts/e2e/openai-chat-tools-docker.sh", - [ - "test/scripts/openai-chat-tools-client.test.ts", - "test/scripts/docker-e2e-plan.test.ts", - ], + ["test/scripts/openai-chat-tools-client.test.ts", "test/scripts/docker-e2e-plan.test.ts"], ], [ "scripts/e2e/lib/openai-web-search-minimal/mock-server.mjs", @@ -595,10 +599,7 @@ describe("scripts/test-projects changed-target routing", () => { ], ["scripts/e2e/mcp-channels-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]], ["scripts/e2e/docker-openai-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]], - [ - "scripts/e2e/mcp-code-mode-gateway-seed.ts", - ["test/scripts/docker-e2e-seeds.test.ts"], - ], + ["scripts/e2e/mcp-code-mode-gateway-seed.ts", ["test/scripts/docker-e2e-seeds.test.ts"]], [ "scripts/e2e/cron-mcp-cleanup-docker.sh", [ @@ -903,14 +904,15 @@ describe("scripts/test-projects changed-target routing", () => { }); it("routes code-mode namespace live Docker repro changes through its regression tests", () => { - expect(resolveChangedTestTargetPlan(["scripts/repro/code-mode-namespace-live-docker.sh"])) - .toEqual({ - mode: "targets", - targets: [ - "test/scripts/code-mode-namespace-live.test.ts", - "test/scripts/docker-build-helper.test.ts", - ], - }); + expect( + resolveChangedTestTargetPlan(["scripts/repro/code-mode-namespace-live-docker.sh"]), + ).toEqual({ + mode: "targets", + targets: [ + "test/scripts/code-mode-namespace-live.test.ts", + "test/scripts/docker-build-helper.test.ts", + ], + }); }); it("routes group visible reply config changes through channel delivery regressions", () => {