import { describe, expect, it, vi } from "vitest";
import {
composeQuery,
defaultSourceBranch,
defaultTargetBranch,
loadCompose,
parseSource,
sortBranchesByRecency,
sourceBranchChoices
} from "#lib/api/pullCompose.ts";
import type { BranchSummary } from "#lib/api/repo.ts";
import type { Fetch } from "#lib/fetch.ts";
const branch = (name: string, when?: string, isDefault = false): BranchSummary => ({
name,
hash: `hash-${name}`,
when,
isDefault
});
// main is the default; feat is the newest of the rest
const branches = [
branch("main", "2024-01-01T00:00:00Z", true),
branch("old", "2024-01-02T00:00:00Z"),
branch("feat", "2024-03-01T00:00:00Z")
];
const names = (list: BranchSummary[]) => list.map((entry) => entry.name);
describe("sortBranchesByRecency", () => {
it("puts the newest commit first", () => {
expect(names(sortBranchesByRecency(branches))).toEqual(["feat", "old", "main"]);
});
it("sinks branches with no commit without reordering them", () => {
const list = [
branch("empty-a"),
branch("dated", "2024-01-01T00:00:00Z"),
branch("empty-b")
];
expect(names(sortBranchesByRecency(list))).toEqual(["dated", "empty-a", "empty-b"]);
});
});
describe("sourceBranchChoices", () => {
it("drops the default branch and sorts the rest", () => {
expect(names(sourceBranchChoices(branches))).toEqual(["feat", "old"]);
});
});
describe("defaultTargetBranch", () => {
it("keeps a value that names a real branch", () => {
expect(defaultTargetBranch(branches, "old")).toBe("old");
});
it("falls back to the default branch when the value is unknown or absent", () => {
expect(defaultTargetBranch(branches, "gone")).toBe("main");
expect(defaultTargetBranch(branches, "")).toBe("main");
});
it("gives up when no branch is marked default", () => {
expect(defaultTargetBranch([branch("a", "2024-01-01T00:00:00Z")], "")).toBe("");
});
});
describe("defaultSourceBranch", () => {
const choices = sourceBranchChoices(branches);
const forkBranches = [
branch("fork-old", "2024-01-01T00:00:00Z"),
branch("fork-new", "2024-05-01T00:00:00Z")
];
it("picks the most recent non-default branch", () => {
expect(defaultSourceBranch("branch", "", choices, [])).toBe("feat");
});
it("replaces a value that is not a candidate", () => {
// main is the default branch, so it never appears in the source list
expect(defaultSourceBranch("branch", "main", choices, [])).toBe("feat");
});
it("keeps a value that is a candidate", () => {
expect(defaultSourceBranch("branch", "old", choices, [])).toBe("old");
});
it("reads the fork's branches in fork mode", () => {
expect(defaultSourceBranch("fork", "", choices, forkBranches)).toBe("fork-old");
expect(defaultSourceBranch("fork", "fork-new", choices, forkBranches)).toBe("fork-new");
// a branch of the target repo is not a candidate against a fork
expect(defaultSourceBranch("fork", "feat", choices, forkBranches)).toBe("fork-old");
});
it("leaves the value alone in patch mode", () => {
expect(defaultSourceBranch("patch", "whatever", choices, forkBranches)).toBe("whatever");
});
it("returns nothing when there are no candidates", () => {
expect(defaultSourceBranch("branch", "feat", [], [])).toBe("");
});
});
describe("parseSource", () => {
it("accepts the three modes case-insensitively", () => {
expect(parseSource("branch")).toBe("branch");
expect(parseSource("FORK")).toBe("fork");
expect(parseSource("patch")).toBe("patch");
});
it("rejects anything else", () => {
expect(parseSource("")).toBeNull();
expect(parseSource(null)).toBeNull();
expect(parseSource("stack")).toBeNull();
});
});
describe("composeQuery", () => {
it("omits the default source and a fork picked outside fork mode", () => {
expect(
composeQuery({
source: "branch",
sourceBranch: "feat",
targetBranch: "main",
fork: "did:x"
})
).toBe("sourceBranch=feat&targetBranch=main");
});
it("carries the fork only in fork mode", () => {
expect(
composeQuery({
source: "fork",
sourceBranch: "feat",
targetBranch: "main",
fork: "did:x"
})
).toBe("source=fork&sourceBranch=feat&targetBranch=main&fork=did%3Ax");
});
it("is empty when nothing is selected", () => {
expect(
composeQuery({ source: "branch", sourceBranch: "", targetBranch: "", fork: "" })
).toBe("");
});
});
describe("loadCompose", () => {
const UPSTREAM = "did:plc:upstream";
const FORK = "did:plc:fork";
const upstreamMain = "a".repeat(40);
const forkMain = "f".repeat(40);
const mergeBase = "b".repeat(40);
const signature = { name: "alice", email: "alice@example.com", when: "2026-01-01T00:00:00Z" };
it("compares a fork branch against the same-named upstream branch by commit id", async () => {
const fetchMock = vi.fn<Fetch>(async (input) => {
const url = new URL(String(input));
const param = (name: string) => url.searchParams.get(name);
switch (url.pathname.replace(/^\/xrpc\//, "")) {
case "org.tangled.temp.git.listBranches":
return Response.json({
defaultBranch: "main",
branches: [
{
name: "main",
oid: param("repo") === FORK ? forkMain : upstreamMain,
committer: signature,
message: "tip"
}
]
});
case "org.tangled.repo.listRepos":
return Response.json({
items: [
{
uri: `at://${FORK}/sh.tangled.repo/self`,
value: { source: { did: UPSTREAM }, declaration: { name: "core" } }
}
]
});
case "org.tangled.temp.git.listCommits":
return Response.json({
commits: [
{
oid: forkMain,
parents: [mergeBase],
tree: "tree",
author: signature,
committer: signature,
message: "fork only",
extraHeaders: []
}
]
});
case "org.tangled.temp.git.getMergeBase":
return Response.json({ commit: mergeBase });
default:
return Response.json({ error: "NotFound" }, { status: 404 });
}
});
const data = await loadCompose({
config: { bobbinUrl: "https://bobbin.test" },
repo: {
uri: `at://${UPSTREAM}/sh.tangled.repo/self`,
repoDid: UPSTREAM,
ownerHandle: "upstream.test",
name: "core",
knot: "knot.test"
} as never,
viewer: { did: "did:plc:alice", handle: "alice.test" },
params: new URLSearchParams({
source: "fork",
fork: FORK,
sourceBranch: "main",
targetBranch: "main"
}),
fetch: fetchMock
});
const call = (nsid: string) =>
fetchMock.mock.calls
.map(([input]) => new URL(String(input)))
.find((url) => url.pathname === `/xrpc/${nsid}`)!;
const commits = call("org.tangled.temp.git.listCommits");
expect(commits.searchParams.getAll("repos")).toEqual([FORK, UPSTREAM]);
expect(commits.searchParams.getAll("ranges")).toEqual([`${upstreamMain}..${forkMain}`]);
const base = call("org.tangled.temp.git.getMergeBase");
expect(base.searchParams.getAll("repos")).toEqual([FORK, UPSTREAM]);
expect(base.searchParams.get("base")).toBe(upstreamMain);
expect(base.searchParams.get("head")).toBe(forkMain);
expect(data.commitsError).toBe("");
expect(data.commits.map((commit) => commit.hash)).toEqual([forkMain]);
expect(data.mergeBase).toBe(mergeBase);
expect(data.sourceHead).toBe(forkMain);
});
});