Skip to content

Commit f8f53de

Browse files
committed
fix(e2e): bound kitchen sink log scans
1 parent 6c9e7de commit f8f53de

2 files changed

Lines changed: 141 additions & 44 deletions

File tree

scripts/e2e/lib/kitchen-sink-plugin/assertions.mjs

Lines changed: 97 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@ const command = process.argv[2];
88
const scratchRoot = process.env.KITCHEN_SINK_TMP_DIR || os.tmpdir();
99

1010
const LOG_SCAN_CHUNK_BYTES = 64 * 1024;
11+
const LOG_SCAN_FINDING_CONTEXT_CHARS = 2048;
12+
const LOG_SCAN_MAX_FILES = 5000;
1113
const LOG_SCAN_MAX_FINDINGS = 100;
14+
const LOG_SCAN_MAX_LINE_CHARS = 16 * 1024;
15+
const LOG_SCAN_SEGMENT_OVERLAP_CHARS = 256;
1216

1317
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
1418
const scratchFile = (name) => path.join(scratchRoot, name);
@@ -46,67 +50,109 @@ function scanTextFileLines(file, onLine) {
4650
const fd = fs.openSync(file, "r");
4751
try {
4852
const buffer = Buffer.alloc(LOG_SCAN_CHUNK_BYTES);
49-
let carry = "";
53+
let currentLine = "";
5054
let lineNumber = 1;
55+
const emitLine = (line, info = {}) => onLine(line, lineNumber, info);
56+
const appendLineText = (text, complete) => {
57+
currentLine += text;
58+
while (currentLine.length > LOG_SCAN_MAX_LINE_CHARS) {
59+
const segment = currentLine.slice(0, LOG_SCAN_MAX_LINE_CHARS);
60+
currentLine = currentLine.slice(LOG_SCAN_MAX_LINE_CHARS - LOG_SCAN_SEGMENT_OVERLAP_CHARS);
61+
if (emitLine(segment, { truncated: true }) === false) {
62+
return false;
63+
}
64+
}
65+
if (complete) {
66+
if (emitLine(currentLine) === false) {
67+
return false;
68+
}
69+
currentLine = "";
70+
lineNumber += 1;
71+
}
72+
return true;
73+
};
74+
5175
while (true) {
5276
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null);
5377
if (bytesRead <= 0) {
5478
break;
5579
}
56-
const text = carry + buffer.subarray(0, bytesRead).toString("utf8");
80+
const text = buffer.subarray(0, bytesRead).toString("utf8");
5781
const lines = text.split(/\r?\n/u);
58-
carry = lines.pop() ?? "";
59-
for (const line of lines) {
60-
if (onLine(line, lineNumber) === false) {
82+
for (let index = 0; index < lines.length - 1; index += 1) {
83+
if (appendLineText(lines[index], true) === false) {
6184
return;
6285
}
63-
lineNumber += 1;
86+
}
87+
if (appendLineText(lines.at(-1) ?? "", false) === false) {
88+
return;
6489
}
6590
}
66-
if (carry.length > 0) {
67-
onLine(carry, lineNumber);
91+
if (currentLine.length > 0) {
92+
onLine(currentLine, lineNumber);
6893
}
6994
} finally {
7095
fs.closeSync(fd);
7196
}
7297
}
7398

74-
function scanLogs() {
75-
if (!process.env.KITCHEN_SINK_TMP_DIR) {
76-
throw new Error("KITCHEN_SINK_TMP_DIR is required for kitchen-sink log scans");
99+
function formatFindingLine(line, pattern, info = {}) {
100+
const matchIndex = Math.max(0, line.search(pattern));
101+
const halfWindow = Math.floor(LOG_SCAN_FINDING_CONTEXT_CHARS / 2);
102+
const start = Math.max(0, matchIndex - halfWindow);
103+
const end = Math.min(line.length, start + LOG_SCAN_FINDING_CONTEXT_CHARS);
104+
const prefix = start > 0 ? "... " : "";
105+
const suffix = end < line.length || info.truncated ? " ..." : "";
106+
return `${prefix}${line.slice(start, end)}${suffix}`;
107+
}
108+
109+
function shouldScanLogFile(entry) {
110+
if (!(/\.(?:log|jsonl)$/u.test(entry) || /openclaw-kitchen-sink-/u.test(path.basename(entry)))) {
111+
return false;
77112
}
78-
const roots = [scratchRoot, path.join(process.env.HOME, ".openclaw")];
79-
const files = [];
80-
const visit = (entry) => {
81-
if (!fs.existsSync(entry)) {
82-
return;
83-
}
84-
const stat = fs.lstatSync(entry);
85-
if (stat.isSymbolicLink()) {
86-
return;
87-
}
88-
if (stat.isDirectory()) {
89-
for (const child of fs.readdirSync(entry).toSorted()) {
90-
visit(path.join(entry, child));
113+
return !normalizedPath(entry).includes("/.npm/_logs/");
114+
}
115+
116+
function scanLogFiles(roots, onFile) {
117+
let scannedFiles = 0;
118+
for (const root of roots) {
119+
const pending = [root];
120+
while (pending.length > 0) {
121+
const entry = pending.pop();
122+
if (!entry || !fs.existsSync(entry)) {
123+
continue;
91124
}
92-
return;
93-
}
94-
if (/\.(?:log|jsonl)$/u.test(entry) || /openclaw-kitchen-sink-/u.test(path.basename(entry))) {
95-
if (normalizedPath(entry).includes("/.npm/_logs/")) {
96-
return;
125+
const stat = fs.lstatSync(entry);
126+
if (stat.isSymbolicLink()) {
127+
continue;
128+
}
129+
if (stat.isDirectory()) {
130+
const children = fs.readdirSync(entry).toSorted((left, right) => right.localeCompare(left));
131+
for (const child of children) {
132+
pending.push(path.join(entry, child));
133+
}
134+
continue;
135+
}
136+
if (!shouldScanLogFile(entry)) {
137+
continue;
138+
}
139+
scannedFiles += 1;
140+
if (scannedFiles > LOG_SCAN_MAX_FILES) {
141+
throw new Error(`kitchen-sink log scan exceeded ${LOG_SCAN_MAX_FILES} candidate files`);
142+
}
143+
if (onFile(entry, scannedFiles) === false) {
144+
return scannedFiles;
97145
}
98-
files.push(entry);
99146
}
100-
};
101-
for (const root of roots) {
102-
visit(root);
103-
}
104-
if (files.length === 0) {
105-
throw new Error(
106-
"kitchen-sink log scan found no files under the isolated scratch root or OpenClaw home",
107-
);
108147
}
148+
return scannedFiles;
149+
}
109150

151+
function scanLogs() {
152+
if (!process.env.KITCHEN_SINK_TMP_DIR) {
153+
throw new Error("KITCHEN_SINK_TMP_DIR is required for kitchen-sink log scans");
154+
}
155+
const roots = [scratchRoot, path.join(process.env.HOME, ".openclaw")];
110156
const deny = [
111157
/\buncaught exception\b/iu,
112158
/\bunhandled rejection\b/iu,
@@ -122,29 +168,36 @@ function scanLogs() {
122168
];
123169
const findings = [];
124170
let omittedFindings = false;
125-
for (const file of files) {
126-
scanTextFileLines(file, (line, lineNumber) => {
171+
const scannedFiles = scanLogFiles(roots, (file) => {
172+
scanTextFileLines(file, (line, lineNumber, info) => {
127173
if (allow.some((pattern) => pattern.test(line))) {
128174
return true;
129175
}
130-
if (deny.some((pattern) => pattern.test(line))) {
176+
const matchedPattern = deny.find((pattern) => pattern.test(line));
177+
if (matchedPattern) {
131178
if (findings.length >= LOG_SCAN_MAX_FINDINGS) {
132179
omittedFindings = true;
133180
return false;
134181
}
135-
findings.push(`${file}:${lineNumber}: ${line}`);
182+
findings.push(`${file}:${lineNumber}: ${formatFindingLine(line, matchedPattern, info)}`);
136183
}
137184
return true;
138185
});
139186
if (omittedFindings) {
140-
break;
187+
return false;
141188
}
189+
return true;
190+
});
191+
if (scannedFiles === 0) {
192+
throw new Error(
193+
"kitchen-sink log scan found no files under the isolated scratch root or OpenClaw home",
194+
);
142195
}
143196
if (findings.length > 0) {
144197
const suffix = omittedFindings ? "\n... additional findings omitted" : "";
145198
throw new Error(`unexpected error-like log lines:\n${findings.join("\n")}${suffix}`);
146199
}
147-
console.log(`log scan passed (${files.length} file(s))`);
200+
console.log(`log scan passed (${scannedFiles} file(s))`);
148201
}
149202

150203
function readConfig() {

test/scripts/kitchen-sink-plugin-assertions.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,50 @@ describe("kitchen-sink plugin assertions", () => {
354354
}
355355
});
356356

357+
it("bounds huge single-line kitchen-sink log findings", () => {
358+
const parent = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-sink-scan-"));
359+
const home = path.join(parent, "home");
360+
const scratchRoot = path.join(parent, "scratch");
361+
try {
362+
mkdirSync(home, { recursive: true });
363+
mkdirSync(scratchRoot, { recursive: true });
364+
writeFileSync(
365+
path.join(scratchRoot, "single-line.jsonl"),
366+
`DO_NOT_DUMP_OLD_PREFIX${"x".repeat(256 * 1024)}recent marker [ERROR] bad state`,
367+
);
368+
369+
const result = runScanLogs({ home, scratchRoot });
370+
371+
expect(result.status).not.toBe(0);
372+
expect(`${result.stdout}\n${result.stderr}`).toContain("recent marker");
373+
expect(`${result.stdout}\n${result.stderr}`).not.toContain("DO_NOT_DUMP_OLD_PREFIX");
374+
expect(`${result.stdout}\n${result.stderr}`.length).toBeLessThan(25 * 1024);
375+
} finally {
376+
rmSync(parent, { force: true, recursive: true });
377+
}
378+
});
379+
380+
it("detects kitchen-sink log errors split across scan segment boundaries", () => {
381+
const parent = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-sink-scan-"));
382+
const home = path.join(parent, "home");
383+
const scratchRoot = path.join(parent, "scratch");
384+
try {
385+
mkdirSync(home, { recursive: true });
386+
mkdirSync(scratchRoot, { recursive: true });
387+
writeFileSync(
388+
path.join(scratchRoot, "split-marker.jsonl"),
389+
`${"x".repeat(16 * 1024 - 3)}[ERROR] split boundary marker`,
390+
);
391+
392+
const result = runScanLogs({ home, scratchRoot });
393+
394+
expect(result.status).not.toBe(0);
395+
expect(`${result.stdout}\n${result.stderr}`).toContain("split boundary marker");
396+
} finally {
397+
rmSync(parent, { force: true, recursive: true });
398+
}
399+
});
400+
357401
it("rejects kitchen-sink log scans without an isolated scratch root", () => {
358402
const parent = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-sink-scan-"));
359403
try {

0 commit comments

Comments
 (0)