Files
paperclip/server/src/__tests__/git-source.test.ts
T
Chris Farhood 80f7d8270c refactor(portability): migrate to git-source; delete github-fetch.ts
Mirrors the skills refactor: company-portability was the second user of
the per-host REST shim (its own parallel parseGitHubSourceUrl + fetch
helpers + raw.githubusercontent URL builder), so importing a company
package from a non-github URL hit the same Gitea 404 the skills path did.

- Extend git-source.ts:
  - parseGitSourceUrl: also recognises query-string shape
    (?ref=...&path=...) used by portability URLs, with precedence over
    path-style segments when both are present.
  - RepoSnapshot: add readBinary (Uint8Array for the company logo
    fetch) and readFileOptional (null on NotFoundError, for the
    COMPANY.md probe + main->master fallback).
- Rewrite resolveSource in company-portability.ts to open a single
  in-memory snapshot per import and serve all reads (COMPANY.md,
  candidate tree, includes, logo) from it. Drops fetchText/fetchJson/
  fetchBinary/fetchOptionalText.
- parseGitHubSourceUrl stays exported with its original return shape
  ({hostname, owner, repo, ref, basePath, companyPath}) so the existing
  test suite passes unchanged. It now delegates URL parsing to
  parseGitSourceUrl and layers companyPath derivation on top.
- Delete server/src/services/github-fetch.ts: zero remaining callers.

Test coverage:
- 7 new git-source tests (query-string parse variants, query-string
  precedence over path style, readBinary, readFileOptional NotFound
  null + non-NotFound rethrow) — 34/34 passing.
- 52 existing company-portability tests still pass via the
  parseGitHubSourceUrl shim contract.
- Smoke-tested end-to-end against https://git.farh.net/.../?ref=main:
  ref resolves, snapshot opens, readFile/readBinary/readFileOptional
  all return expected results.

Note: two pre-existing failures in company-skills-routes.test.ts
("does not expose a skill reference...") exist on dev too and are
unrelated to this change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 10:28:22 -04:00

411 lines
15 KiB
TypeScript

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();
});
it("parses a query-string URL with ?ref= and ?path=", () => {
expect(
parseGitSourceUrl("https://github.com/o/r?ref=feature%2Fdemo&path=subdir"),
).toMatchObject({
cloneUrl: "https://github.com/o/r.git",
ref: "feature/demo",
basePath: "subdir",
filePath: null,
explicitRef: true,
});
});
it("parses a query-string URL with only ?ref=", () => {
expect(parseGitSourceUrl("https://github.com/o/r?ref=develop")).toMatchObject({
ref: "develop",
basePath: "",
explicitRef: true,
});
});
it("parses a query-string URL with only ?path=", () => {
expect(parseGitSourceUrl("https://github.com/o/r?path=sub")).toMatchObject({
ref: null,
basePath: "sub",
explicitRef: false,
});
});
it("query-string parsing takes precedence over path-style segments", () => {
expect(
parseGitSourceUrl("https://github.com/o/r/tree/main/old?ref=newref&path=newpath"),
).toMatchObject({
ref: "newref",
basePath: "newpath",
});
});
});
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);
});
it("readBinary returns the raw blob bytes", async () => {
cloneFn.mockResolvedValue(undefined);
resolveRefFn.mockResolvedValue("ffffffffffffffffffffffffffffffffffffffff");
walkFn.mockImplementation(async () => {});
const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]);
readBlobFn.mockResolvedValue({ blob: bytes });
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
const snap = await openRepoSnapshot(parsed, "main", "ffffffffffffffffffffffffffffffffffffffff");
const result = await snap.readBinary("logo.png");
expect(result).toBe(bytes);
});
it("readFileOptional returns null on NotFoundError", async () => {
cloneFn.mockResolvedValue(undefined);
resolveRefFn.mockResolvedValue("ffffffffffffffffffffffffffffffffffffffff");
walkFn.mockImplementation(async () => {});
const err = Object.assign(new Error("missing"), { code: "NotFoundError" });
readBlobFn.mockRejectedValue(err);
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
const snap = await openRepoSnapshot(parsed, "main", "ffffffffffffffffffffffffffffffffffffffff");
const result = await snap.readFileOptional("missing.md");
expect(result).toBeNull();
});
it("readFileOptional rethrows non-NotFound errors", async () => {
cloneFn.mockResolvedValue(undefined);
resolveRefFn.mockResolvedValue("ffffffffffffffffffffffffffffffffffffffff");
walkFn.mockImplementation(async () => {});
readBlobFn.mockRejectedValue(new Error("disk explosion"));
const parsed = parseGitSourceUrl("https://git.example.com/o/r");
const snap = await openRepoSnapshot(parsed, "main", "ffffffffffffffffffffffffffffffffffffffff");
await expect(snap.readFileOptional("any.md")).rejects.toThrow(/disk explosion/);
});
});