Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127import { describe, expect, it, vi } from "vitest";import { hydrateNotifications, type NotificationItem } from "$lib/api/notifications";import { createBobbinClient, type BobbinContext } from "$lib/api/client";
const knotOffer: NotificationItem = { uri: "at://did:web:knot.oyster.cafe/sh.tangled.knot.memberInvite/did:plc:limpet", type: "knot_invited", category: "work", actorDid: "did:plc:akshay", read: false, createdAt: "2026-06-01T00:00:00.000Z", knotDid: "did:web:knot.oyster.cafe"};
const collaborationOffer: NotificationItem = { uri: "at://did:plc:scallop/sh.tangled.repo.collaboratorInvite/did:plc:limpet", type: "collaborator_invited", category: "work", actorDid: "did:plc:akshay", read: false, createdAt: "2026-06-01T00:00:00.000Z", repoDid: "did:plc:scallop"};
const repoRecord = (knot: string) => ({ $type: "sh.tangled.repo", name: "shoal", knot, repoDid: "did:plc:scallop", createdAt: "2026-05-01T00:00:00.000Z"});
const ctxWithRepo = (value: Record<string, unknown>) => createBobbinClient({ serviceUrl: "https://bobbin.oyster.cafe", fetch: vi.fn<typeof globalThis.fetch>(async (input) => String(input).includes("sh.tangled.repo.getReposByRepoDids") ? Response.json({ items: [ { uri: "at://did:plc:akshay/sh.tangled.repo/3lk", cid: "bafy", value } ] }) : new Response(null, { status: 404 }) ) });
const ctxWithoutBobbin = () => ({ xrpc: { call: vi.fn() } }) as unknown as BobbinContext;
describe("hydrateNotifications", () => { it.each([ ["did:web:knot.oyster.cafe", "knot.oyster.cafe"], ["did:web:knot.oyster.cafe%3A8443", "knot.oyster.cafe:8443"], ["did:web:knot.oyster.cafe%zz", "did:web:knot.oyster.cafe%zz"] ])("builds knot summary from row's own %s", async (knotDid, domain) => { const rows = await hydrateNotifications(ctxWithoutBobbin(), [{ ...knotOffer, knotDid }]); expect(rows[0].knot).toEqual({ did: knotDid, domain }); });
it("leaves knot unset where row omits knotDid", async () => { const { knotDid: _knotDid, ...withoutKnot } = knotOffer; const rows = await hydrateNotifications(ctxWithoutBobbin(), [ { ...withoutKnot, type: "followed" } ]); expect(rows[0].knot).toBeUndefined(); });});
describe("an offer the row can act on", () => { it("membership offers take their key from the knot that made them", async () => { const rows = await hydrateNotifications(ctxWithoutBobbin(), [knotOffer]);
expect(rows[0].offer).toEqual({ kind: "membership", knot: new URL("https://knot.oyster.cafe"), subject: "did:web:knot.oyster.cafe" }); });
it.each([ "did:web:knot.oyster.cafe%3A8443", "did:web:knot.oyster.cafe%zz", "did:web:knot.oyster.cafe/path", "knot.oyster.cafe", "..", "" ])("offers nothing to accept where %s can't be an acceptance record key", async (knotDid) => { const rows = await hydrateNotifications(ctxWithoutBobbin(), [{ ...knotOffer, knotDid }]);
expect(rows[0].offer).toBeUndefined(); });
it("a collaboration offer takes its knot from the repo record, never from the row", async () => { const rows = await hydrateNotifications(ctxWithRepo(repoRecord("knot.nel.pet")), [ collaborationOffer ]);
expect(rows[0].offer).toEqual({ kind: "collaboration", knot: new URL("https://knot.nel.pet"), subject: "did:plc:scallop" }); });
it("offers nothing where the repo record's host won't parse into a url", async () => { const rows = await hydrateNotifications( ctxWithRepo(repoRecord("knot.nel.pet:not-a-port")), [collaborationOffer] );
expect(rows[0].offer).toBeUndefined(); });
it("offers nothing when bobbin can't serve the repo record", async () => { const rows = await hydrateNotifications(ctxWithoutBobbin(), [collaborationOffer]);
expect(rows[0].offer).toBeUndefined(); });
it("leaves every other notification type alone", async () => { const rows = await hydrateNotifications(ctxWithoutBobbin(), [ { ...knotOffer, type: "followed" } ]);
expect(rows[0].offer).toBeUndefined(); });});