From 04d22d31aafbcd3c4dd588cce7b3b6fd7e510098 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Wed, 15 Jul 2026 16:23:35 +0800 Subject: [PATCH 1/4] fix(miner): label rejection-signaled aborts with the actual trigger reason resolveRejectionSignaled now returns a trigger-specific reason string instead of a bare boolean, and attempt-cli uses it for audit-trail events and CLI output so own-rejection-history blocks are no longer mislabeled as ai_usage_policy_ban. Closes #6055 Co-authored-by: Cursor --- packages/loopover-miner/lib/attempt-cli.js | 20 ++++--- .../loopover-miner/lib/rejection-signal.d.ts | 7 ++- .../loopover-miner/lib/rejection-signal.js | 24 +++++--- test/unit/miner-attempt-cli.test.ts | 56 +++++++++++++++++-- test/unit/miner-rejection-signal.test.ts | 20 ++++--- 5 files changed, 96 insertions(+), 31 deletions(-) diff --git a/packages/loopover-miner/lib/attempt-cli.js b/packages/loopover-miner/lib/attempt-cli.js index 6c57b989e2..9a48b2ee10 100644 --- a/packages/loopover-miner/lib/attempt-cli.js +++ b/packages/loopover-miner/lib/attempt-cli.js @@ -27,7 +27,7 @@ import { initEventLedger } from "./event-ledger.js"; import { initAttemptLog } from "./attempt-log.js"; import { initGovernorLedger } from "./governor-ledger.js"; import { openWorktreeAllocator } from "./worktree-allocator.js"; -import { resolveRejectionSignaled } from "./rejection-signal.js"; +import { REJECTION_REASON_AI_USAGE_POLICY_BAN, REJECTION_REASON_OWN_SUBMISSION_REJECTED, resolveRejectionSignaled } from "./rejection-signal.js"; import { cleanupAttemptWorktree, prepareAttemptWorktree } from "./attempt-worktree.js"; import { fetchSelfReviewContext } from "./self-review-context.js"; import { buildCodingTaskSpec } from "./coding-task-spec.js"; @@ -212,14 +212,14 @@ export async function runAttempt(args, options = {}) { attemptLog = (options.initAttemptLog ?? initAttemptLog)(); governorLedger = (options.initGovernorLedger ?? initGovernorLedger)(); - // Checked before acquiring a worktree slot: a banned repo should never consume one. This resolves the - // first of rejectionSignaled's two documented triggers (an explicit AI-usage-policy ban, #5132 follow-up) - // -- the second (a prior own-submission rejection on this exact repo) remains a documented gap, see - // rejection-signal.js's own header for why. + // Checked before acquiring a worktree slot: a rejection-signaled repo should never consume one. + // resolveRejectionSignaled resolves both documented triggers (#5132 policy ban, #5655 own-rejection + // history) and returns a trigger-specific reason string for accurate audit-trail labeling. const resolveRejection = options.resolveRejectionSignaled ?? resolveRejectionSignaled; - const rejectionSignaled = await resolveRejection(parsed.repoFullName, { fetchImpl: options.fetchImpl }); - if (rejectionSignaled) { - const reason = "ai_usage_policy_ban"; + const rejectionSignal = await resolveRejection(parsed.repoFullName, { fetchImpl: options.fetchImpl }); + if (rejectionSignal) { + const reason = + rejectionSignal === true ? REJECTION_REASON_AI_USAGE_POLICY_BAN : rejectionSignal; attemptLog.appendAttemptLogEvent({ eventType: "attempt_aborted", attemptId, @@ -247,7 +247,9 @@ export async function runAttempt(args, options = {}) { console.log(JSON.stringify(rejectedResult, null, 2)); } else { console.error( - `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this repo's AI-usage policy bans automated/AI-authored contributions.`, + reason === REJECTION_REASON_OWN_SUBMISSION_REJECTED + ? `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this miner was previously rejected on this repo.` + : `Attempt for ${parsed.repoFullName}#${parsed.issueNumber} is blocked: this repo's AI-usage policy bans automated/AI-authored contributions.`, ); } options.onResult?.(rejectedResult); diff --git a/packages/loopover-miner/lib/rejection-signal.d.ts b/packages/loopover-miner/lib/rejection-signal.d.ts index f9506441b9..8a31208440 100644 --- a/packages/loopover-miner/lib/rejection-signal.d.ts +++ b/packages/loopover-miner/lib/rejection-signal.d.ts @@ -16,10 +16,15 @@ export interface RejectionSignaledOptions extends OwnRejectionHistoryOptions { rawContentBaseUrl?: string; } +export type RejectionSignaledReason = "ai_usage_policy_ban" | "own_submission_rejected"; + +export const REJECTION_REASON_AI_USAGE_POLICY_BAN: "ai_usage_policy_ban"; +export const REJECTION_REASON_OWN_SUBMISSION_REJECTED: "own_submission_rejected"; + export function resolveRejectionSignaled( repoFullName: string, options?: RejectionSignaledOptions, -): Promise; +): Promise; export function resolveOwnRejectionHistory( repoFullName: string, diff --git a/packages/loopover-miner/lib/rejection-signal.js b/packages/loopover-miner/lib/rejection-signal.js index 2fdd8945d2..1d180e57d4 100644 --- a/packages/loopover-miner/lib/rejection-signal.js +++ b/packages/loopover-miner/lib/rejection-signal.js @@ -14,8 +14,14 @@ import { resolveRejection } from "./rejection-state-machine.js"; // resolved by resolveOwnRejectionHistory (#5655), closing the gap this header previously documented: it checks // each of this miner's recorded own-submissions on the repo (governor-state.js's listRecentOwnSubmissions, // #5134) against its live PR outcome via rejection-state-machine.js's resolveRejection (#4278) -- consuming both -// upstream modules without modifying either. resolveRejectionSignaled now returns true if EITHER trigger fires, -// so `rejectionSignaled` finally means what iterate-policy.ts's doc comment has always said. +// upstream modules without modifying either. resolveRejectionSignaled now returns a trigger-specific reason +// string if EITHER trigger fires (or `false` when neither does), so `rejectionSignaled` finally means what +// iterate-policy.ts's doc comment has always said. + +/** @typedef {"ai_usage_policy_ban" | "own_submission_rejected"} RejectionSignaledReason */ + +export const REJECTION_REASON_AI_USAGE_POLICY_BAN = "ai_usage_policy_ban"; +export const REJECTION_REASON_OWN_SUBMISSION_REJECTED = "own_submission_rejected"; const DEFAULT_RAW_CONTENT_BASE_URL = "https://raw.githubusercontent.com"; const MAX_POLICY_DOC_BYTES = 128 * 1024; @@ -149,13 +155,14 @@ export async function resolveOwnRejectionHistory(repoFullName, options = {}) { } /** - * Resolve whether the target repo has an explicit, live AI-usage-policy ban -- the first of - * `rejectionSignaled`'s two documented triggers. Returns `false` (never throws) on any fetch/parse failure, - * matching resolveAiPolicyVerdict's own fail-open default for an absent/unreadable policy doc. + * Resolve whether the target repo has signaled it does not want automated/AI-authored contributions -- + * either trigger documented above. Returns `false` (never throws) on any fetch/parse failure for the policy + * docs, matching resolveAiPolicyVerdict's own fail-open default for an absent/unreadable policy doc. When a + * trigger fires, returns a trigger-specific reason string so callers can label audit-trail events accurately. * * @param {string} repoFullName * @param {{ rawContentBaseUrl?: string, fetchImpl?: import("./self-review-context.js").SelfReviewContextFetch }} [options] - * @returns {Promise} + * @returns {Promise} */ export async function resolveRejectionSignaled(repoFullName, options = {}) { const target = parseRepoFullName(repoFullName); @@ -167,7 +174,8 @@ export async function resolveRejectionSignaled(repoFullName, options = {}) { const verdict = resolveAiPolicyVerdict({ aiUsage, contributing }); // First trigger: an explicit live AI-usage-policy ban. A ban short-circuits -- no need to also check history. - if (!verdict.allowed) return true; + if (!verdict.allowed) return REJECTION_REASON_AI_USAGE_POLICY_BAN; // Second trigger (#5655): a prior submission from this same miner on this exact repo was closed/rejected. - return resolveOwnRejectionHistory(repoFullName, options); + const ownHistoryRejected = await resolveOwnRejectionHistory(repoFullName, options); + return ownHistoryRejected ? REJECTION_REASON_OWN_SUBMISSION_REJECTED : false; } diff --git a/test/unit/miner-attempt-cli.test.ts b/test/unit/miner-attempt-cli.test.ts index 763e55cb6d..7915fd8449 100644 --- a/test/unit/miner-attempt-cli.test.ts +++ b/test/unit/miner-attempt-cli.test.ts @@ -18,6 +18,10 @@ import { closeDefaultGovernorState } from "../../packages/loopover-miner/lib/gov import { buildAttemptDeps, parseAttemptArgs, runAttempt } from "../../packages/loopover-miner/lib/attempt-cli.js"; import * as minerSentryModule from "../../packages/loopover-miner/lib/sentry.js"; import type { PrepareAttemptWorktreeResult } from "../../packages/loopover-miner/lib/attempt-worktree.js"; +import { + REJECTION_REASON_AI_USAGE_POLICY_BAN, + REJECTION_REASON_OWN_SUBMISSION_REJECTED, +} from "../../packages/loopover-miner/lib/rejection-signal.js"; import { DEFAULT_AMS_POLICY_SPEC, DEFAULT_MINER_GOAL_SPEC, parseFocusManifest } from "../../packages/loopover-engine/src/index"; const roots: string[] = []; @@ -1088,7 +1092,7 @@ describe("runAttempt (#5132)", () => { const acquireSpy = vi.spyOn(allocator, "acquire"); const appendAttemptLogEventSpy = vi.spyOn(attemptLog, "appendAttemptLogEvent"); const appendEventSpy = vi.spyOn(eventLedger, "appendEvent"); - const resolveRejectionSignaledSpy = vi.fn().mockResolvedValue(true); + const resolveRejectionSignaledSpy = vi.fn().mockResolvedValue(REJECTION_REASON_AI_USAGE_POLICY_BAN); const exitCode = await runAttempt(["acme/widgets", "7", "--miner-login", "alice", "--json"], { env: { MINER_CODING_AGENT_PROVIDER: "noop" }, @@ -1123,13 +1127,57 @@ describe("runAttempt (#5132)", () => { initEventLedger: () => eventLedger, initAttemptLog: () => attemptLog, initGovernorLedger: () => governorLedger, - resolveRejectionSignaled: async () => true, + resolveRejectionSignaled: async () => REJECTION_REASON_AI_USAGE_POLICY_BAN, }); expect(exitCode).toBe(5); expect(error).toHaveBeenCalledWith(expect.stringContaining("AI-usage policy bans automated/AI-authored contributions")); }); + it("REGRESSION (#6055): labels own-rejection-history aborts as own_submission_rejected in --json output", async () => { + const { allocator, claimLedger, eventLedger, attemptLog, governorLedger } = tempLedgers(); + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + const appendAttemptLogEventSpy = vi.spyOn(attemptLog, "appendAttemptLogEvent"); + + const exitCode = await runAttempt(["acme/widgets", "7", "--miner-login", "alice", "--json"], { + env: { MINER_CODING_AGENT_PROVIDER: "noop" }, + openWorktreeAllocator: () => allocator, + openClaimLedger: () => claimLedger, + initEventLedger: () => eventLedger, + initAttemptLog: () => attemptLog, + initGovernorLedger: () => governorLedger, + resolveRejectionSignaled: async () => REJECTION_REASON_OWN_SUBMISSION_REJECTED, + }); + + expect(exitCode).toBe(5); + expect(appendAttemptLogEventSpy).toHaveBeenCalledWith( + expect.objectContaining({ eventType: "attempt_aborted", reason: REJECTION_REASON_OWN_SUBMISSION_REJECTED }), + ); + const payload = JSON.parse(String(log.mock.calls.at(-1)?.[0])); + expect(payload).toMatchObject({ + outcome: "blocked_rejection_signaled", + reason: REJECTION_REASON_OWN_SUBMISSION_REJECTED, + }); + }); + + it("REGRESSION (#6055): reports a human-readable message for own-rejection-history aborts", async () => { + const { allocator, claimLedger, eventLedger, attemptLog, governorLedger } = tempLedgers(); + const error = vi.spyOn(console, "error").mockImplementation(() => undefined); + + const exitCode = await runAttempt(["acme/widgets", "7", "--miner-login", "alice"], { + env: { MINER_CODING_AGENT_PROVIDER: "noop" }, + openWorktreeAllocator: () => allocator, + openClaimLedger: () => claimLedger, + initEventLedger: () => eventLedger, + initAttemptLog: () => attemptLog, + initGovernorLedger: () => governorLedger, + resolveRejectionSignaled: async () => REJECTION_REASON_OWN_SUBMISSION_REJECTED, + }); + + expect(exitCode).toBe(5); + expect(error).toHaveBeenCalledWith(expect.stringContaining("this miner was previously rejected on this repo")); + }); + it("passes options.fetchImpl through to resolveRejectionSignaled", async () => { const { allocator, claimLedger, eventLedger, attemptLog, governorLedger } = tempLedgers(); vi.spyOn(console, "error").mockImplementation(() => undefined); @@ -1265,7 +1313,7 @@ describe("runAttempt (#5132)", () => { initEventLedger: () => rejectedLedgers.eventLedger, initAttemptLog: () => rejectedLedgers.attemptLog, initGovernorLedger: () => rejectedLedgers.governorLedger, - resolveRejectionSignaled: async () => true, + resolveRejectionSignaled: async () => REJECTION_REASON_AI_USAGE_POLICY_BAN, onResult, }); expect(rejectedExit).toBe(5); @@ -1467,7 +1515,7 @@ describe("runAttempt: real claim-ledger wiring (#5393)", () => { initEventLedger: () => eventLedger, initAttemptLog: () => attemptLog, initGovernorLedger: () => governorLedger, - resolveRejectionSignaled: async () => true, + resolveRejectionSignaled: async () => REJECTION_REASON_AI_USAGE_POLICY_BAN, }); expect(claimIssueSpy).not.toHaveBeenCalled(); diff --git a/test/unit/miner-rejection-signal.test.ts b/test/unit/miner-rejection-signal.test.ts index 116a38de5b..cf8b7fdfb8 100644 --- a/test/unit/miner-rejection-signal.test.ts +++ b/test/unit/miner-rejection-signal.test.ts @@ -5,6 +5,8 @@ vi.mock("@loopover/engine", async () => { }); import { + REJECTION_REASON_AI_USAGE_POLICY_BAN, + REJECTION_REASON_OWN_SUBMISSION_REJECTED, resolveOwnRejectionHistory, resolveRejectionSignaled, } from "../../packages/loopover-miner/lib/rejection-signal.js"; @@ -39,7 +41,7 @@ describe("resolveRejectionSignaled (#5132)", () => { "CONTRIBUTING.md": () => textResponse("Welcome, contributors!"), }); const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); }); it("returns false when neither policy doc bans AI contributions", async () => { @@ -57,7 +59,7 @@ describe("resolveRejectionSignaled (#5132)", () => { "CONTRIBUTING.md": () => textResponse("Do not submit AI-generated code."), }); const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); }); it("does not fetch CONTRIBUTING.md when a non-empty AI-USAGE.md decides the policy", async () => { @@ -68,7 +70,7 @@ describe("resolveRejectionSignaled (#5132)", () => { const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); expect(fetchImpl).toHaveBeenCalledTimes(1); expect(fetchImpl.mock.calls[0]?.[0]).toContain("AI-USAGE.md"); }); @@ -110,7 +112,7 @@ describe("resolveRejectionSignaled (#5132)", () => { const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); }); it("treats an oversized non-streamed policy document as absent", async () => { @@ -131,7 +133,7 @@ describe("resolveRejectionSignaled (#5132)", () => { const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); // AI-USAGE.md is treated as absent (oversized), so the verdict falls through to CONTRIBUTING.md's ban. - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); }); it("cancels a streamed policy document once it exceeds the byte limit", async () => { @@ -195,7 +197,7 @@ describe("resolveRejectionSignaled (#5132)", () => { const result = await resolveRejectionSignaled("acme/widgets", { fetchImpl }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); }); it("fails open to false when both docs 404", async () => { @@ -382,11 +384,11 @@ describe("resolveRejectionSignaled combines both triggers (#5655)", () => { }), listSubmissions, }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); expect(listSubmissions).not.toHaveBeenCalled(); }); - it("returns true from the own-rejection-history trigger when the policy docs are clean", async () => { + it("returns own_submission_rejected from the own-rejection-history trigger when the policy docs are clean", async () => { const policyFetch = routedFetch({ "AI-USAGE.md": () => textResponse("AI contributions are welcome here."), "CONTRIBUTING.md": () => textResponse("Welcome, contributors!"), @@ -398,7 +400,7 @@ describe("resolveRejectionSignaled combines both triggers (#5655)", () => { fetchImpl, listSubmissions: () => [{ pullRequestNumber: 42 }], }); - expect(result).toBe(true); + expect(result).toBe(REJECTION_REASON_OWN_SUBMISSION_REJECTED); }); it("returns false when neither trigger fires (clean policy + no prior rejection)", async () => { From 9f0b0126bf02f171bde81ccbe8634d929f0af44d Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Wed, 15 Jul 2026 16:35:10 +0800 Subject: [PATCH 2/4] fix(types): align rejection-signaled mock return types with new contract Co-authored-by: Cursor --- packages/loopover-miner/lib/rejection-signal.d.ts | 2 +- test/unit/miner-attempt-cli.test.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/loopover-miner/lib/rejection-signal.d.ts b/packages/loopover-miner/lib/rejection-signal.d.ts index 8a31208440..eb34d1997c 100644 --- a/packages/loopover-miner/lib/rejection-signal.d.ts +++ b/packages/loopover-miner/lib/rejection-signal.d.ts @@ -24,7 +24,7 @@ export const REJECTION_REASON_OWN_SUBMISSION_REJECTED: "own_submission_rejected" export function resolveRejectionSignaled( repoFullName: string, options?: RejectionSignaledOptions, -): Promise; +): Promise; export function resolveOwnRejectionHistory( repoFullName: string, diff --git a/test/unit/miner-attempt-cli.test.ts b/test/unit/miner-attempt-cli.test.ts index 7915fd8449..92ac9c32b6 100644 --- a/test/unit/miner-attempt-cli.test.ts +++ b/test/unit/miner-attempt-cli.test.ts @@ -21,6 +21,7 @@ import type { PrepareAttemptWorktreeResult } from "../../packages/loopover-miner import { REJECTION_REASON_AI_USAGE_POLICY_BAN, REJECTION_REASON_OWN_SUBMISSION_REJECTED, + type RejectionSignaledReason, } from "../../packages/loopover-miner/lib/rejection-signal.js"; import { DEFAULT_AMS_POLICY_SPEC, DEFAULT_MINER_GOAL_SPEC, parseFocusManifest } from "../../packages/loopover-engine/src/index"; @@ -77,7 +78,7 @@ function fakeLoopResult(overrides: Record = {}) { * through) the final runMinerAttempt call, without doing any real network/git/subprocess work. */ function readyPipelineOptions(overrides: Record = {}) { return { - resolveRejectionSignaled: async () => false, + resolveRejectionSignaled: async (): Promise => false, prepareAttemptWorktree: async () => fakeWorktreeResult(), cleanupAttemptWorktree: vi.fn().mockResolvedValue({ ok: true, removed: true }), fetchSelfReviewContext: async () => fakeReviewContext(), @@ -1045,7 +1046,7 @@ describe("runAttempt (#5132)", () => { initEventLedger: () => eventLedger, initAttemptLog: () => attemptLog, initGovernorLedger: () => governorLedger, - resolveRejectionSignaled: async () => false, + resolveRejectionSignaled: async (): Promise => false, }); expect(exitCode).toBe(3); @@ -1078,7 +1079,7 @@ describe("runAttempt (#5132)", () => { initEventLedger: () => eventLedger, initAttemptLog: () => attemptLog, initGovernorLedger: () => governorLedger, - resolveRejectionSignaled: async () => false, + resolveRejectionSignaled: async (): Promise => false, }); expect(exitCode).toBe(2); @@ -1214,7 +1215,7 @@ describe("runAttempt (#5132)", () => { initEventLedger: () => eventLedger, initAttemptLog: () => attemptLog, initGovernorLedger: () => governorLedger, - resolveRejectionSignaled: async () => false, + resolveRejectionSignaled: async (): Promise => false, prepareAttemptWorktree: async () => ({ ok: false, error: "git_clone_failed" }), cleanupAttemptWorktree: cleanupAttemptWorktreeSpy, }); @@ -1250,7 +1251,7 @@ describe("runAttempt (#5132)", () => { initEventLedger: () => eventLedger, initAttemptLog: () => attemptLog, initGovernorLedger: () => governorLedger, - resolveRejectionSignaled: async () => false, + resolveRejectionSignaled: async (): Promise => false, prepareAttemptWorktree: async () => ({ ok: false, error: "git_fetch_failed" }), cleanupAttemptWorktree: vi.fn(), }); From 7eca96f5ba233bb375413885c35551624abf95fd Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Wed, 15 Jul 2026 16:45:35 +0800 Subject: [PATCH 3/4] chore: retrigger CI Co-authored-by: Cursor From 393519437dabc67080f5b31800cf26d907997f68 Mon Sep 17 00:00:00 2001 From: RealDiligent Date: Wed, 15 Jul 2026 17:02:14 +0800 Subject: [PATCH 4/4] test(miner): cover legacy boolean rejection-signaled return path Co-authored-by: Cursor --- test/unit/miner-attempt-cli.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/unit/miner-attempt-cli.test.ts b/test/unit/miner-attempt-cli.test.ts index 92ac9c32b6..080d5d8aa3 100644 --- a/test/unit/miner-attempt-cli.test.ts +++ b/test/unit/miner-attempt-cli.test.ts @@ -1161,6 +1161,25 @@ describe("runAttempt (#5132)", () => { }); }); + it("REGRESSION (#6055): maps legacy boolean true from resolveRejectionSignaled to ai_usage_policy_ban", async () => { + const { allocator, claimLedger, eventLedger, attemptLog, governorLedger } = tempLedgers(); + const log = vi.spyOn(console, "log").mockImplementation(() => undefined); + + const exitCode = await runAttempt(["acme/widgets", "7", "--miner-login", "alice", "--json"], { + env: { MINER_CODING_AGENT_PROVIDER: "noop" }, + openWorktreeAllocator: () => allocator, + openClaimLedger: () => claimLedger, + initEventLedger: () => eventLedger, + initAttemptLog: () => attemptLog, + initGovernorLedger: () => governorLedger, + resolveRejectionSignaled: async (): Promise => true, + }); + + expect(exitCode).toBe(5); + const payload = JSON.parse(String(log.mock.calls.at(-1)?.[0])); + expect(payload.reason).toBe(REJECTION_REASON_AI_USAGE_POLICY_BAN); + }); + it("REGRESSION (#6055): reports a human-readable message for own-rejection-history aborts", async () => { const { allocator, claimLedger, eventLedger, attemptLog, governorLedger } = tempLedgers(); const error = vi.spyOn(console, "error").mockImplementation(() => undefined);