test(skills): add vitest coverage for git-source module

27 tests covering the surface that had none:

- parseGitSourceUrl: bare URLs (github/gitea/gitlab), tree/blob/src
  shapes, subpaths, file paths, trailing .git stripping, https-only
  enforcement, malformed/missing-segment rejection.
- resolveGitRef: 40-hex SHA passthrough (no network call), default
  branch via HEAD symref, named branch, peeled annotated tag, lightweight
  tag, ref-not-found, network/401/404 error translation, onAuth
  callback shape (token-as-username, x-oauth-basic) and absence.
- openRepoSnapshot: clone args (singleBranch/depth=1/noCheckout),
  tree walk filtering trees vs blobs, readFile path, SHA fallback
  when tracking ref is null, 404 translation.

Mocks at the isomorphic-git boundary; verifies our adaptation logic,
not isomorphic-git itself.

Known limit surfaced by a test (not fixed here): gitea URLs with
slash-containing branch names like /src/branch/feature/x are
ambiguous without server-side disambiguation. The test uses a
single-segment branch; the multi-segment case needs a separate fix
(refCandidates from longest-to-shortest, resolved against
listServerRefs output).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 09:36:27 -04:00
parent 0fd4e9c4d1
commit d30afdb1b2
+336
View File
@@ -0,0 +1,336 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const listServerRefs = vi.fn();
const cloneFn = vi.fn();
const walkFn = vi.fn();
const readBlobFn = vi.fn();
const resolveRefFn = vi.fn();
const treeFn = vi.fn((args: unknown) => ({ __tree: args }));
vi.mock("isomorphic-git", () => ({
default: {
listServerRefs: (...args: unknown[]) => listServerRefs(...args),
clone: (...args: unknown[]) => cloneFn(...args),
walk: (...args: unknown[]) => walkFn(...args),
readBlob: (...args: unknown[]) => readBlobFn(...args),
resolveRef: (...args: unknown[]) => resolveRefFn(...args),
TREE: (...args: unknown[]) => treeFn(...args),
},
}));
vi.mock("isomorphic-git/http/node", () => ({
default: { request: vi.fn() },
}));
const { parseGitSourceUrl, resolveGitRef, openRepoSnapshot, buildCloneUrl } =
await import("../services/git-source.js");
beforeEach(() => {
listServerRefs.mockReset();
cloneFn.mockReset();
walkFn.mockReset();
readBlobFn.mockReset();
resolveRefFn.mockReset();
treeFn.mockClear();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("parseGitSourceUrl", () => {
it("parses a bare github repo URL", () => {
expect(parseGitSourceUrl("https://github.com/anthropics/claude-code")).toMatchObject({
cloneUrl: "https://github.com/anthropics/claude-code.git",
hostname: "github.com",
owner: "anthropics",
repo: "claude-code",
ref: null,
basePath: "",
filePath: null,
explicitRef: false,
});
});
it("strips trailing .git from the repo segment", () => {
expect(parseGitSourceUrl("https://example.com/o/r.git")).toMatchObject({
cloneUrl: "https://example.com/o/r.git",
repo: "r",
});
});
it("parses a github tree URL with subpath", () => {
expect(
parseGitSourceUrl("https://github.com/o/r/tree/develop/sub/dir"),
).toMatchObject({
ref: "develop",
basePath: "sub/dir",
filePath: null,
explicitRef: true,
});
});
it("parses a github blob URL as a file path", () => {
expect(
parseGitSourceUrl("https://github.com/o/r/blob/main/path/to/file.md"),
).toMatchObject({
ref: "main",
basePath: "path/to",
filePath: "path/to/file.md",
explicitRef: true,
});
});
it("parses a gitea src/branch URL with subpath", () => {
expect(
parseGitSourceUrl("https://git.example.com/o/r/src/branch/main/skills"),
).toMatchObject({
cloneUrl: "https://git.example.com/o/r.git",
ref: "main",
basePath: "skills",
filePath: null,
explicitRef: true,
});
});
it("parses a gitea src/tag URL", () => {
expect(
parseGitSourceUrl("https://git.example.com/o/r/src/tag/v1.2.3"),
).toMatchObject({
ref: "v1.2.3",
basePath: "",
explicitRef: true,
});
});
it("parses a gitea src/commit URL with file", () => {
expect(
parseGitSourceUrl("https://git.example.com/o/r/src/commit/abc123/dir/SKILL.md"),
).toMatchObject({
ref: "abc123",
basePath: "dir",
filePath: "dir/SKILL.md",
});
});
it("parses a gitlab tree URL", () => {
expect(
parseGitSourceUrl("https://gitlab.com/group/proj/-/tree/main/sub"),
).toMatchObject({
cloneUrl: "https://gitlab.com/group/proj.git",
ref: "main",
basePath: "sub",
explicitRef: true,
});
});
it("parses a gitlab blob URL", () => {
expect(
parseGitSourceUrl("https://gitlab.com/group/proj/-/blob/main/sub/file.md"),
).toMatchObject({
ref: "main",
filePath: "sub/file.md",
basePath: "sub",
});
});
it("rejects non-https URLs", () => {
expect(() => parseGitSourceUrl("http://github.com/o/r")).toThrow(/HTTPS/);
});
it("rejects URLs without owner/repo", () => {
expect(() => parseGitSourceUrl("https://github.com/o")).toThrow();
});
it("rejects malformed URLs", () => {
expect(() => parseGitSourceUrl("not a url")).toThrow();
});
});
describe("buildCloneUrl", () => {
it("produces a .git suffix URL on the given host", () => {
expect(buildCloneUrl("git.example.com", "o", "r")).toBe(
"https://git.example.com/o/r.git",
);
});
});
describe("resolveGitRef", () => {
it("passes through a 40-hex SHA without hitting the network", async () => {
const parsed = parseGitSourceUrl(
"https://github.com/o/r/tree/0123456789abcdef0123456789abcdef01234567",
);
const result = await resolveGitRef(parsed);
expect(result).toEqual({
pinnedSha: "0123456789abcdef0123456789abcdef01234567",
trackingRef: "0123456789abcdef0123456789abcdef01234567",
});
expect(listServerRefs).not.toHaveBeenCalled();
});
it("returns default branch via HEAD symref when ref is absent", async () => {
listServerRefs.mockResolvedValue([
{ ref: "HEAD", oid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", target: "refs/heads/main" },
{ ref: "refs/heads/main", oid: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
{ ref: "refs/heads/chore", oid: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
const result = await resolveGitRef(parsed);
expect(result).toEqual({
pinnedSha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
trackingRef: "main",
});
expect(listServerRefs).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://git.example.com/o/r.git",
symrefs: true,
protocolVersion: 2,
}),
);
});
it("resolves a named branch to its SHA", async () => {
listServerRefs.mockResolvedValue([
{ ref: "HEAD", oid: "1111111111111111111111111111111111111111", target: "refs/heads/main" },
{ ref: "refs/heads/main", oid: "1111111111111111111111111111111111111111" },
{ ref: "refs/heads/develop", oid: "2222222222222222222222222222222222222222" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/branch/develop");
const result = await resolveGitRef(parsed);
expect(result).toEqual({
pinnedSha: "2222222222222222222222222222222222222222",
trackingRef: "develop",
});
});
it("prefers a peeled annotated tag over the tag object", async () => {
listServerRefs.mockResolvedValue([
{ ref: "refs/tags/v1.0", oid: "tttttttttttttttttttttttttttttttttttttttt" },
{ ref: "refs/tags/v1.0^{}", oid: "cccccccccccccccccccccccccccccccccccccccc" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/tag/v1.0");
const result = await resolveGitRef(parsed);
expect(result.pinnedSha).toBe("cccccccccccccccccccccccccccccccccccccccc");
expect(result.trackingRef).toBe("v1.0");
});
it("resolves a lightweight tag when no peeled entry exists", async () => {
listServerRefs.mockResolvedValue([
{ ref: "refs/tags/v2.0", oid: "dddddddddddddddddddddddddddddddddddddddd" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/tag/v2.0");
const result = await resolveGitRef(parsed);
expect(result.pinnedSha).toBe("dddddddddddddddddddddddddddddddddddddddd");
});
it("throws when an explicit ref does not exist", async () => {
listServerRefs.mockResolvedValue([
{ ref: "HEAD", oid: "9999999999999999999999999999999999999999", target: "refs/heads/main" },
{ ref: "refs/heads/main", oid: "9999999999999999999999999999999999999999" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r/src/branch/missing");
await expect(resolveGitRef(parsed)).rejects.toThrow(/Ref 'missing' not found/);
});
it("translates network errors into a user-facing message", async () => {
listServerRefs.mockRejectedValue(new Error("ENOTFOUND git.invalid"));
const parsed = parseGitSourceUrl("https://git.invalid/o/r");
await expect(resolveGitRef(parsed)).rejects.toThrow(/could not connect/i);
});
it("translates 401 errors into an auth message", async () => {
listServerRefs.mockRejectedValue(new Error("HTTP Error: 401 Unauthorized"));
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
await expect(resolveGitRef(parsed)).rejects.toThrow(/authentication/i);
});
it("translates 404 errors into a repo-not-found message", async () => {
listServerRefs.mockRejectedValue(new Error("HTTP Error: 404 Not Found"));
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
await expect(resolveGitRef(parsed)).rejects.toThrow(/repository not found/i);
});
it("sends an onAuth callback when a token is supplied", async () => {
listServerRefs.mockResolvedValue([
{ ref: "HEAD", oid: "1111111111111111111111111111111111111111", target: "refs/heads/main" },
{ ref: "refs/heads/main", oid: "1111111111111111111111111111111111111111" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
await resolveGitRef(parsed, "tok_abc");
const callArgs = listServerRefs.mock.calls[0]![0] as { onAuth: () => unknown };
expect(typeof callArgs.onAuth).toBe("function");
expect(callArgs.onAuth()).toEqual({ username: "tok_abc", password: "x-oauth-basic" });
});
it("omits onAuth when no token is supplied", async () => {
listServerRefs.mockResolvedValue([
{ ref: "HEAD", oid: "1111111111111111111111111111111111111111", target: "refs/heads/main" },
{ ref: "refs/heads/main", oid: "1111111111111111111111111111111111111111" },
]);
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
await resolveGitRef(parsed);
const callArgs = listServerRefs.mock.calls[0]![0] as { onAuth?: unknown };
expect(callArgs.onAuth).toBeUndefined();
});
});
describe("openRepoSnapshot", () => {
it("clones at the tracking ref and walks the tree at the resolved SHA", async () => {
cloneFn.mockResolvedValue(undefined);
resolveRefFn.mockResolvedValue("ffffffffffffffffffffffffffffffffffffffff");
walkFn.mockImplementation(async ({ map }: { map: (filepath: string, entries: Array<{ type: () => Promise<string> }>) => Promise<void> }) => {
await map(".", [{ type: () => Promise.resolve("tree") }]);
await map("README.md", [{ type: () => Promise.resolve("blob") }]);
await map("skills/x/SKILL.md", [{ type: () => Promise.resolve("blob") }]);
await map("skills/x", [{ type: () => Promise.resolve("tree") }]);
});
readBlobFn.mockResolvedValue({ blob: new TextEncoder().encode("hello") });
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
const snap = await openRepoSnapshot(parsed, "main", "ffffffffffffffffffffffffffffffffffffffff", "tok");
expect(cloneFn).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://git.example.com/o/r.git",
ref: "main",
singleBranch: true,
depth: 1,
noCheckout: true,
}),
);
expect(snap.sha).toBe("ffffffffffffffffffffffffffffffffffffffff");
const files = await snap.listFiles();
expect(files).toEqual(["README.md", "skills/x/SKILL.md"]);
const content = await snap.readFile("README.md");
expect(content).toBe("hello");
expect(readBlobFn).toHaveBeenCalledWith(
expect.objectContaining({
oid: "ffffffffffffffffffffffffffffffffffffffff",
filepath: "README.md",
}),
);
});
it("falls back to the expected SHA as ref when no tracking ref is known", async () => {
cloneFn.mockResolvedValue(undefined);
resolveRefFn.mockResolvedValue("abc1234567890abc1234567890abc1234567890a");
walkFn.mockImplementation(async () => {});
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
await openRepoSnapshot(parsed, null, "abc1234567890abc1234567890abc1234567890a");
expect(cloneFn).toHaveBeenCalledWith(
expect.objectContaining({ ref: "abc1234567890abc1234567890abc1234567890a" }),
);
});
it("surfaces a 404 from clone as repository-not-found", async () => {
cloneFn.mockRejectedValue(new Error("HTTP Error: 404 Not Found"));
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
await expect(
openRepoSnapshot(parsed, "main", "1111111111111111111111111111111111111111"),
).rejects.toThrow(/repository not found/i);
});
});