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"
]);
});
});