diff --git a/lib/live-score/hosts.ts b/lib/live-score/hosts.ts index 1e61170..ff31c3b 100644 --- a/lib/live-score/hosts.ts +++ b/lib/live-score/hosts.ts @@ -227,35 +227,26 @@ export async function resolveCommit( ref: string, token?: string, ): Promise { - const init = { headers: requestHeaders(host, token) }; - - if (host === "github") { - const res = await fetch(`https://api.github.com/repos/${owner}/${name}/commits/${ref}`, init); - if (!res.ok) { - assertNotRateLimited(host, res); - return null; - } - return ((await res.json()) as { sha?: string }).sha ?? null; - } - - if (host === "gitlab") { - const res = await fetch( - `https://gitlab.com/api/v4/projects/${gitlabProjectId(owner, name)}/repository/commits/${ref}`, - init, - ); - if (!res.ok) { - assertNotRateLimited(host, res); - return null; - } - return ((await res.json()) as { id?: string }).id ?? null; - } - - const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${owner}/${name}/commit/${ref}`, init); + const url = + host === "github" + ? `https://api.github.com/repos/${owner}/${name}/commits/${ref}` + : host === "gitlab" + ? `https://gitlab.com/api/v4/projects/${gitlabProjectId(owner, name)}/repository/commits/${ref}` + : `https://api.bitbucket.org/2.0/repositories/${owner}/${name}/commit/${ref}`; + + const res = await fetch(url, { headers: requestHeaders(host, token) }); + + // Null is cached as "no such repo" for an hour, so only answers that really + // mean that may produce it: 404, and GitHub's 409 for an empty repo. A 401 + // from an expired token must throw, or every unindexed repo reads as missing. + if (res.status === 404 || res.status === 409) return null; if (!res.ok) { assertNotRateLimited(host, res); - return null; + throw new Error(`${res.status} resolving commit`); } - return ((await res.json()) as { hash?: string }).hash ?? null; + + const body = (await res.json()) as { sha?: string; id?: string; hash?: string }; + return body.sha ?? body.id ?? body.hash ?? null; } export function hostToken(host: RepoHost): string | undefined { diff --git a/scripts/seed-list.ts b/scripts/seed-list.ts index c816f40..6943331 100644 --- a/scripts/seed-list.ts +++ b/scripts/seed-list.ts @@ -575,6 +575,10 @@ export const SEEDS: Seed[] = [ note: "This project — meta self-ranking", url: "https://github.com/hsnice16/agent-friendly-code", }, + { + url: "https://github.com/hsnice16/tula", + note: "tula — crypto liquidation-risk terminal tool", + }, { url: "https://github.com/farzaa/clicky", note: "Clicky — coding-agent side project", diff --git a/tests/live-score.test.ts b/tests/live-score.test.ts index bfa469e..4b9b78c 100644 --- a/tests/live-score.test.ts +++ b/tests/live-score.test.ts @@ -1,10 +1,10 @@ import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { join } from "node:path"; -import { describe, it } from "node:test"; +import { afterEach, describe, it } from "node:test"; import { CONTENT_CANDIDATES } from "../lib/live-score/content-files"; -import { blobUrl, rawUrl } from "../lib/live-score/hosts"; +import { blobUrl, RateLimitedError, rawUrl, resolveCommit } from "../lib/live-score/hosts"; import { safeAbsolute } from "../lib/live-score/materialize"; import { SUPPORTED_HOSTS } from "../lib/live-score/supported"; @@ -59,6 +59,39 @@ describe("host URLs", () => { }); }); +describe("resolveCommit", () => { + const realFetch = globalThis.fetch; + afterEach(() => { + globalThis.fetch = realFetch; + }); + + const respond = (status: number, body: unknown = {}) => { + globalThis.fetch = (async () => new Response(JSON.stringify(body), { status })) as unknown as typeof fetch; + }; + + it("returns the sha on success", async () => { + respond(200, { sha: "abc" }); + assert.equal(await resolveCommit("github", "o", "n", "HEAD"), "abc"); + }); + + it("returns null only when the repo is missing or empty", async () => { + respond(404); + assert.equal(await resolveCommit("github", "o", "n", "HEAD"), null); + respond(409); + assert.equal(await resolveCommit("github", "o", "n", "HEAD"), null); + }); + + it("throws on a rejected token instead of reporting the repo missing", async () => { + respond(401); + await assert.rejects(resolveCommit("github", "o", "n", "HEAD", "stale")); + }); + + it("throws RateLimitedError on a spent quota", async () => { + respond(403); + await assert.rejects(resolveCommit("github", "o", "n", "HEAD"), RateLimitedError); + }); +}); + describe("supported hosts", () => { it("ships GitHub only until GitLab pagination and Bitbucket rate limits are guarded", () => { assert.deepEqual(SUPPORTED_HOSTS, ["github"]);