mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
QA Matrix: exit cleanly on failure
Make the Matrix QA CLI single-shot exit contract symmetric: artifact-backed failures now print the preserved error, flush stdio, and exit with code 1 instead of waiting on Matrix native handles. Keep an opt-out for direct test harnesses with OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT.
This commit is contained in:
@@ -1,8 +1,55 @@
|
||||
import { Command } from "commander";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { runQaMatrixCommand } = vi.hoisted(() => ({
|
||||
runQaMatrixCommand: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./cli.runtime.js", () => ({
|
||||
runQaMatrixCommand,
|
||||
}));
|
||||
|
||||
import { matrixQaCliRegistration } from "./cli.js";
|
||||
|
||||
function mockProcessWrite(
|
||||
_chunk: string | Uint8Array,
|
||||
encodingOrCallback?: BufferEncoding | ((err?: Error | null) => void),
|
||||
callback?: (err?: Error | null) => void,
|
||||
) {
|
||||
if (typeof encodingOrCallback === "function") {
|
||||
encodingOrCallback();
|
||||
} else {
|
||||
callback?.();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
describe("matrix qa cli registration", () => {
|
||||
const originalDisableForceExit = process.env.OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT;
|
||||
let exitSpy: ReturnType<typeof vi.spyOn>;
|
||||
let stderrSpy: ReturnType<typeof vi.spyOn>;
|
||||
let stdoutSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
runQaMatrixCommand.mockReset();
|
||||
exitSpy = vi.spyOn(process, "exit").mockImplementation((code?: string | number | null) => {
|
||||
throw new Error(`process.exit(${String(code)})`);
|
||||
});
|
||||
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(mockProcessWrite);
|
||||
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(mockProcessWrite);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalDisableForceExit === undefined) {
|
||||
delete process.env.OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT;
|
||||
} else {
|
||||
process.env.OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT = originalDisableForceExit;
|
||||
}
|
||||
exitSpy.mockRestore();
|
||||
stderrSpy.mockRestore();
|
||||
stdoutSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("keeps disposable Matrix lane flags focused", () => {
|
||||
const qa = new Command();
|
||||
|
||||
@@ -26,4 +73,27 @@ describe("matrix qa cli registration", () => {
|
||||
expect(optionNames).not.toContain("--credential-source");
|
||||
expect(optionNames).not.toContain("--credential-role");
|
||||
});
|
||||
|
||||
it("exits with failure after Matrix artifacts are written for a failed run", async () => {
|
||||
const qa = new Command();
|
||||
matrixQaCliRegistration.register(qa);
|
||||
runQaMatrixCommand.mockRejectedValue(new Error("Matrix QA failed.\nreport: /tmp/report.md"));
|
||||
|
||||
await expect(qa.parseAsync(["node", "openclaw", "matrix"])).rejects.toThrow("process.exit(1)");
|
||||
|
||||
expect(runQaMatrixCommand).toHaveBeenCalledOnce();
|
||||
expect(stderrSpy).toHaveBeenCalledWith("Matrix QA failed.\nreport: /tmp/report.md\n");
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("can disable the forced exit for direct test harnesses", async () => {
|
||||
process.env.OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT = "1";
|
||||
const qa = new Command();
|
||||
matrixQaCliRegistration.register(qa);
|
||||
runQaMatrixCommand.mockRejectedValue(new Error("scenario failed"));
|
||||
|
||||
await expect(qa.parseAsync(["node", "openclaw", "matrix"])).rejects.toThrow("scenario failed");
|
||||
|
||||
expect(exitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Command } from "commander";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import {
|
||||
createLazyCliRuntimeLoader,
|
||||
createLiveTransportQaCliRegistration,
|
||||
@@ -8,6 +9,8 @@ import {
|
||||
|
||||
type MatrixQaCliRuntime = typeof import("./cli.runtime.js");
|
||||
|
||||
const DISABLE_MATRIX_QA_FORCE_EXIT_ENV = "OPENCLAW_QA_MATRIX_DISABLE_FORCE_EXIT";
|
||||
|
||||
const loadMatrixQaCliRuntime = createLazyCliRuntimeLoader<MatrixQaCliRuntime>(
|
||||
() => import("./cli.runtime.js"),
|
||||
);
|
||||
@@ -25,16 +28,26 @@ async function flushProcessStream(stream: NodeJS.WriteStream) {
|
||||
});
|
||||
}
|
||||
|
||||
async function exitMatrixQaCommand(code: number): Promise<never> {
|
||||
// Matrix crypto native handles can outlive the QA run even after every
|
||||
// client/gateway/harness has been stopped. This command is single-shot, so
|
||||
// artifact completion should terminate deterministically on both pass and fail.
|
||||
await Promise.all([flushProcessStream(process.stdout), flushProcessStream(process.stderr)]);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
async function runQaMatrix(opts: LiveTransportQaCommandOptions) {
|
||||
const runtime = await loadMatrixQaCliRuntime();
|
||||
await runtime.runQaMatrixCommand(opts);
|
||||
if (process.env.OPENCLAW_QA_MATRIX_DISABLE_SUCCESS_EXIT !== "1") {
|
||||
// Matrix crypto native handles can outlive the QA run even after every
|
||||
// client/gateway/harness has been stopped. This command is single-shot, so
|
||||
// a successful artifact write should terminate deterministically instead of
|
||||
// waiting for external timeout cleanup.
|
||||
await Promise.all([flushProcessStream(process.stdout), flushProcessStream(process.stderr)]);
|
||||
process.exit(0);
|
||||
if (process.env[DISABLE_MATRIX_QA_FORCE_EXIT_ENV] === "1") {
|
||||
await runtime.runQaMatrixCommand(opts);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await runtime.runQaMatrixCommand(opts);
|
||||
await exitMatrixQaCommand(0);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${formatErrorMessage(error)}\n`);
|
||||
await exitMatrixQaCommand(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user