Skip to content

Commit 1eb27da

Browse files
committed
fix(testing): bound openclaw instance logs
1 parent 9ff071f commit 1eb27da

2 files changed

Lines changed: 96 additions & 20 deletions

File tree

test/helpers/openclaw-test-instance.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "node:fs/promises";
22
import path from "node:path";
33
import { describe, expect, it } from "vitest";
4-
import { createOpenClawTestInstance } from "./openclaw-test-instance.js";
4+
import { createOpenClawTestInstance, testing } from "./openclaw-test-instance.js";
55

66
async function expectPathMissing(targetPath: string): Promise<void> {
77
try {
@@ -14,6 +14,23 @@ async function expectPathMissing(targetPath: string): Promise<void> {
1414
}
1515

1616
describe("openclaw test instance", () => {
17+
it("keeps only bounded child output tails in helper logs", () => {
18+
const stdout = testing.createBoundedStringLog();
19+
const stderr = testing.createBoundedStringLog();
20+
21+
testing.appendLogChunk(stdout, `old stdout ${"x".repeat(64)}\n`, 32);
22+
testing.appendLogChunk(stdout, "recent stdout\n", 32);
23+
testing.appendLogChunk(stderr, `old stderr ${"y".repeat(64)}\n`, 32);
24+
testing.appendLogChunk(stderr, "recent stderr\n", 32);
25+
26+
const logs = testing.formatLogs(stdout, stderr);
27+
expect(logs).toContain("[output truncated to last");
28+
expect(logs).toContain("recent stdout");
29+
expect(logs).toContain("recent stderr");
30+
expect(logs).not.toContain("old stdout");
31+
expect(logs).not.toContain("old stderr");
32+
});
33+
1734
it("creates isolated config and spawn env without mutating process env", async () => {
1835
const previousHome = process.env.HOME;
1936
const inst = await createOpenClawTestInstance({

test/helpers/openclaw-test-instance.ts

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,63 @@ const GATEWAY_START_TIMEOUT_MS = 60_000;
6464
const GATEWAY_STOP_TIMEOUT_MS = 1_500;
6565
const GATEWAY_ENTRYPOINT_PREPARE_TIMEOUT_MS = 120_000;
6666
const COMMAND_TIMEOUT_MS = 30_000;
67+
const LOG_TAIL_MAX_BYTES = 256 * 1024;
6768
const entrypointPromises = new Map<string, Promise<string[]>>();
6869

70+
type BoundedStringLog = string[] & {
71+
byteLength?: number;
72+
truncated?: boolean;
73+
};
74+
75+
function createBoundedStringLog(): string[] {
76+
const log = [] as BoundedStringLog;
77+
log.byteLength = 0;
78+
log.truncated = false;
79+
return log;
80+
}
81+
82+
function appendLogChunk(log: string[], chunk: unknown, maxBytes = LOG_TAIL_MAX_BYTES): void {
83+
const chunks = log as BoundedStringLog;
84+
const limit = Math.max(1, maxBytes);
85+
const text = String(chunk);
86+
const textBytes = Buffer.byteLength(text);
87+
if (textBytes >= limit) {
88+
const buffer = Buffer.from(text);
89+
const tail = buffer.subarray(buffer.length - limit).toString("utf8");
90+
chunks.splice(0, chunks.length, tail);
91+
chunks.byteLength = Buffer.byteLength(tail);
92+
chunks.truncated = true;
93+
return;
94+
}
95+
96+
chunks.push(text);
97+
chunks.byteLength = (chunks.byteLength ?? 0) + textBytes;
98+
while ((chunks.byteLength ?? 0) > limit && chunks.length > 0) {
99+
const first = chunks[0] ?? "";
100+
const firstBytes = Buffer.byteLength(first);
101+
const overflow = (chunks.byteLength ?? 0) - limit;
102+
if (firstBytes <= overflow) {
103+
chunks.shift();
104+
chunks.byteLength = (chunks.byteLength ?? 0) - firstBytes;
105+
chunks.truncated = true;
106+
continue;
107+
}
108+
109+
const buffer = Buffer.from(first);
110+
const tail = buffer.subarray(overflow).toString("utf8");
111+
chunks[0] = tail;
112+
chunks.byteLength = chunks.reduce((total, entry) => total + Buffer.byteLength(entry), 0);
113+
chunks.truncated = true;
114+
}
115+
}
116+
117+
function readLogBuffer(log: string[]): string {
118+
const text = log.join("");
119+
return (log as BoundedStringLog).truncated
120+
? `[output truncated to last ${LOG_TAIL_MAX_BYTES} bytes]\n${text}`
121+
: text;
122+
}
123+
69124
async function resolveBuiltGatewayEntrypoint(cwd: string): Promise<string[] | null> {
70125
const buildStampPath = path.join(cwd, "dist", BUILD_STAMP_FILE);
71126
const runtimePostBuildStampPath = path.join(cwd, "dist", RUNTIME_POSTBUILD_STAMP_FILE);
@@ -90,17 +145,17 @@ async function prepareGatewayEntrypoint(cwd: string): Promise<string[]> {
90145
return builtEntrypoint;
91146
}
92147

93-
const stdout: string[] = [];
94-
const stderr: string[] = [];
148+
const stdout = createBoundedStringLog();
149+
const stderr = createBoundedStringLog();
95150
const child = spawn("node", ["scripts/run-node.mjs", "--help"], {
96151
cwd,
97152
env: { ...process.env, VITEST: "1" },
98153
stdio: ["ignore", "pipe", "pipe"],
99154
});
100155
child.stdout?.setEncoding("utf8");
101156
child.stderr?.setEncoding("utf8");
102-
child.stdout?.on("data", (d) => stdout.push(String(d)));
103-
child.stderr?.on("data", (d) => stderr.push(String(d)));
157+
child.stdout?.on("data", (d) => appendLogChunk(stdout, d));
158+
child.stderr?.on("data", (d) => appendLogChunk(stderr, d));
104159

105160
const completed = await Promise.race([
106161
new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
@@ -112,15 +167,13 @@ async function prepareGatewayEntrypoint(cwd: string): Promise<string[]> {
112167

113168
if (completed === null) {
114169
child.kill("SIGKILL");
115-
throw new Error(
116-
`timeout preparing gateway entrypoint\n--- stdout ---\n${stdout.join("")}\n--- stderr ---\n${stderr.join("")}`,
117-
);
170+
throw new Error(`timeout preparing gateway entrypoint\n${formatLogs(stdout, stderr)}`);
118171
}
119172
if (completed.code !== 0) {
120173
throw new Error(
121174
`failed preparing gateway entrypoint (code=${String(completed.code)} signal=${String(
122175
completed.signal,
123-
)})\n--- stdout ---\n${stdout.join("")}\n--- stderr ---\n${stderr.join("")}`,
176+
)})\n${formatLogs(stdout, stderr)}`,
124177
);
125178
}
126179

@@ -224,7 +277,7 @@ function mergeConfig(
224277
}
225278

226279
function formatLogs(stdout: string[], stderr: string[]): string {
227-
return `--- stdout ---\n${stdout.join("")}\n--- stderr ---\n${stderr.join("")}`;
280+
return `--- stdout ---\n${readLogBuffer(stdout)}\n--- stderr ---\n${readLogBuffer(stderr)}`;
228281
}
229282

230283
function createInstanceEnv(params: {
@@ -282,8 +335,8 @@ export async function createOpenClawTestInstance(
282335
),
283336
);
284337

285-
const stdout: string[] = [];
286-
const stderr: string[] = [];
338+
const stdout = createBoundedStringLog();
339+
const stderr = createBoundedStringLog();
287340
const env = createInstanceEnv({
288341
stateEnv: state.env,
289342
extraEnv: options.env ?? {},
@@ -343,8 +396,8 @@ export async function createOpenClawTestInstance(
343396

344397
child.stdout?.setEncoding("utf8");
345398
child.stderr?.setEncoding("utf8");
346-
child.stdout?.on("data", (d) => stdout.push(String(d)));
347-
child.stderr?.on("data", (d) => stderr.push(String(d)));
399+
child.stdout?.on("data", (d) => appendLogChunk(stdout, d));
400+
child.stderr?.on("data", (d) => appendLogChunk(stderr, d));
348401

349402
try {
350403
await waitForPortOpen(
@@ -410,17 +463,17 @@ async function runCommand(params: {
410463
if (!command) {
411464
throw new Error("missing command");
412465
}
413-
const stdout: string[] = [];
414-
const stderr: string[] = [];
466+
const stdout = createBoundedStringLog();
467+
const stderr = createBoundedStringLog();
415468
const child = spawn(command, args, {
416469
cwd: params.cwd,
417470
env: params.env,
418471
stdio: ["ignore", "pipe", "pipe"],
419472
});
420473
child.stdout?.setEncoding("utf8");
421474
child.stderr?.setEncoding("utf8");
422-
child.stdout?.on("data", (d) => stdout.push(String(d)));
423-
child.stderr?.on("data", (d) => stderr.push(String(d)));
475+
child.stdout?.on("data", (d) => appendLogChunk(stdout, d));
476+
child.stderr?.on("data", (d) => appendLogChunk(stderr, d));
424477

425478
const completed = await Promise.race([
426479
new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve, reject) => {
@@ -438,7 +491,13 @@ async function runCommand(params: {
438491
}
439492
return {
440493
...completed,
441-
stdout: stdout.join(""),
442-
stderr: stderr.join(""),
494+
stdout: readLogBuffer(stdout),
495+
stderr: readLogBuffer(stderr),
443496
};
444497
}
498+
499+
export const testing = {
500+
appendLogChunk,
501+
createBoundedStringLog,
502+
formatLogs,
503+
};

0 commit comments

Comments
 (0)