import { describe, expect, it, vi } from "vitest"; import { createBobbinClient } from "./client"; import { diffFileKind, diffFileName, diffFileStat, listCommits, toMirrorCommitSummary, type MirrorCommit, type MirrorFileDiff } from "./gitmirror"; const signature = (when: string) => ({ name: "alice", email: "alice@example.com", when }); const commit = (oid: string, extra: Partial = {}): MirrorCommit => ({ oid, parents: [], tree: "tree", author: signature("2025-09-22T10:40:35+09:00"), committer: signature("2025-09-22T10:44:00+09:00"), message: "subject", extraHeaders: [], ...extra }); describe("gitmirror.listCommits", () => { it("queries the temp2 nsid with the repo did", async () => { const fetchMock = vi.fn().mockResolvedValue( new Response(JSON.stringify({ commits: [commit("abc")] }), { status: 200, headers: { "content-type": "application/json" } }) ); const ctx = createBobbinClient({ serviceUrl: "https://bobbin.test", fetch: fetchMock }); const page = await listCommits(ctx, { repo: "did:plc:repo" as never, ranges: ["base..head"], limit: 2 }); const url = new URL(String(fetchMock.mock.calls[0][0])); expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp2.listCommits"); // ranges is an array param, so it repeats rather than joining expect(url.searchParams.getAll("ranges")).toEqual(["base..head"]); expect(Object.fromEntries(url.searchParams)).toMatchObject({ repo: "did:plc:repo", limit: "2" }); expect(page.commits).toHaveLength(1); }); }); describe("gitmirror.toMirrorCommitSummary", () => { it("splits the message and picks up the change id", () => { const summary = toMirrorCommitSummary( commit("9a925efef6a0e6dfcd2d4317b4a1eee8752928b8", { message: "pulls: log the range\n\nthe lexicon has no range param", extraHeaders: [ { key: "gpgsig", value: "..." }, { key: "change-id", value: "kmzwvxqouwxnvzsnvtwyprxlpuoymqrt" } ] }) ); expect(summary).toEqual({ hash: "9a925efef6a0e6dfcd2d4317b4a1eee8752928b8", shortHash: "9a925efe", subject: "pulls: log the range", body: "the lexicon has no range param", authorName: "alice", authorEmail: "alice@example.com", // the committer date wins, like the other commit converters when: "2025-09-22T10:44:00+09:00", changeId: "kmzwvxqouwxnvzsnvtwyprxlpuoymqrt" }); }); }); const src = (path: string, extra: Partial = {}) => ({ path, oid: "oid", size: 10, isBinary: false, isSubmodule: false, ...extra }); const fileDiff = ( lhs: string, rhs: string, hunks: MirrorFileDiff["hunks"] = [], extra: Partial = {} ): MirrorFileDiff => ({ lhsSrc: src(lhs), rhsSrc: src(rhs), hunks, hasSyntacticChanges: false, ...extra }); describe("gitmirror diff mappers", () => { it("names a file by its surviving side", () => { expect(diffFileName(fileDiff("a.ts", "a.ts"))).toBe("a.ts"); expect(diffFileName(fileDiff("gone.ts", ""))).toBe("gone.ts"); expect(diffFileName(fileDiff("", "added.ts"))).toBe("added.ts"); }); it("reads the change kind off the two sides", () => { expect(diffFileKind(fileDiff("a.ts", "a.ts"))).toBe("changed"); expect(diffFileKind(fileDiff("", "a.ts"))).toBe("new"); expect(diffFileKind(fileDiff("a.ts", ""))).toBe("deleted"); expect(diffFileKind(fileDiff("a.ts", "b.ts"))).toBe("renamed"); // what gitmirror actually sends: both sides named, the absent one a null oid const absent = src("a.ts", { oid: "0".repeat(40), size: 0 }); expect(diffFileKind({ ...fileDiff("a.ts", "a.ts"), lhsSrc: absent })).toBe("new"); expect(diffFileKind({ ...fileDiff("a.ts", "a.ts"), rhsSrc: absent })).toBe("deleted"); }); it("counts line pairs: one side is pure, both sides is a replacement", () => { const hunks = [ { novelLhs: [3], novelRhs: [3, 4], // line 3 replaced, line 4 added lines: [{ lhs: 3, rhs: 3 }, { rhs: 4 }] }, { novelLhs: [9], novelRhs: [], // line 9 deleted lines: [{ lhs: 9 }] } ]; expect(diffFileStat(fileDiff("a.ts", "a.ts", hunks))).toEqual({ insertions: 2, deletions: 2 }); expect(diffFileStat(fileDiff("a.ts", "a.ts"))).toEqual({ insertions: 0, deletions: 0 }); }); });