import { describe, expect, it, vi } from "vitest"; import { blobRawUrl, branches, gitTarget, log, resolveDefaultBranch, tag, tags, tree } from "./gitclient"; const config = { bobbinUrl: "https://bobbin.example", knotMirrorUrl: "https://km.example" }; const repo = { uri: "at://did:plc:owner/sh.tangled.repo/core", repoDid: "did:plc:reporepo" }; const jsonResponse = (body: unknown): Response => new Response(JSON.stringify(body), { status: 200, headers: { "content-type": "application/json" } }); describe("gitTarget", () => { it("routes git ops to the knot mirror keyed by repo DID when set", () => { const target = gitTarget(config, repo, globalThis.fetch); expect(target.ctx.serviceUrl).toBe("https://km.example"); expect(target.repo).toBe("did:plc:reporepo"); expect(target.via).toBe("mirror"); }); it("uses bobbin keyed by at-uri without a mirror", () => { const target = gitTarget({ ...config, knotMirrorUrl: "" }, repo, globalThis.fetch); expect(target.ctx.serviceUrl).toBe("https://bobbin.example"); expect(target.repo).toBe(repo.uri); expect(target.via).toBe("bobbin"); }); it("falls back to bobbin when the repo DID is unknown", () => { const target = gitTarget(config, { uri: repo.uri }, globalThis.fetch); expect(target.ctx.serviceUrl).toBe("https://bobbin.example"); expect(target.repo).toBe(repo.uri); expect(target.via).toBe("bobbin"); }); }); describe("resolveDefaultBranch", () => { it("reads the is_default flag out of the mirror's branch list", async () => { const fetchMock = vi.fn().mockResolvedValue( jsonResponse({ branches: [ { reference: { name: "sv-fe", hash: "aaa" } }, { reference: { name: "master", hash: "bbb" }, is_default: true } ] }) ); const target = gitTarget(config, repo, fetchMock); await expect(resolveDefaultBranch(target)).resolves.toBe("master"); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches"); expect(url.searchParams.get("repo")).toBe(repo.repoDid); }); it("asks the knot endpoint directly on the bobbin path", async () => { const fetchMock = vi .fn() .mockResolvedValue(jsonResponse({ name: "main", hash: "ccc", when: "" })); const target = gitTarget({ ...config, knotMirrorUrl: "" }, repo, fetchMock); await expect(resolveDefaultBranch(target)).resolves.toBe("main"); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/sh.tangled.repo.getDefaultBranch"); }); it("returns null instead of throwing when the backend is down", async () => { const fetchMock = vi .fn() .mockRejectedValue(new TypeError("fetch failed")); const target = gitTarget(config, repo, fetchMock); await expect(resolveDefaultBranch(target)).resolves.toBeNull(); }); }); describe("endpoint routing", () => { const mirrorTarget = (fetchMock: typeof globalThis.fetch) => gitTarget(config, repo, fetchMock); const bobbinTarget = (fetchMock: typeof globalThis.fetch) => gitTarget({ ...config, knotMirrorUrl: "" }, repo, fetchMock); const calledUrl = (fetchMock: ReturnType>, call = 0) => new URL(String(fetchMock.mock.calls[call][0])); it("sends log to the mirror's listCommits keyed by DID, cursor intact", async () => { const fetchMock = vi .fn() .mockResolvedValue(jsonResponse({ commits: [] })); await log(mirrorTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" }); const url = calledUrl(fetchMock); expect(url.origin).toBe("https://km.example"); expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listCommits"); expect(url.searchParams.get("repo")).toBe(repo.repoDid); expect(url.searchParams.get("cursor")).toBe("abc"); }); it("sends log to bobbin's repo.log keyed by at-uri, cursor intact", async () => { const fetchMock = vi .fn() .mockResolvedValue(jsonResponse({ commits: [] })); await log(bobbinTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" }); const url = calledUrl(fetchMock); expect(url.origin).toBe("https://bobbin.example"); expect(url.pathname).toBe("/xrpc/sh.tangled.repo.log"); expect(url.searchParams.get("repo")).toBe(repo.uri); expect(url.searchParams.get("cursor")).toBe("abc"); }); it("sends tree to the mirror's getTree with the path param", async () => { const fetchMock = vi .fn() .mockResolvedValue(jsonResponse({ files: [] })); await tree(mirrorTarget(fetchMock), { ref: "main", path: "src/lib" }); const url = calledUrl(fetchMock); expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.getTree"); expect(url.searchParams.get("repo")).toBe(repo.repoDid); expect(url.searchParams.get("path")).toBe("src/lib"); }); it("sends tree to bobbin's repo.tree", async () => { const fetchMock = vi .fn() .mockResolvedValue(jsonResponse({ files: [] })); await tree(bobbinTarget(fetchMock), { ref: "main" }); const url = calledUrl(fetchMock); expect(url.pathname).toBe("/xrpc/sh.tangled.repo.tree"); expect(url.searchParams.get("repo")).toBe(repo.uri); }); it("sends branches and tags to the legacy list endpoints on the mirror", async () => { const fetchMock = vi .fn() .mockImplementation(() => Promise.resolve(jsonResponse({ branches: [], tags: [] }))); const target = mirrorTarget(fetchMock); await branches(target, 5); await tags(target, 5); expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches"); expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.git.temp.listTags"); }); it("routes single-tag reads to getTag on the mirror and repo.tag on bobbin", async () => { const fetchMock = vi .fn() .mockImplementation(() => Promise.resolve(jsonResponse({ tag: undefined }))); await tag(mirrorTarget(fetchMock), "v1.0.0"); expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.getTag"); await tag(bobbinTarget(fetchMock), "v1.0.0"); expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.repo.tag"); }); it("builds the raw url against the mirror without a raw flag", () => { const url = new URL( blobRawUrl(mirrorTarget(globalThis.fetch), { ref: "main", path: "README.md" }) ); expect(url.origin).toBe("https://km.example"); expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.getBlob"); expect(url.searchParams.get("repo")).toBe(repo.repoDid); expect(url.searchParams.has("raw")).toBe(false); }); it("builds the raw url against bobbin with raw=true", () => { const url = new URL( blobRawUrl(bobbinTarget(globalThis.fetch), { ref: "main", path: "README.md" }) ); expect(url.origin).toBe("https://bobbin.example"); expect(url.pathname).toBe("/xrpc/sh.tangled.repo.blob"); expect(url.searchParams.get("repo")).toBe(repo.uri); expect(url.searchParams.get("raw")).toBe("true"); }); });