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 `,
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<string, unknown>[] = [];
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: ` `,
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");
});
});