fix(test): stream QA Lab stdout artifacts (#95119)

* fix(test): bound QA Lab stdout artifact reads

* fix(test): scan QA Lab stdout artifacts incrementally
This commit is contained in:
Vincent Koc
2026-06-20 07:16:14 +08:00
committed by GitHub
parent 1e105d5340
commit f9f94e7dcd
2 changed files with 90 additions and 3 deletions

View File

@@ -3,7 +3,7 @@
import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, open, rm, writeFile } from "node:fs/promises";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { Socket } from "node:net";
import { tmpdir } from "node:os";
@@ -158,6 +158,7 @@ const MAX_STDOUT_DIAGNOSTIC_LINE_BYTES = readPositiveIntegerEnv(
"OPENCLAW_QA_OTEL_MAX_STDOUT_DIAGNOSTIC_LINE_BYTES",
512 * 1024,
);
const GATEWAY_STDOUT_ARTIFACT_READ_CHUNK_BYTES = 64 * 1024;
function readPositiveIntegerEnv(
name: string,
@@ -1075,7 +1076,11 @@ async function appendGatewayStdoutArtifactLogs(params: {
"gateway.stdout.log",
);
try {
params.capture.append(await readFile(gatewayStdoutPath, "utf8"));
await appendUtf8FileToStdoutDiagnosticCapture(
gatewayStdoutPath,
params.capture,
GATEWAY_STDOUT_ARTIFACT_READ_CHUNK_BYTES,
);
params.capture.flush();
} catch (error) {
if (!isErrnoCode(error, "ENOENT")) {
@@ -1084,6 +1089,26 @@ async function appendGatewayStdoutArtifactLogs(params: {
}
}
async function appendUtf8FileToStdoutDiagnosticCapture(
filePath: string,
capture: ReturnType<typeof createStdoutDiagnosticLogCapture>,
chunkBytes = GATEWAY_STDOUT_ARTIFACT_READ_CHUNK_BYTES,
): Promise<void> {
const file = await open(filePath, "r");
try {
const buffer = Buffer.alloc(Math.max(1, chunkBytes));
for (;;) {
const { bytesRead } = await file.read(buffer, 0, buffer.length);
if (bytesRead === 0) {
break;
}
capture.append(buffer.subarray(0, bytesRead));
}
} finally {
await file.close();
}
}
async function stopDockerContainer(name: string): Promise<void> {
await new Promise<void>((resolve) => {
const child = spawn("docker", ["stop", name], {
@@ -1882,14 +1907,17 @@ async function main() {
}
export const testing = {
appendGatewayStdoutArtifactLogs,
appendCapturedBodyText,
assertSmoke,
createBoundedTextAccumulator,
createStdoutDiagnosticLogCapture,
decodeRequestBody,
parseArgs,
parseStdoutDiagnosticLogLine,
readPositiveIntegerEnv,
readRequestBody,
appendUtf8FileToStdoutDiagnosticCapture,
startLocalOtlpReceiver,
startDockerOtelCollector,
terminateChildTree,

View File

@@ -1,7 +1,7 @@
// QA OTEL Smoke tests cover QA Lab telemetry evidence.
import { spawn, spawnSync } from "node:child_process";
import { EventEmitter } from "node:events";
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from "node:fs";
import { createConnection as createNetConnection } from "node:net";
import os from "node:os";
import path from "node:path";
@@ -501,6 +501,65 @@ describe("qa-otel-smoke receiver bounds", () => {
expect(output.text()).not.toContain("DO_NOT_RETAIN_COLLECTOR_PREFIX");
});
it("streams gateway stdout artifact records without requiring them in the tail", async () => {
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "openclaw-qa-otel-stdout-stream-"));
const logPath = path.join(tempRoot, "gateway.stdout.log");
const capture = testing.createStdoutDiagnosticLogCapture();
const record = {
signal: "openclaw.diagnostic.log",
ts: "2026-06-18T00:00:00.000Z",
"service.name": "openclaw-qa-lab-otel-smoke",
severityText: "INFO",
severityNumber: 9,
body: "early log",
attributes: {},
};
try {
writeFileSync(
logPath,
`${JSON.stringify(record)}\n${"x".repeat(256 * 1024)}\nGATEWAY_STDOUT_TAIL\n`,
);
await testing.appendUtf8FileToStdoutDiagnosticCapture(logPath, capture);
capture.flush();
expect(capture.records).toEqual([record]);
expect(capture.lines).toHaveLength(1);
} finally {
rmSync(tempRoot, { force: true, recursive: true });
}
});
it("keeps gateway stdout artifact fallback parsing bounded", async () => {
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "openclaw-qa-otel-stdout-artifact-"));
const outputDir = path.join(tempRoot, "output");
const artifactDir = path.join(outputDir, "artifacts", "gateway-runtime");
const record = {
signal: "openclaw.diagnostic.log",
ts: "2026-06-18T00:00:00.000Z",
"service.name": "openclaw-qa-lab-otel-smoke",
severityText: "INFO",
severityNumber: 9,
body: "tail log",
attributes: {},
};
try {
mkdirSync(artifactDir, { recursive: true });
writeFileSync(
path.join(artifactDir, "gateway.stdout.log"),
`${JSON.stringify(record)}\n${"x".repeat(256 * 1024)}\n`,
);
const capture = testing.createStdoutDiagnosticLogCapture();
await testing.appendGatewayStdoutArtifactLogs({ capture, outputDir });
expect(capture.records).toEqual([record]);
expect(capture.lines).toHaveLength(1);
} finally {
rmSync(tempRoot, { force: true, recursive: true });
}
});
it("times out and kills a wedged QA suite child with a detached gateway", async () => {
if (process.platform === "win32") {
return;