From a0c3067b545b345be03e2ce37594bf6b9c573aab Mon Sep 17 00:00:00 2001 From: "thomas.szbay" Date: Mon, 20 Jul 2026 06:48:53 +0800 Subject: [PATCH] fix(qa-lab): bound evidence checkout ref git probe with timeout (#111448) Co-authored-by: thomas.szbay --- .../qa-lab/src/evidence-environment.test.ts | 66 +++++++++++++++++++ extensions/qa-lab/src/evidence-environment.ts | 6 ++ 2 files changed, 72 insertions(+) create mode 100644 extensions/qa-lab/src/evidence-environment.test.ts diff --git a/extensions/qa-lab/src/evidence-environment.test.ts b/extensions/qa-lab/src/evidence-environment.test.ts new file mode 100644 index 00000000000..29f68a79717 --- /dev/null +++ b/extensions/qa-lab/src/evidence-environment.test.ts @@ -0,0 +1,66 @@ +// Qa Lab tests bound the evidence checkout ref git probe. +import { afterEach, describe, expect, it, vi } from "vitest"; + +const execFileSyncMock = vi.hoisted(() => vi.fn()); + +vi.mock("node:child_process", async () => { + const actual = await vi.importActual("node:child_process"); + return { + ...actual, + execFileSync: execFileSyncMock, + }; +}); + +import { resolveQaEvidenceEnvironment } from "./evidence-environment.js"; + +afterEach(() => { + vi.restoreAllMocks(); + execFileSyncMock.mockReset(); +}); + +describe("resolveQaEvidenceEnvironment", () => { + it("bounds the checkout ref git probe with a timeout", () => { + execFileSyncMock.mockReturnValue("abc123\n"); + + const environment = resolveQaEvidenceEnvironment({ env: {} }); + + expect(environment.ref).toBe("abc123"); + expect(execFileSyncMock).toHaveBeenCalledWith( + "git", + ["rev-parse", "--verify", "HEAD"], + expect.objectContaining({ + killSignal: "SIGKILL", + timeout: 5_000, + }), + ); + }); + + it("falls back to GITHUB_SHA when the git probe times out", () => { + execFileSyncMock.mockImplementation(() => { + throw Object.assign(new Error("Command timed out"), { code: "ETIMEDOUT" }); + }); + + const environment = resolveQaEvidenceEnvironment({ env: { GITHUB_SHA: "fallbacksha" } }); + + expect(environment.ref).toBe("fallbacksha"); + }); + + it("prefers OPENCLAW_QA_REF without invoking git", () => { + const environment = resolveQaEvidenceEnvironment({ + env: { OPENCLAW_QA_REF: "qa-ref", GITHUB_SHA: "fallbacksha" }, + }); + + expect(environment.ref).toBe("qa-ref"); + expect(execFileSyncMock).not.toHaveBeenCalled(); + }); + + it("returns a null ref when the probe fails and no env fallback exists", () => { + execFileSyncMock.mockImplementation(() => { + throw new Error("git unavailable"); + }); + + const environment = resolveQaEvidenceEnvironment({ env: {} }); + + expect(environment.ref).toBeNull(); + }); +}); diff --git a/extensions/qa-lab/src/evidence-environment.ts b/extensions/qa-lab/src/evidence-environment.ts index 88e6ec1e999..bbbca51bfa6 100644 --- a/extensions/qa-lab/src/evidence-environment.ts +++ b/extensions/qa-lab/src/evidence-environment.ts @@ -1,12 +1,18 @@ // Qa Lab plugin module resolves evidence runtime metadata. import { execFileSync } from "node:child_process"; +// A wedged git (NFS hang, credential helper prompt) must not block evidence +// metadata resolution; the caller already falls back to GITHUB_SHA/null. +const QA_EVIDENCE_GIT_PROBE_TIMEOUT_MS = 5_000; + function resolveQaEvidenceCheckoutRef(repoRoot?: string) { try { const ref = execFileSync("git", ["rev-parse", "--verify", "HEAD"], { cwd: repoRoot ?? process.cwd(), encoding: "utf8", + killSignal: "SIGKILL", stdio: ["ignore", "pipe", "ignore"], + timeout: QA_EVIDENCE_GIT_PROBE_TIMEOUT_MS, }).trim(); return ref || undefined; } catch {