From f4b9d246216afd95d13d41a0fbbf1cbbdee8b6a2 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 27 May 2026 13:12:55 +0200 Subject: [PATCH] fix(qa): stop Matrix phases after run timeout --- .../src/runners/contract/runtime.test.ts | 32 +++++++++++++++++++ .../qa-matrix/src/runners/contract/runtime.ts | 17 ++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/extensions/qa-matrix/src/runners/contract/runtime.test.ts b/extensions/qa-matrix/src/runners/contract/runtime.test.ts index 605374e9e01..36a56272435 100644 --- a/extensions/qa-matrix/src/runners/contract/runtime.test.ts +++ b/extensions/qa-matrix/src/runners/contract/runtime.test.ts @@ -4,6 +4,7 @@ import { renderQaMarkdownReport } from "../../report.js"; import { testing as liveTesting } from "./runtime.js"; afterEach(() => { + vi.restoreAllMocks(); vi.useRealTimers(); }); @@ -106,6 +107,37 @@ describe("matrix live qa runtime", () => { } }); + it("does not start Matrix QA work after the hard run deadline expires", async () => { + const task = vi.fn(async () => "started"); + vi.spyOn(Date, "now").mockReturnValue(1_001); + + await expect( + liveTesting.withMatrixQaRunDeadline( + { + deadlineMs: 1_000, + timeoutMs: 30_000, + }, + "Matrix scenario late", + task, + ), + ).rejects.toThrow(/Matrix scenario late not started because Matrix QA run timed out/u); + expect(task).not.toHaveBeenCalled(); + }); + + it("passes the remaining Matrix QA run budget to the phase timeout", async () => { + vi.spyOn(Date, "now").mockReturnValue(1_000); + + expect( + liveTesting.remainingMatrixQaRunMs( + { + deadlineMs: 1_250, + timeoutMs: 30_000, + }, + "Matrix canary", + ), + ).toBe(250); + }); + it("normalizes the Matrix QA canary timeout env", () => { const previous = process.env.OPENCLAW_QA_MATRIX_CANARY_TIMEOUT_MS; try { diff --git a/extensions/qa-matrix/src/runners/contract/runtime.ts b/extensions/qa-matrix/src/runners/contract/runtime.ts index 1a657c4d2ca..1bae85832d1 100644 --- a/extensions/qa-matrix/src/runners/contract/runtime.ts +++ b/extensions/qa-matrix/src/runners/contract/runtime.ts @@ -201,8 +201,17 @@ function resolveMatrixQaCanaryTimeoutMs() { ); } -function remainingMatrixQaRunMs(deadline: { deadlineMs: number }) { - return Math.max(1, deadline.deadlineMs - Date.now()); +function remainingMatrixQaRunMs( + deadline: { deadlineMs: number; timeoutMs: number }, + label: string, +) { + const remainingMs = Math.floor(deadline.deadlineMs - Date.now()); + if (!Number.isFinite(deadline.deadlineMs) || remainingMs <= 0) { + throw new Error( + `${label} not started because Matrix QA run timed out after ${formatMatrixQaDurationMs(deadline.timeoutMs)}`, + ); + } + return remainingMs; } async function withMatrixQaTimeout( @@ -232,7 +241,7 @@ async function withMatrixQaRunDeadline( label: string, task: () => Promise, ) { - return await withMatrixQaTimeout(label, remainingMatrixQaRunMs(deadline), task); + return await withMatrixQaTimeout(label, remainingMatrixQaRunMs(deadline, label), task); } async function cleanupMatrixQaResource(params: { @@ -1137,10 +1146,12 @@ export const testing = { findMatrixQaScenarios, isMatrixAccountReady, patchMatrixQaGatewayConfig, + remainingMatrixQaRunMs, resolveMatrixQaCanaryTimeoutMs, resolveMatrixQaModels, shouldWriteMatrixQaProgress, summarizeMatrixQaConfigSnapshot, waitForMatrixChannelReady, + withMatrixQaRunDeadline, }; export { testing as __testing };