fix(e2e): bound release journey output assertions

This commit is contained in:
Vincent Koc
2026-06-04 10:55:30 +02:00
parent 7c1deea5fa
commit 72bb5cd692
2 changed files with 93 additions and 2 deletions

View File

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

View File

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