refactor(scripts): dedupe release assertion readers

This commit is contained in:
Vincent Koc
2026-06-18 12:17:32 +08:00
parent cb8daec729
commit de10eca7d6
5 changed files with 104 additions and 148 deletions

View File

@@ -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);
}
}

View File

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

View File

@@ -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();

View File

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

View File

@@ -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", () => {