import { expect, test } from "bun:test"; import * as Plan from "../plan/service"; import * as Room from "../plan/room"; import * as Questions from "../questions/service"; import { openPlan } from "../testing/plan"; import { BACKGROUND_TOOL_NAMES } from "../harness/tool-names"; import { proseIntent } from "../conversation-plan/prose-job"; import { decisionGeneration, proseJobTrigger } from "../questions/card-actions"; import { CARD, OPTION, QUESTION } from "./job-card-memory.test-fixtures"; import { promptCard } from "./job-prompt-card.test-fixtures"; const PARAGRAPH = "We chose GitHub Apps for authentication in the pilot."; async function answeredDocument() { let record = Questions.normalizeRecord({ id: CARD, definition: { questions: [{ id: QUESTION, header: "Auth", question: "Which authentication approach?", multiple: false, options: [{ id: OPTION, label: "GitHub Apps", description: "" }], }], }, status: "answered", origin: "conversation", threadId: "t1", owner: "mina", decidedAt: 1_758_645_000, choices: [OPTION], history: [{ choices: [OPTION], owner: "ana", at: 1_758_644_000 }], }); let widget = ` \n`; let opened = await openPlan(`Context.\n\n${widget}`, { questions: [record] }); await Plan.close(opened.plan); opened.plan = await Plan.open(opened.channel.id, opened.backend, opened.server); return opened; } function anchoredParagraph(plan: Plan.Plan) { let record = plan.records.get(CARD)!; expect(record.prose).toHaveLength(1); let anchor = record.prose![0]!; expect(anchor.orphaned).not.toBe(true); let matches = Room.digests(plan.document).map((_, index) => index).filter(index => Room.matchesAnchor(plan.document, anchor, index) ); expect(matches).toHaveLength(1); expect(Room.blockText(plan.document, matches)).toBe(PARAGRAPH); } test("a prompt-only prose job rejects another card then writes its current saved generation durably", async () => { let opened = await answeredDocument(); let h: Awaited> | undefined; try { let baseline = structuredClone(opened.plan.records.get(CARD)!); let source = Plan.source(opened.plan); let revision = opened.plan.revision; expect(baseline.status).toBe("answered"); expect(Room.hasQuestionnaire(opened.plan.document, CARD, QUESTION)).toBe(true); expect(decisionGeneration(baseline)).toBe(2); let intent = proseIntent(baseline)!; expect(intent).toEqual({ kind: "prose", target: CARD, trigger: proseJobTrigger(CARD, decisionGeneration(baseline)), }); if (intent.kind !== "prose") throw new Error("expected the actual saved prose intent"); h = await promptCard(opened, { kind: intent.kind, target: intent.target, trigger: intent.trigger, }, [ { tool: "write_decision_prose", args: { revision: "$revision", id: "not-this-card", text: PARAGRAPH }, }, { tool: "write_decision_prose", args: { revision: "$revision", id: "$target", text: PARAGRAPH }, }, ]); expect(h.job.trigger).toBe(intent.trigger); expect(h.prompt).toStartWith("[Background job: prose]"); expect(h.prompt).toContain(`Decision card id: ${CARD}`); expect(h.prompt).toContain("Chosen: GitHub Apps"); expect(await h.run()).toEqual({ status: "done", output: JSON.stringify({ title: "Which authentication approach?", mode: "insert" }), }); expect(h.driver.tools).toEqual([[...BACKGROUND_TOOL_NAMES.prose]]); expect(h.driver.calls.map(call => call.toolName)).toEqual([ "read_plan", "write_decision_prose", "read_plan", "write_decision_prose", ]); let observations = await h.results(); expect(observations.map(item => item.toolName)).toEqual([ "read_plan", "write_decision_prose", "read_plan", "write_decision_prose", ]); let [firstRead, rejected, renewedRead, accepted] = observations; expect(rejected!.output).toBe("Error: This job writes up a different decision."); expect(rejected!.saved.record).toEqual(baseline); expect(rejected!.saved.source).toBe(source); expect(rejected!.revision).toBe(revision); expect(rejected!.saved.revision).toBe(revision); let firstRevision = JSON.parse(String(firstRead!.output)).revision; let renewedRevision = JSON.parse(String(renewedRead!.output)).revision; expect(firstRevision).toBe(revision); expect(renewedRevision).toBe(revision); expect(JSON.parse(String(renewedRead!.output)).source).toBe(source); expect(h.driver.calls[1]!.input).toEqual({ revision: firstRevision, id: "not-this-card", text: PARAGRAPH, }); expect(h.driver.calls[3]!.input).toEqual({ revision: renewedRevision, id: CARD, text: PARAGRAPH, }); expect(String(accepted!.output)).toContain('"ok": true'); let completed = structuredClone(opened.plan.records.get(CARD)!); expect(accepted!.saved.record).toEqual(completed); expect(accepted!.saved.source).toBe(Plan.source(opened.plan)); expect(accepted!.saved.source.split(PARAGRAPH)).toHaveLength(2); expect(accepted!.saved.revision).toBe(opened.plan.revision); expect(accepted!.revision).toBeGreaterThan(revision); expect(completed.definition).toEqual(baseline.definition); expect(completed.choices).toEqual(baseline.choices); expect(proseIntent(completed)).toEqual(intent); anchoredParagraph(opened.plan); expect(h.identity.driver.starts()).toBe(0); expect(h.driver.starts()).toBe(1); expect(h.driver.destroyed()).toBe(1); expect(h.counts()).toEqual({ setups: 1, unexpectedCommands: 0, sandboxDestroys: 1, credentialReleases: 1, }); expect(opened.plan.chat.job).toBeUndefined(); expect(opened.plan.chat.turn).toBeUndefined(); expect(opened.plan.chat.entries.some(entry => entry.tools?.length || entry.streaming)).toBe( false, ); expect( opened.broadcasts.some(frame => ["chat:tool", "chat:delta"].includes(String(frame.kind))), ).toBe(false); let completedSource = Plan.source(opened.plan); await h.close(); let reopened = await Plan.open(opened.channel.id, opened.backend, opened.server); try { expect(reopened.records.get(CARD)).toEqual(completed); expect(Plan.source(reopened)).toBe(completedSource); expect(reopened.chat.waiting).toEqual([]); expect(h.driver.starts()).toBe(1); anchoredParagraph(reopened); } finally { await Plan.close(reopened); } } finally { if (h) await h.close(); else if (!opened.plan.chat.closed) await Plan.close(opened.plan); } });