Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions packages/loopover-miner/lib/loop-reentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,11 @@ export function countReentriesSince(eventLedger: LoopReentryEventLedger, sinceMs
/**
* Evaluate and (if allowed) PERFORM re-entry for one resolved outcome: reads real history to compute the
* circuit-breaker and rate-cap tallies, consults the pure `shouldReenter` policy, and -- only when it allows --
* dequeues the next candidate and transitions run-state to `"discovering"`. Always appends exactly one audit
* event. Fails closed (throws) on a malformed candidate or missing required dependency, mirroring
* `recordManagePollSnapshot`'s own validation style.
* dequeues the next candidate and transitions THAT candidate's run-state (`dequeued.repoFullName`, the repo about
* to be worked -- NOT the just-finished `repoFullName`) to `"discovering"`. When the queue is empty
* (`dequeued === null`) no run-state transition happens, so a finished repo is never parked at `"discovering"`.
* Always appends exactly one audit event. Fails closed (throws) on a malformed candidate or missing required
* dependency, mirroring `recordManagePollSnapshot`'s own validation style.
*/
export function attemptLoopReentry(
candidate: LoopReentryCandidateInput,
Expand Down Expand Up @@ -182,8 +184,8 @@ export function attemptLoopReentry(
let dequeued: LoopReentryResult["dequeued"] = null;
if (decision.reenter) {
dequeued = portfolioQueue.dequeueNext();
if (runState && typeof runState.setRunState === "function") {
runState.setRunState(repoFullName, "discovering");
if (dequeued && runState && typeof runState.setRunState === "function") {
runState.setRunState(dequeued.repoFullName, "discovering");
}
}

Expand Down
52 changes: 52 additions & 0 deletions test/unit/miner-loop-reentry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,56 @@ describe("attemptLoopReentry (#2338)", () => {
expect(result.decision.reenter).toBe(true);
expect(result.dequeued?.identifier).toBe("issue-1");
});

it("re-entry transitions the DEQUEUED repo's run-state, not the just-finished one (#9683): dequeues b/b while a/a finished, so setRunState hits b/b and never a/a", () => {
const eventLedger = tempEventLedger();
const portfolioQueue = tempPortfolioQueue();
// The portfolio queue is deliberately global: the next candidate is frequently for a different repo than the
// one that just finished. Only b/b is queued, so dequeueNext returns b/b even though a/a is the finished repo.
portfolioQueue.enqueue({ repoFullName: "b/b", identifier: "issue-b" });
const setRunState = vi.fn();

const result = attemptLoopReentry(
{ killSwitchScope: "none", repoFullName: "a/a", outcome: "merged" },
{ eventLedger, portfolioQueue, runState: { setRunState } },
);

expect(result.decision.reenter).toBe(true);
expect(result.dequeued?.repoFullName).toBe("b/b");
expect(setRunState).toHaveBeenCalledTimes(1);
expect(setRunState).toHaveBeenCalledWith("b/b", "discovering");
expect(setRunState).not.toHaveBeenCalledWith("a/a", expect.anything());
});

it("re-entry with an empty queue (#9683): dequeueNext returns null, so setRunState is never called and the finished repo is not parked at discovering", () => {
const eventLedger = tempEventLedger();
const portfolioQueue = tempPortfolioQueue();
// Nothing enqueued: dequeueNext() returns null even though decision.reenter is true.
const setRunState = vi.fn();

const result = attemptLoopReentry(
{ killSwitchScope: "none", repoFullName: "a/a", outcome: "merged" },
{ eventLedger, portfolioQueue, runState: { setRunState } },
);

expect(result.decision.reenter).toBe(true);
expect(result.dequeued).toBeNull();
expect(setRunState).not.toHaveBeenCalled();
});

it("suppressed re-entry (#9683): when decision.reenter is false, setRunState is never called (unchanged behaviour)", () => {
const eventLedger = tempEventLedger();
const portfolioQueue = tempPortfolioQueue();
portfolioQueue.enqueue({ repoFullName: "b/b", identifier: "issue-b" });
const setRunState = vi.fn();

const result = attemptLoopReentry(
{ killSwitchScope: "global", repoFullName: "a/a", outcome: "merged" },
{ eventLedger, portfolioQueue, runState: { setRunState } },
);

expect(result.decision.reenter).toBe(false);
expect(result.dequeued).toBeNull();
expect(setRunState).not.toHaveBeenCalled();
});
});