From 0c977cd687939eb61f80f6d5d78d57aa9a809e7d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 5 May 2026 12:50:44 +0100 Subject: [PATCH] fix: avoid early Slack credential leases in Mantis --- .../workflows/mantis-slack-desktop-smoke.yml | 12 ++++ .../slack-desktop-smoke.runtime.test.ts | 68 +++++++++++++++++++ .../src/mantis/slack-desktop-smoke.runtime.ts | 18 ++--- 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/.github/workflows/mantis-slack-desktop-smoke.yml b/.github/workflows/mantis-slack-desktop-smoke.yml index 8fb019ec6cc..8c06752b6d9 100644 --- a/.github/workflows/mantis-slack-desktop-smoke.yml +++ b/.github/workflows/mantis-slack-desktop-smoke.yml @@ -229,6 +229,7 @@ jobs: keep_args=(--no-keep-lease) fi + set +e pnpm openclaw qa mantis slack-desktop-smoke \ --repo-root "$candidate_repo" \ --output-dir "$output_rel" \ @@ -245,6 +246,13 @@ jobs: --fast \ --scenario "$SCENARIO_ID" \ "${keep_args[@]}" + mantis_exit=$? + set -e + + if [[ ! -f "$root/mantis-slack-desktop-smoke-summary.json" ]]; then + echo "Mantis Slack desktop smoke did not produce a summary." >&2 + exit "$mantis_exit" + fi if [[ -f "$root/slack-desktop-smoke.mp4" ]]; then if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v ffprobe >/dev/null 2>&1; then @@ -296,6 +304,10 @@ jobs: echo "Slack desktop smoke failed." >&2 exit 1 fi + if [[ "$mantis_exit" -ne 0 ]]; then + echo "Slack desktop smoke exited with $mantis_exit after reporting status $status." >&2 + exit "$mantis_exit" + fi - name: Upload Mantis Slack desktop artifacts id: upload_artifact diff --git a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.test.ts b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.test.ts index d950e08515b..03a4b951c07 100644 --- a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.test.ts +++ b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.test.ts @@ -157,9 +157,11 @@ describe("mantis Slack desktop smoke runtime", () => { it("leases Convex Slack credentials for gateway setup and maps them into the VM env", async () => { const commands: { args: readonly string[]; command: string; env?: NodeJS.ProcessEnv }[] = []; + const events: string[] = []; const fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { const url = describeFetchInput(input); if (url.endsWith("/acquire")) { + events.push("acquire"); return new Response( JSON.stringify({ credentialId: "cred-slack", @@ -177,6 +179,7 @@ describe("mantis Slack desktop smoke runtime", () => { ); } if (url.endsWith("/release") || url.endsWith("/heartbeat")) { + events.push(url.endsWith("/release") ? "release" : "heartbeat"); return new Response(JSON.stringify({ status: "ok" }), { status: 200 }); } throw new Error(`unexpected fetch: ${url} ${describeFetchBody(init?.body)}`); @@ -186,6 +189,7 @@ describe("mantis Slack desktop smoke runtime", () => { const runner = vi.fn( async (command: string, args: readonly string[], options: { env?: NodeJS.ProcessEnv }) => { commands.push({ command, args, env: options.env }); + events.push(`${command}:${args[0]}`); if (command === "/tmp/crabbox" && args[0] === "warmup") { return { stdout: "ready lease cbx_c0ffee\n", stderr: "" }; } @@ -244,6 +248,17 @@ describe("mantis Slack desktop smoke runtime", () => { }); expect(result.status).toBe("pass"); + expect(events).toEqual( + expect.arrayContaining([ + "/tmp/crabbox:warmup", + "/tmp/crabbox:inspect", + "acquire", + "/tmp/crabbox:run", + "release", + ]), + ); + expect(events.indexOf("acquire")).toBeGreaterThan(events.indexOf("/tmp/crabbox:inspect")); + expect(events.indexOf("acquire")).toBeLessThan(events.indexOf("/tmp/crabbox:run")); const runCommand = commands.find( (entry) => entry.command === "/tmp/crabbox" && entry.args[0] === "run", ); @@ -274,6 +289,59 @@ describe("mantis Slack desktop smoke runtime", () => { expect(summary.slackUrl).toBe("https://app.slack.com/client/TLEASED/CLEASED"); }); + it("stops a created no-keep lease when the remote Slack QA run fails", async () => { + const commands: { args: readonly string[]; command: string }[] = []; + const runner = vi.fn(async (command: string, args: readonly string[]) => { + commands.push({ command, args }); + if (command === "/tmp/crabbox" && args[0] === "warmup") { + return { stdout: "ready lease cbx_fade123\n", stderr: "" }; + } + if (command === "/tmp/crabbox" && args[0] === "inspect") { + return { + stdout: `${JSON.stringify({ + host: "203.0.113.10", + id: "cbx_fade123", + provider: "hetzner", + sshKey: "/tmp/key", + sshPort: "2222", + sshUser: "crabbox", + })}\n`, + stderr: "", + }; + } + if (command === "/tmp/crabbox" && args[0] === "run") { + throw new Error("remote Slack QA failed"); + } + if (command === "rsync") { + const outputDir = args.at(-1); + await fs.mkdir(outputDir as string, { recursive: true }); + if (!String(outputDir).endsWith("slack-qa/")) { + await fs.writeFile(path.join(outputDir as string, "slack-desktop-smoke.png"), "png"); + await fs.writeFile(path.join(outputDir as string, "remote-metadata.json"), "{}\n"); + } + } + return { stdout: "", stderr: "" }; + }); + + const result = await runMantisSlackDesktopSmoke({ + commandRunner: runner, + crabboxBin: "/tmp/crabbox", + keepLease: false, + outputDir: ".artifacts/qa-e2e/mantis/slack-desktop-created-fail", + repoRoot, + }); + + expect(result.status).toBe("fail"); + expect(commands).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + args: ["stop", "--provider", "hetzner", "cbx_fade123"], + command: "/tmp/crabbox", + }), + ]), + ); + }); + it("passes gateway setup when Crabbox returns non-zero after remote metadata proves success", async () => { const runner = vi.fn(async (command: string, args: readonly string[]) => { if (command === "/tmp/crabbox" && args[0] === "warmup") { diff --git a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts index 8351a0b5860..882c20d9a08 100644 --- a/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts +++ b/extensions/qa-lab/src/mantis/slack-desktop-smoke.runtime.ts @@ -822,14 +822,6 @@ export async function runMantisSlackDesktopSmoke( let remoteMetadata: SlackDesktopRemoteMetadata | undefined; try { - const preparedCredentialEnv = await prepareGatewayCredentialEnv({ - credentialRole, - credentialSource, - env, - gatewaySetup, - }); - credentialLease = preparedCredentialEnv.credentialLease; - leaseHeartbeat = preparedCredentialEnv.leaseHeartbeat; leaseId = leaseId ?? (await warmupCrabbox({ @@ -850,6 +842,14 @@ export async function runMantisSlackDesktopSmoke( provider, runner, }); + const preparedCredentialEnv = await prepareGatewayCredentialEnv({ + credentialRole, + credentialSource, + env, + gatewaySetup, + }); + credentialLease = preparedCredentialEnv.credentialLease; + leaseHeartbeat = preparedCredentialEnv.leaseHeartbeat; let remoteRunError: unknown; await runCommand({ command: crabboxBin, @@ -989,7 +989,7 @@ export async function runMantisSlackDesktopSmoke( await fs.writeFile(summaryPath, `${JSON.stringify(summary, null, 2)}\n`, "utf8"); await fs.writeFile(reportPath, renderReport(summary), "utf8"); } - if (summary?.status === "pass" && createdLease && leaseId && !keepLease) { + if (createdLease && leaseId && !keepLease) { await stopCrabbox({ crabboxBin, cwd: repoRoot, env, leaseId, provider, runner }); } if (leaseHeartbeat) {