From cddc84752d2bcd0d25cb6b12728e680b548bc488 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Mon, 27 Jul 2026 23:08:08 +0800 Subject: [PATCH] fix(miner): add blocked_own_open_pr to the AttemptCliResult union runAttempt builds and casts a blocked_own_open_pr result (the #8808 idempotency guard) but AttemptCliResult's union had no matching member, so loop-cli.ts's re-exported AttemptCliResult["outcome"] drifted from the real runtime outcome. Add the member with duplicateResult's exact shape (CommonAttemptResultFields + reason + existingPullRequestNumber). The as-AttemptCliResult cast stays: tsc confirms it is still required because the object literal's outcome widens to string, identical to every sibling result cast site (including in-union outcomes like dry_run). Adds a compile-time guard test asserting the outcome is assignable without a cast, so the .d.ts drift cannot silently return. --- packages/loopover-miner/lib/attempt-cli.ts | 1 + test/unit/miner-attempt-cli.test.ts | 25 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/loopover-miner/lib/attempt-cli.ts b/packages/loopover-miner/lib/attempt-cli.ts index 6d430e532c..00e90641e6 100644 --- a/packages/loopover-miner/lib/attempt-cli.ts +++ b/packages/loopover-miner/lib/attempt-cli.ts @@ -98,6 +98,7 @@ type CommonAttemptResultFields = { export type AttemptCliResult = | (CommonAttemptResultFields & { outcome: "dry_run" }) | (CommonAttemptResultFields & { outcome: "blocked_rejection_signaled"; reason: string }) + | (CommonAttemptResultFields & { outcome: "blocked_own_open_pr"; reason: string; existingPullRequestNumber: number }) | (CommonAttemptResultFields & { outcome: "blocked_worktree_preparation_failed"; reason: string }) | (CommonAttemptResultFields & { outcome: "blocked_infeasible"; diff --git a/test/unit/miner-attempt-cli.test.ts b/test/unit/miner-attempt-cli.test.ts index 22b1c5359d..68b0cb2333 100644 --- a/test/unit/miner-attempt-cli.test.ts +++ b/test/unit/miner-attempt-cli.test.ts @@ -16,7 +16,7 @@ import { closeDefaultWorktreeAllocator, openWorktreeAllocator } from "../../pack import { closeDefaultPortfolioQueueStore } from "../../packages/loopover-miner/lib/portfolio-queue"; import { closeDefaultGovernorState } from "../../packages/loopover-miner/lib/governor-state"; import { buildAttemptDeps, parseAttemptArgs, runAttempt, resolveAttemptHouseRulesConfig } from "../../packages/loopover-miner/lib/attempt-cli"; -import type { RunAttemptOptions } from "../../packages/loopover-miner/lib/attempt-cli"; +import type { AttemptCliResult, RunAttemptOptions } from "../../packages/loopover-miner/lib/attempt-cli"; import type { RuleFiredEvent, SignalStore } from "../../packages/loopover-engine/src/calibration/signal-tracking"; import * as minerSentryModule from "../../packages/loopover-miner/lib/sentry"; import * as liveIssueSnapshotModule from "../../packages/loopover-miner/lib/live-issue-snapshot"; @@ -143,6 +143,29 @@ afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); +describe("AttemptCliResult union (#9331)", () => { + it("includes the blocked_own_open_pr outcome with the runtime duplicateResult shape", () => { + // Compile-time guard: this object is the exact shape runAttempt builds for a crash-retry duplicate + // (#8808). Before the union member was added it only type-checked behind an `as` cast; removing the + // member makes this assignment a type error, so the .d.ts drift can't silently return. + const result: AttemptCliResult = { + outcome: "blocked_own_open_pr", + reason: "this miner already has an open PR for this issue", + repoFullName: "acme/widgets", + issueNumber: 7, + existingPullRequestNumber: 42, + minerLogin: "miner", + base: "main", + mode: "live", + attemptId: "attempt-1", + }; + expect(result.outcome).toBe("blocked_own_open_pr"); + if (result.outcome === "blocked_own_open_pr") { + expect(result.existingPullRequestNumber).toBe(42); + } + }); +}); + describe("parseAttemptArgs (#5132)", () => { it("parses a full, valid argv", () => { expect(