Skip to content

Commit 4d099c3

Browse files
committed
fix(e2e): bound kitchen sink log scans
1 parent e2f6734 commit 4d099c3

2 files changed

Lines changed: 156 additions & 18 deletions

File tree

scripts/e2e/kitchen-sink-rpc-walk.mjs

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,21 @@ const OUTPUT_CAPTURE_CHARS = readPositiveInt(
3737
);
3838
const DEFAULT_PORT = 19000 + Math.floor(Math.random() * 1000);
3939
const LOG_SCAN_CHUNK_BYTES = 64 * 1024;
40+
const LOG_SCAN_MAX_LINE_CHARS = 16 * 1024;
41+
const LOG_TAIL_BYTES = 256 * 1024;
42+
const ERROR_LOG_DENY_PATTERNS = [
43+
/\buncaught exception\b/iu,
44+
/\bunhandled rejection\b/iu,
45+
/\bfatal\b/iu,
46+
/\bpanic\b/iu,
47+
/\blevel["']?\s*:\s*["']error["']/iu,
48+
/\[(?:error|ERROR)\]/u,
49+
];
50+
const ERROR_LOG_ALLOW_PATTERNS = [
51+
/0 errors?/iu,
52+
/expected no diagnostics errors?/iu,
53+
/diagnostics errors?:\s*$/iu,
54+
];
4055

4156
let callGatewayModulePromise;
4257

@@ -1138,37 +1153,109 @@ export function assertResourceCeiling(sample) {
11381153
}
11391154
}
11401155

1156+
export function findErrorLogFindings(logPath) {
1157+
if (!fs.existsSync(logPath)) {
1158+
return [];
1159+
}
1160+
const scanBytes = fs.statSync(logPath).size;
1161+
1162+
const findings = [];
1163+
let currentLine = "";
1164+
let currentLineNumber = 1;
1165+
let currentLineHasFinding = false;
1166+
let currentLineTruncated = false;
1167+
const recordLine = (lineNumber, line) => {
1168+
if (currentLineHasFinding) {
1169+
return;
1170+
}
1171+
if (
1172+
ERROR_LOG_ALLOW_PATTERNS.some((pattern) => pattern.test(line)) ||
1173+
!ERROR_LOG_DENY_PATTERNS.some((pattern) => pattern.test(line))
1174+
) {
1175+
return;
1176+
}
1177+
currentLineHasFinding = true;
1178+
findings.push({ line, lineNumber });
1179+
if (findings.length > 20) {
1180+
findings.shift();
1181+
}
1182+
};
1183+
const inspectCurrentLine = () => {
1184+
const normalizedLine = currentLine.replace(/\r$/u, "");
1185+
const line = currentLineTruncated ? `[truncated] ${normalizedLine}` : normalizedLine;
1186+
recordLine(currentLineNumber, line);
1187+
};
1188+
const appendLineFragment = (fragment) => {
1189+
currentLine += fragment;
1190+
if (currentLine.length <= LOG_SCAN_MAX_LINE_CHARS) {
1191+
return;
1192+
}
1193+
inspectCurrentLine();
1194+
currentLine = currentLine.slice(-LOG_SCAN_MAX_LINE_CHARS);
1195+
currentLineTruncated = true;
1196+
};
1197+
const finishLine = () => {
1198+
inspectCurrentLine();
1199+
currentLine = "";
1200+
currentLineNumber += 1;
1201+
currentLineHasFinding = false;
1202+
currentLineTruncated = false;
1203+
};
1204+
1205+
const fd = fs.openSync(logPath, "r");
1206+
try {
1207+
const buffer = Buffer.allocUnsafe(LOG_SCAN_CHUNK_BYTES);
1208+
let offset = 0;
1209+
while (offset < scanBytes) {
1210+
const bytesToRead = Math.min(buffer.length, scanBytes - offset);
1211+
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, offset);
1212+
if (bytesRead <= 0) {
1213+
break;
1214+
}
1215+
offset += bytesRead;
1216+
const lines = buffer.subarray(0, bytesRead).toString("utf8").split(/\n/u);
1217+
for (const [index, line] of lines.entries()) {
1218+
appendLineFragment(line);
1219+
if (index < lines.length - 1) {
1220+
finishLine();
1221+
}
1222+
}
1223+
}
1224+
} finally {
1225+
fs.closeSync(fd);
1226+
}
1227+
if (currentLine) {
1228+
inspectCurrentLine();
1229+
}
1230+
return findings;
1231+
}
1232+
11411233
function assertNoErrorLogs(logPath) {
1142-
const log = fs.existsSync(logPath) ? fs.readFileSync(logPath, "utf8") : "";
1143-
const deny = [
1144-
/\buncaught exception\b/iu,
1145-
/\bunhandled rejection\b/iu,
1146-
/\bfatal\b/iu,
1147-
/\bpanic\b/iu,
1148-
/\blevel["']?\s*:\s*["']error["']/iu,
1149-
/\[(?:error|ERROR)\]/u,
1150-
];
1151-
const allow = [/0 errors?/iu, /expected no diagnostics errors?/iu, /diagnostics errors?:\s*$/iu];
1152-
const findings = log
1153-
.split(/\r?\n/u)
1154-
.map((line, index) => ({ line, lineNumber: index + 1 }))
1155-
.filter(({ line }) => !allow.some((pattern) => pattern.test(line)))
1156-
.filter(({ line }) => deny.some((pattern) => pattern.test(line)));
1234+
const findings = findErrorLogFindings(logPath);
11571235
if (findings.length > 0) {
11581236
throw new Error(
11591237
`unexpected error-like gateway logs:\n${findings
1160-
.slice(-20)
11611238
.map(({ line, lineNumber }) => `${logPath}:${lineNumber}: ${line}`)
11621239
.join("\n")}`,
11631240
);
11641241
}
11651242
}
11661243

1167-
function tailFile(file) {
1244+
export function tailFile(file, maxBytes = LOG_TAIL_BYTES) {
11681245
if (!fs.existsSync(file)) {
11691246
return "";
11701247
}
1171-
return tailText(fs.readFileSync(file, "utf8"));
1248+
const stat = fs.statSync(file);
1249+
const start = Math.max(0, stat.size - Math.max(1, maxBytes));
1250+
const length = stat.size - start;
1251+
const fd = fs.openSync(file, "r");
1252+
try {
1253+
const buffer = Buffer.allocUnsafe(length);
1254+
fs.readSync(fd, buffer, 0, length, start);
1255+
return tailText(buffer.toString("utf8"));
1256+
} finally {
1257+
fs.closeSync(fd);
1258+
}
11721259
}
11731260

11741261
function tailText(text) {

test/scripts/kitchen-sink-rpc-walk.test.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import {
1212
createGatewayReadyLogScanner,
1313
extractPluginCommandNames,
1414
fetchJson,
15+
findErrorLogFindings,
1516
findDistCallGatewayModuleFiles,
1617
makeEnv,
1718
runCommand,
1819
sampleProcess,
1920
sampleWindowsProcessByPort,
2021
stopGateway,
2122
summarizeProcessSamples,
23+
tailFile,
2224
usesBuiltOpenClawEntry,
2325
} from "../../scripts/e2e/kitchen-sink-rpc-walk.mjs";
2426

@@ -112,6 +114,55 @@ describe("kitchen-sink RPC gateway readiness logs", () => {
112114
rmSync(root, { recursive: true, force: true });
113115
}
114116
});
117+
118+
it("tails large gateway logs without returning older content", () => {
119+
const root = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-rpc-log-tail-"));
120+
try {
121+
const logPath = path.join(root, "gateway.log");
122+
writeFileSync(logPath, `old fatal marker\n${"noise\n".repeat(2000)}recent ready\n`);
123+
124+
const tail = tailFile(logPath, 128);
125+
126+
expect(tail).toContain("recent ready");
127+
expect(tail).not.toContain("old fatal marker");
128+
} finally {
129+
rmSync(root, { recursive: true, force: true });
130+
}
131+
});
132+
133+
it("scans gateway error logs incrementally and keeps the latest findings", () => {
134+
const root = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-rpc-log-errors-"));
135+
try {
136+
const logPath = path.join(root, "gateway.log");
137+
writeFileSync(logPath, `${"ordinary line\n".repeat(2000)}0 errors\n[ERROR] late failure\n`);
138+
139+
expect(findErrorLogFindings(logPath)).toEqual([
140+
{
141+
line: "[ERROR] late failure",
142+
lineNumber: 2002,
143+
},
144+
]);
145+
} finally {
146+
rmSync(root, { recursive: true, force: true });
147+
}
148+
});
149+
150+
it("bounds scanner memory for very long log lines", () => {
151+
const root = mkdtempSync(path.join(tmpdir(), "openclaw-kitchen-rpc-log-long-line-"));
152+
try {
153+
const logPath = path.join(root, "gateway.log");
154+
writeFileSync(logPath, `${"x".repeat(200_000)}[ERROR] giant line\n`);
155+
156+
const findings = findErrorLogFindings(logPath);
157+
158+
expect(findings).toHaveLength(1);
159+
expect(findings[0]?.lineNumber).toBe(1);
160+
expect(findings[0]?.line).toContain("[truncated]");
161+
expect(findings[0]?.line.length).toBeLessThan(20_000);
162+
} finally {
163+
rmSync(root, { recursive: true, force: true });
164+
}
165+
});
115166
});
116167

117168
describe("kitchen-sink RPC command output capture", () => {

0 commit comments

Comments
 (0)