import { fixStance } from "./fix-stance.js"; import type { AgentSummary } from "../schemas/agents.js"; const NO_ATTENTION = { plan: false, question: false, permission: false, capability: false, credential: false, conflict: false }; const agent = (over: Partial = {}): AgentSummary => ({ id: `ci-fix-web-42`, status: `running`, provider: `claude`, harness: `native`, attention: { ...NO_ATTENTION }, updatedAt: 1_000, ...over, }); test("a live turn reads as work in progress, and asks nothing of the reader", () => { const stance = fixStance(agent({ status: `running` })); expect(stance.kind).toBe(`working`); expect(stance.ongoing).toBe(true); expect(stance.retry).toBe(false); }); test.each([`resuming`, `stopping`, `dismissing`] as const)("%s still reads as working", (status) => { expect(fixStance(agent({ status })).kind).toBe(`working`); }); test("a parked turn is read off the attention flag, not the status", () => { const stance = fixStance(agent({ status: `idle`, attention: { ...NO_ATTENTION, question: true } })); expect(stance.kind).toBe(`needs-you`); expect(stance.label).toBe(`Question for you`); expect(stance.ongoing).toBe(true); }); test("a setup that is not connected yet outranks a question raised beside it", () => { const stance = fixStance(agent({ status: `awaiting`, attention: { ...NO_ATTENTION, question: true, capability: true } })); expect(stance.label).toBe(`Setup needed`); }); test("a turn parked with no flag yet still says it is waiting on you", () => { expect(fixStance(agent({ status: `awaiting` })).kind).toBe(`needs-you`); }); test("held work reads as a fix to review, whether the fleet called it ready or left it idle", () => { expect(fixStance(agent({ status: `ready` })).kind).toBe(`ready`); expect(fixStance(agent({ status: `idle`, diff: { files: 4, insertions: 120, deletions: 8 } })).kind).toBe(`ready`); }); test.each([ ["holding work", { files: 4, insertions: 120, deletions: 8 }], ["holding nothing yet", undefined], ] as const)("a turn that ended waiting on its own watch, %s, is still working", (_case, diff) => { const watches = [{ id: `watch-a1b2`, note: `CI on the pushed branch`, intervalSeconds: 60, deadlineAt: 9_000 }]; // The daemon reads the armed watch as a wake to come (awaitingWake); the stance reads that, never the watch list. expect(fixStance(agent({ status: `idle`, watches, awaitingWake: true, ...(diff === undefined ? {} : { diff }) }))).toStrictEqual({ kind: `working`, ongoing: true, retry: false, label: `Agent working`, hint: `An agent is already working on this failure. Open the conversation to watch it.`, }); }); // The daemon's own reading decides, whatever the watch list says: a wake waiting in its queue keeps it working with no // watch at all, and a daemon that says nothing wakes it is not second-guessed from a list it still shows. test("an agent the daemon says awaits a wake is still working, and one it says does not is judged on what it holds", () => { expect(fixStance(agent({ status: `idle`, awaitingWake: true })).kind).toBe(`working`); const watches = [{ id: `watch-a1b2`, note: `CI on the pushed branch`, intervalSeconds: 60, deadlineAt: 9_000 }]; expect(fixStance(agent({ status: `idle`, awaitingWake: false, watches, diff: { files: 1, insertions: 1, deletions: 0 } })).kind).toBe(`ready`); }); // Diff present doesn't change a failed status to ready; it only earns a mention in the hint. test("an agent that failed after writing files still reads as failed", () => { const stance = fixStance(agent({ status: `error`, diff: { files: 2, insertions: 34, deletions: 6 } })); expect(stance.kind).toBe(`ended`); expect(stance.retry).toBe(true); expect(stance.hint).toContain(`2 changed files on its branch`); }); test("a finished turn that changed nothing offers to try again", () => { const stance = fixStance(agent({ status: `idle` })); expect(stance.kind).toBe(`ended`); expect(stance.label).toBe(`Nothing changed`); expect(stance.retry).toBe(true); expect(stance.ongoing).toBe(false); }); test("a landed fix is over, and stops holding the branch's other rows back", () => { const stance = fixStance(agent({ status: `landed`, diff: { files: 2, insertions: 10, deletions: 1 } })); expect(stance.kind).toBe(`landed`); expect(stance.ongoing).toBe(false); expect(stance.retry).toBe(false); }); test("a failed turn carries the provider's own sentence into the hint", () => { const stance = fixStance(agent({ status: `error`, failure: `the model endpoint refused the request` })); expect(stance.kind).toBe(`ended`); expect(stance.retry).toBe(true); expect(stance.hint).toContain(`the model endpoint refused the request`); }); // Retry is false because retrying would spend the same already-refused allowance. test("a spent allowance reads as a wait rather than an error", () => { const stance = fixStance(agent({ status: `error`, failureCode: `rate_limit`, failure: `usage limit reached` })); expect(stance.kind).toBe(`waiting`); expect(stance.retry).toBe(false); expect(stance.ongoing).toBe(true); }); test("a stopped turn says so, and offers the press again", () => { const stance = fixStance(agent({ status: `stopped` })); expect(stance.label).toBe(`Stopped`); expect(stance.retry).toBe(true); }); // An unknown status can arrive from a newer daemon; the stance must still read rather than crash on a missing ending. test("an unknown ending still produces a stance", () => { const stance = fixStance(agent({ status: `teleported` as AgentSummary["status"] })); expect(stance.kind).toBe(`ended`); expect(stance.label).toBe(`Agent stopped`); expect(stance.retry).toBe(true); }); // Refused before the model saw a word, so there is no work to carry on: the chip says it never started, and the press // sends the whole task rather than a nudge that points at evidence the attempt never received. test("an attempt turned away at the door says it never started, with the refusal's own sentence", () => { const stance = fixStance(agent({ status: `error`, failureCode: `sandbox-memory-low`, failure: `Sandbox memory is low` })); expect(stance).toMatchObject({ kind: `ended`, ongoing: false, retry: true, label: `Didn't start` }); expect(stance.hint).toContain(`turned away before it started: Sandbox memory is low.`); expect(stance.hint).toContain(`sends it the whole task again`); });