QA Matrix: capture full runner output

This commit is contained in:
Gustavo Madeira Santana
2026-04-16 16:14:27 -04:00
parent 988447ca24
commit 56a9fd4b34
9 changed files with 622 additions and 61 deletions

View File

@@ -50,6 +50,34 @@ function createExitedProcess(code: number | null, signal: string | null = null)
};
}
function createPipedExitedProcess(params: {
code?: number | null;
signal?: string | null;
stderr?: string;
stdout?: string;
}) {
const stdout = new EventEmitter();
const stderr = new EventEmitter();
return {
stdout,
stderr,
on: (event: string, cb: (code: number | null, signal: string | null) => void) => {
if (event === "exit") {
queueMicrotask(() => {
if (params.stdout) {
stdout.emit("data", Buffer.from(params.stdout));
}
if (params.stderr) {
stderr.emit("data", Buffer.from(params.stderr));
}
cb(params.code ?? 0, params.signal ?? null);
});
}
return undefined;
},
};
}
function createFakeProcess() {
return Object.assign(new EventEmitter(), {
pid: 4242,
@@ -322,6 +350,125 @@ describe("run-node script", () => {
});
});
it("tees launcher output into the requested generic output log", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp);
const outputPath = path.join(tmp, ".artifacts", "qa-e2e", "matrix", "output.log");
const spawnCalls: Array<{
args: string[];
env: Record<string, string | undefined>;
stdio: unknown;
}> = [];
const spawn = (_cmd: string, args: string[], options?: unknown) => {
const opts = options as { env?: NodeJS.ProcessEnv; stdio?: unknown } | undefined;
spawnCalls.push({
args,
env: { ...opts?.env },
stdio: opts?.stdio,
});
return createPipedExitedProcess({
stdout: args[0] === "openclaw.mjs" ? "child stdout\n" : "",
stderr: args[0] === "openclaw.mjs" ? "child stderr\n" : "",
});
};
const mutedStream = {
write: () => true,
} as unknown as NodeJS.WriteStream;
const exitCode = await runNodeMain({
cwd: tmp,
args: ["status"],
env: {
...process.env,
OPENCLAW_FORCE_BUILD: "1",
OPENCLAW_RUNNER_LOG: "1",
OPENCLAW_RUN_NODE_OUTPUT_LOG: outputPath,
},
spawn,
stderr: mutedStream,
stdout: mutedStream,
execPath: process.execPath,
platform: process.platform,
} as Parameters<typeof runNodeMain>[0] & { stdout: NodeJS.WriteStream });
expect(exitCode).toBe(0);
await expect(fs.readFile(outputPath, "utf-8")).resolves.toContain("child stdout\n");
await expect(fs.readFile(outputPath, "utf-8")).resolves.toContain("child stderr\n");
await expect(fs.readFile(outputPath, "utf-8")).resolves.toContain("[openclaw]");
expect(spawnCalls.at(-1)?.args).toEqual(["openclaw.mjs", "status"]);
expect(spawnCalls.at(-1)?.env.OPENCLAW_RUN_NODE_OUTPUT_LOG).toBe(outputPath);
expect(spawnCalls.at(-1)?.stdio).toEqual(["inherit", "pipe", "pipe"]);
});
});
it("surfaces generic output log stream errors", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp);
const outputPath = path.join(tmp, ".artifacts", "qa-e2e", "matrix", "output.log");
await fs.mkdir(outputPath, { recursive: true });
const spawn = () => createPipedExitedProcess({ stdout: "child stdout\n" });
const stderrChunks: string[] = [];
const mutedStream = {
write: (chunk: string | Buffer) => {
stderrChunks.push(String(chunk));
return true;
},
} as unknown as NodeJS.WriteStream;
const exitCode = await runNodeMain({
cwd: tmp,
args: ["status"],
env: {
...process.env,
OPENCLAW_RUNNER_LOG: "0",
OPENCLAW_RUN_NODE_OUTPUT_LOG: outputPath,
},
spawn,
stderr: mutedStream,
stdout: mutedStream,
execPath: process.execPath,
platform: process.platform,
} as Parameters<typeof runNodeMain>[0] & { stdout: NodeJS.WriteStream });
expect(exitCode).toBe(1);
expect(stderrChunks.join("")).toContain("Failed to write output log");
});
});
it("does not mutate Matrix QA args when no generic output log is requested", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp);
const spawnCalls: Array<{ args: string[]; env: Record<string, string | undefined> }> = [];
const spawn = (_cmd: string, args: string[], options?: unknown) => {
const opts = options as { env?: NodeJS.ProcessEnv } | undefined;
spawnCalls.push({ args, env: { ...opts?.env } });
return createPipedExitedProcess({});
};
const mutedStream = {
write: () => true,
} as unknown as NodeJS.WriteStream;
const exitCode = await runNodeMain({
cwd: tmp,
args: ["qa", "matrix"],
env: {
...process.env,
OPENCLAW_RUNNER_LOG: "0",
},
spawn,
stderr: mutedStream,
stdout: mutedStream,
execPath: process.execPath,
platform: process.platform,
} as Parameters<typeof runNodeMain>[0] & { stdout: NodeJS.WriteStream });
expect(exitCode).toBe(0);
const childArgs = spawnCalls.at(-1)?.args ?? [];
expect(childArgs).toEqual(["openclaw.mjs", "qa", "matrix"]);
expect(spawnCalls.at(-1)?.env.OPENCLAW_RUN_NODE_OUTPUT_LOG).toBeUndefined();
});
});
it("skips rebuilding when dist is current and the source tree is clean", async () => {
await withTempDir({ prefix: "openclaw-run-node-" }, async (tmp) => {
await setupTrackedProject(tmp, {