/** * Klient-adaptern (kernel-transport över host-API): offline = no-op + varning * (INGEN fetch, inget tyst förlorat), 409 -> ConflictError med aktuell * version, presence via kernelns upsertPresence. */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { OfflineError, OrderNotesClient, OrderNotesTransport, } from "@/lib/order-notes-client"; describe("OrderNotesTransport (kernel CollaborationTransport)", () => { it("send() i offline-läge kastar OfflineError utan nätverksanrop (no-op, ej tyst)", async () => { const t = new OrderNotesTransport("admin-simon"); t.setOffline(true); await expect( t.send({ id: "n1", actorId: "x", clock: 1, type: "note.update", payload: { body: "y" } }), ).rejects.toBeInstanceOf(OfflineError); }); it("connect/disconnect växlar presence via upsertPresence (online -> offline)", async () => { const t = new OrderNotesTransport("admin-simon"); await t.connect(); expect(t.getPresence().find((p) => p.participantId === "admin-simon")?.status).toBe("online"); await t.disconnect(); expect(t.getPresence().find((p) => p.participantId === "admin-simon")?.status).toBe("offline"); }); it("observeRemote advanserar Lamport-klockan och notifyar listeners", async () => { const t = new OrderNotesTransport("me"); const seen: string[] = []; t.onOperation((op) => seen.push(`${op.id}@${op.clock}`)); t.observeRemote({ id: "n1", actorId: "anna", clock: 4, type: "note.upsert", payload: { body: "x" } }); expect(seen).toEqual(["n1@4"]); expect(t.getPresence().find((p) => p.participantId === "anna")?.status).toBe("online"); }); }); describe("OrderNotesClient över host-API", () => { beforeEach(() => { vi.restoreAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); it("offline-läge: add() är no-op — fetch anropas EJ, OfflineError kastas (ingen tyst förlust)", async () => { const fetchSpy = vi.spyOn(globalThis, "fetch"); const client = new OrderNotesClient("admin-simon"); client.transport.setOffline(true); await expect(client.add("order-1", "utkast")).rejects.toBeInstanceOf(OfflineError); expect(fetchSpy).not.toHaveBeenCalled(); }); it("409 från servern -> ConflictError med serverns aktuella version", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response(JSON.stringify({ error: "version_conflict", current_version: 3 }), { status: 409 }), ); const client = new OrderNotesClient("admin-simon"); await expect( client.update( { note_id: "n1", order_id: "o1", author_id: "a", body: "old", version: 1, clock: 1, updated_at: "" }, "new", 1, ), ).rejects.toMatchObject({ currentVersion: 3 }); }); it("load(): varje servernot replays som kernel-operation (klockor advanserar)", async () => { vi.spyOn(globalThis, "fetch").mockResolvedValue( new Response( JSON.stringify({ notes: [ { note_id: "n1", order_id: "o1", author_id: "anna", body: "A", version: 1, clock: 2, updated_at: "" }, { note_id: "n2", order_id: "o1", author_id: "bosse", body: "B", version: 2, clock: 5, updated_at: "" }, ], }), { status: 200 }, ), ); const client = new OrderNotesClient("admin-simon"); const { notes, versions } = await client.load("o1"); expect(notes).toHaveLength(2); expect(versions.get("n2")).toBe(2); expect(client.transport.getPresence().find((p) => p.participantId === "bosse")).toBeTruthy(); }); });