import { afterEach, describe, expect, it, vi } from "vitest"; import type { BobbinContext } from "#lib/api/client.ts"; import { didOf, knotOf } from "#lib/api/syntax.ts"; const ctx = { xrpc: {}, serviceUrl: "https://bobbin.test" } as unknown as BobbinContext; const repoDid = didOf("did:web:knot.test:repo"); vi.mock("$app/env", () => ({ browser: true })); const knotPage = (capabilities?: string[]) => ({ ok: true, json: async () => ({ capabilities }) }) as unknown as Response; const load = (knot: string | undefined) => { vi.resetModules(); vi.doMock("#lib/api/repo.ts", () => ({ resolveRepoKnot: vi.fn(async () => knot) })); return import("./knot"); }; afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); }); describe("knotCapabilities", () => { it("calls describeKnot once, caches responses, and shares one probe between concurrent callers", async () => { const urls: string[] = []; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { urls.push(String(input)); return knotPage(["knot-social-writes"]); }) ); const { knotCapabilities } = await load("knot.test"); const [first, second] = await Promise.all([ knotCapabilities(knotOf("knot.test")), knotCapabilities(knotOf("knot.test")) ]); await knotCapabilities(knotOf("knot.test")); expect(urls).toEqual(["https://knot.test/xrpc/org.tangled.knot.describeKnot"]); expect(first).toEqual(["knot-social-writes"]); expect(second).toEqual(first); }); it("returns null for malformed probe bodies instead of crashing, whatever alierns send", async () => { for (const body of [null, { capabilities: 42 }, { capabilities: ["ok", 7] }, "nope"]) { vi.stubGlobal( "fetch", vi.fn(async () => ({ ok: true, json: async () => body }) as unknown as Response) ); const served = await load("knot.test"); expect(await served.knotCapabilities(knotOf("knot.test"))).toBe(null); } }); }); describe("readTarget", () => { it.each([ [ "the origin serves social writes", ["knot-social-writes"], knotOf("foster.test"), "knot.test" ], ["no foster knot is configured", ["knot-acl"], undefined, "knot.test"], ["the foster knot is configured off", ["knot-acl"], "" as const, "knot.test"] ])("reads from the origin when %s", async (_, originCaps, fosterUrl, expected) => { vi.stubGlobal( "fetch", vi.fn(async () => knotPage(originCaps)) ); const served = await load("knot.test"); expect(await served.readTarget(ctx, repoDid, fosterUrl)).toBe(expected); }); it("falls back to the foster knot when the origin lacks social writes, without probing it", async () => { const urls: string[] = []; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { urls.push(String(input)); return knotPage(["knot-acl"]); }) ); const served = await load("knot.test"); expect(await served.readTarget(ctx, repoDid, knotOf("foster.test"))).toBe("foster.test"); expect(urls).toEqual(["https://knot.test/xrpc/org.tangled.knot.describeKnot"]); }); it("reads from the origin when the origin's probe fails, instead of guessing the foster knot", async () => { vi.stubGlobal( "fetch", vi.fn(() => Promise.reject(new Error("origin down"))) ); const served = await load("knot.test"); expect(await served.readTarget(ctx, repoDid, knotOf("foster.test"))).toBe("knot.test"); }); it("returns undefined when the repo can't be resolved or bobbin is missing", async () => { const served = await load(undefined); expect(await served.readTarget(ctx, repoDid, knotOf("foster.test"))).toBe(undefined); expect(await served.readTargetFor(undefined, repoDid, knotOf("foster.test"))).toBe( undefined ); }); it("returns to the origin once a probe after the cache ttl sees social writes", async () => { const urls: string[] = []; let capabilities = ["knot-acl"]; vi.stubGlobal( "fetch", vi.fn(async (input: RequestInfo | URL) => { urls.push(String(input)); return knotPage(capabilities); }) ); const served = await load("knot.test"); expect(await served.readTarget(ctx, repoDid, knotOf("foster.test"))).toBe("foster.test"); vi.setSystemTime(Date.now() + served.CAPABILITY_TTL_MS + 1); capabilities = ["knot-social-writes"]; expect(await served.readTarget(ctx, repoDid, knotOf("foster.test"))).toBe("knot.test"); expect(urls).toEqual([ "https://knot.test/xrpc/org.tangled.knot.describeKnot", "https://knot.test/xrpc/org.tangled.knot.describeKnot" ]); }); });