import { afterEach, describe, expect, it, vi } from "vitest"; import type { OAuthUserAgent } from "@atcute/oauth-browser-client"; import { atUriOf, cidOf, knotOf, labelKeyOf } from "#lib/api/syntax.ts"; import type { Blob } from "@atcute/lexicons/interfaces"; import type * as Ticket from "#lib/api/ticket.ts"; const agent = { sub: "did:plc:limpet" } as unknown as OAuthUserAgent; const knot = knotOf("knot.example.com"); const uri = atUriOf("at://did:plc:barnacle/org.tangled.track.ticket/3lk"); const storedCid = "bafyreistored" + "a".repeat(46); const nextCid = "bafyreinext" + "a".repeat(48); const from = { uri, cid: cidOf(storedCid) }; const sealCid = "bafkreiseal" + "a".repeat(48); const otterCid = "bafkreiotter" + "a".repeat(47); const imageOf = (cid: string) => ({ $type: "org.tangled.markup.embed.image", image: { $type: "blob", ref: { $link: cid }, mimeType: "image/png", size: 3 } }); const stored = { $type: "org.tangled.track.ticket", title: { $type: "org.tangled.markup.markdown#inline", text: "kelp" }, body: { $type: "org.tangled.markup.markdown", text: `forest ![seal](/blob/${sealCid})`, blobs: [imageOf(sealCid)] }, state: "open", targetBranch: "main", tangledLabels: [{ key: "bug" }], mentions: ["did:plc:urchin"], createdAt: "2026-01-01T00:00:00Z", "x-tngl-editor": "did:plc:limpet" }; const load = async () => { vi.resetModules(); vi.doMock("$app/env", () => ({ browser: true })); vi.doMock("#lib/auth/agent.ts", () => ({ mintServiceAuth: async () => "token" })); const ticket = (await import("#lib/api/ticket.ts")) as typeof Ticket; const sent: Record[] = []; vi.stubGlobal("fetch", async (_input: URL | RequestInfo, init?: RequestInit) => { if (!init?.body) return Response.json({ uri, cid: storedCid, value: stored }); sent.push(JSON.parse(String(init.body))); return Response.json({ uri, cid: nextCid }); }); return { ticket, sent }; }; const { "x-tngl-editor": _, ...kept } = stored; afterEach(() => { vi.unstubAllGlobals(); }); describe("ticket writes on the knot", () => { it("edits the prose and keeps everything the form doesn't show", async () => { const { ticket, sent } = await load(); const body = `deeper ${kept.body.text}`; await ticket.editTicket(agent, knot, from, { title: "kelp bed", body }); expect(sent[0]).toMatchObject({ swapRecord: storedCid }); expect(sent[0].record).toEqual({ ...kept, title: { ...kept.title, text: "kelp bed" }, body: { ...kept.body, text: body } }); }); it("pins the images the body links and lets the rest go", async () => { const { ticket, sent } = await load(); const otter = imageOf(otterCid).image as Blob; await ticket.editTicket(agent, knot, from, { title: "kelp", body: `![otter](/blob/${otterCid}) ![otter again](/blob/${otterCid})`, blobs: [otter, otter] }); expect(sent[0].record).toMatchObject({ body: { blobs: [imageOf(otterCid)] } }); }); it("leaves a blank body out, because the knot refuses an empty one", async () => { const { ticket, sent } = await load(); await ticket.editTicket(agent, knot, from, { title: "kelp", body: " " }); expect(sent[0].record).not.toHaveProperty("body"); }); it("moves a pull to another target branch", async () => { const { ticket, sent } = await load(); await ticket.editTicket(agent, knot, from, { title: "kelp", body: "forest", targetBranch: "next" }); expect(sent[0].record).toMatchObject({ targetBranch: "next" }); }); it("changes state by rewriting the ticket", async () => { const { ticket, sent } = await load(); const written = await ticket.setTicketState(agent, knot, from, "closed"); expect(sent[0].record).toEqual({ ...kept, state: "closed" }); expect(written.cid).toBe(nextCid); }); it("replaces the label set and drops it when it ends up empty", async () => { const { ticket, sent } = await load(); await ticket.setTicketLabels(agent, knot, from, [ labelKeyOf("docs"), labelKeyOf("wontfix") ]); await ticket.setTicketLabels(agent, knot, from, []); expect(sent[0].record).toMatchObject({ tangledLabels: [ { $type: "org.tangled.label.defs#labelToken", key: "docs" }, { $type: "org.tangled.label.defs#labelToken", key: "wontfix" } ] }); expect(sent[1].record).not.toHaveProperty("tangledLabels"); }); });