Skip to content

Commit f9f94e7

Browse files
authored
fix(test): stream QA Lab stdout artifacts (#95119)
* fix(test): bound QA Lab stdout artifact reads * fix(test): scan QA Lab stdout artifacts incrementally
1 parent 1e105d5 commit f9f94e7

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

test/e2e/qa-lab/runtime/qa-otel-smoke-runtime.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
44
import { randomUUID } from "node:crypto";
55
import { existsSync } from "node:fs";
6-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
6+
import { mkdir, mkdtemp, open, rm, writeFile } from "node:fs/promises";
77
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
88
import { Socket } from "node:net";
99
import { tmpdir } from "node:os";
@@ -158,6 +158,7 @@ const MAX_STDOUT_DIAGNOSTIC_LINE_BYTES = readPositiveIntegerEnv(
158158
"OPENCLAW_QA_OTEL_MAX_STDOUT_DIAGNOSTIC_LINE_BYTES",
159159
512 * 1024,
160160
);
161+
const GATEWAY_STDOUT_ARTIFACT_READ_CHUNK_BYTES = 64 * 1024;
161162

162163
function readPositiveIntegerEnv(
163164
name: string,
@@ -1075,7 +1076,11 @@ async function appendGatewayStdoutArtifactLogs(params: {
10751076
"gateway.stdout.log",
10761077
);
10771078
try {
1078-
params.capture.append(await readFile(gatewayStdoutPath, "utf8"));
1079+
await appendUtf8FileToStdoutDiagnosticCapture(
1080+
gatewayStdoutPath,
1081+
params.capture,
1082+
GATEWAY_STDOUT_ARTIFACT_READ_CHUNK_BYTES,
1083+
);
10791084
params.capture.flush();
10801085
} catch (error) {
10811086
if (!isErrnoCode(error, "ENOENT")) {
@@ -1084,6 +1089,26 @@ async function appendGatewayStdoutArtifactLogs(params: {
10841089
}
10851090
}
10861091

1092+
async function appendUtf8FileToStdoutDiagnosticCapture(
1093+
filePath: string,
1094+
capture: ReturnType<typeof createStdoutDiagnosticLogCapture>,
1095+
chunkBytes = GATEWAY_STDOUT_ARTIFACT_READ_CHUNK_BYTES,
1096+
): Promise<void> {
1097+
const file = await open(filePath, "r");
1098+
try {
1099+
const buffer = Buffer.alloc(Math.max(1, chunkBytes));
1100+
for (;;) {
1101+
const { bytesRead } = await file.read(buffer, 0, buffer.length);
1102+
if (bytesRead === 0) {
1103+
break;
1104+
}
1105+
capture.append(buffer.subarray(0, bytesRead));
1106+
}
1107+
} finally {
1108+
await file.close();
1109+
}
1110+
}
1111+
10871112
async function stopDockerContainer(name: string): Promise<void> {
10881113
await new Promise<void>((resolve) => {
10891114
const child = spawn("docker", ["stop", name], {
@@ -1882,14 +1907,17 @@ async function main() {
18821907
}
18831908

18841909
export const testing = {
1910+
appendGatewayStdoutArtifactLogs,
18851911
appendCapturedBodyText,
18861912
assertSmoke,
18871913
createBoundedTextAccumulator,
1914+
createStdoutDiagnosticLogCapture,
18881915
decodeRequestBody,
18891916
parseArgs,
18901917
parseStdoutDiagnosticLogLine,
18911918
readPositiveIntegerEnv,
18921919
readRequestBody,
1920+
appendUtf8FileToStdoutDiagnosticCapture,
18931921
startLocalOtlpReceiver,
18941922
startDockerOtelCollector,
18951923
terminateChildTree,

test/e2e/qa-lab/runtime/qa-otel-smoke.e2e.test.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// QA OTEL Smoke tests cover QA Lab telemetry evidence.
22
import { spawn, spawnSync } from "node:child_process";
33
import { EventEmitter } from "node:events";
4-
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync } from "node:fs";
4+
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
55
import { createConnection as createNetConnection } from "node:net";
66
import os from "node:os";
77
import path from "node:path";
@@ -501,6 +501,65 @@ describe("qa-otel-smoke receiver bounds", () => {
501501
expect(output.text()).not.toContain("DO_NOT_RETAIN_COLLECTOR_PREFIX");
502502
});
503503

504+
it("streams gateway stdout artifact records without requiring them in the tail", async () => {
505+
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "openclaw-qa-otel-stdout-stream-"));
506+
const logPath = path.join(tempRoot, "gateway.stdout.log");
507+
const capture = testing.createStdoutDiagnosticLogCapture();
508+
const record = {
509+
signal: "openclaw.diagnostic.log",
510+
ts: "2026-06-18T00:00:00.000Z",
511+
"service.name": "openclaw-qa-lab-otel-smoke",
512+
severityText: "INFO",
513+
severityNumber: 9,
514+
body: "early log",
515+
attributes: {},
516+
};
517+
try {
518+
writeFileSync(
519+
logPath,
520+
`${JSON.stringify(record)}\n${"x".repeat(256 * 1024)}\nGATEWAY_STDOUT_TAIL\n`,
521+
);
522+
523+
await testing.appendUtf8FileToStdoutDiagnosticCapture(logPath, capture);
524+
capture.flush();
525+
526+
expect(capture.records).toEqual([record]);
527+
expect(capture.lines).toHaveLength(1);
528+
} finally {
529+
rmSync(tempRoot, { force: true, recursive: true });
530+
}
531+
});
532+
533+
it("keeps gateway stdout artifact fallback parsing bounded", async () => {
534+
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "openclaw-qa-otel-stdout-artifact-"));
535+
const outputDir = path.join(tempRoot, "output");
536+
const artifactDir = path.join(outputDir, "artifacts", "gateway-runtime");
537+
const record = {
538+
signal: "openclaw.diagnostic.log",
539+
ts: "2026-06-18T00:00:00.000Z",
540+
"service.name": "openclaw-qa-lab-otel-smoke",
541+
severityText: "INFO",
542+
severityNumber: 9,
543+
body: "tail log",
544+
attributes: {},
545+
};
546+
try {
547+
mkdirSync(artifactDir, { recursive: true });
548+
writeFileSync(
549+
path.join(artifactDir, "gateway.stdout.log"),
550+
`${JSON.stringify(record)}\n${"x".repeat(256 * 1024)}\n`,
551+
);
552+
const capture = testing.createStdoutDiagnosticLogCapture();
553+
554+
await testing.appendGatewayStdoutArtifactLogs({ capture, outputDir });
555+
556+
expect(capture.records).toEqual([record]);
557+
expect(capture.lines).toHaveLength(1);
558+
} finally {
559+
rmSync(tempRoot, { force: true, recursive: true });
560+
}
561+
});
562+
504563
it("times out and kills a wedged QA suite child with a detached gateway", async () => {
505564
if (process.platform === "win32") {
506565
return;

0 commit comments

Comments
 (0)