Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/github/pr-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,8 @@ export async function getLastCloserLogin(env: Env, installationId: number, repoF
const token = await createInstallationToken(env, installationId);
const octokit = new Octokit({ auth: token });
let lastCloser: string | null = null;
for (let page = 1; ; page += 1) {
// Cap at 10 pages (1 000 events) — enough for any real PR timeline without risking API rate exhaustion.
for (let page = 1; page <= 10; page += 1) {
// issue-events are returned oldest-first; walk every page so the final `closed` entry is truly the latest.
const response = await octokit.request("GET /repos/{owner}/{repo}/issues/{issue_number}/events", { owner, repo, issue_number: issueNumber, per_page: 100, page });
const events = response.data as Array<{ event?: string; actor?: { login?: string | null } | null }>;
Expand All @@ -129,6 +130,7 @@ export async function getLastCloserLogin(env: Env, installationId: number, repoF
}
if (events.length < 100) return lastCloser;
}
return lastCloser;
} catch {
return null;
}
Expand Down
22 changes: 22 additions & 0 deletions test/unit/github-pr-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ describe("GitHub PR action primitives (#778)", () => {
});
await expect(getLastCloserLogin(envWithKey(), 123, "owner/repo", 19)).resolves.toBeNull();
});

it("returns the best-known closer when the 10-page event cap is reached (page-cap exit path)", async () => {
const fetchedPages: number[] = [];
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("/access_tokens")) return Response.json({ token: "t" });
if (url.includes("/issues/20/events")) {
const page = Number(new URL(url).searchParams.get("page") ?? "1");
fetchedPages.push(page);
// Page 5 contains the close event; all pages return exactly 100 entries so the loop never exits early.
const events = Array.from({ length: 100 }, (_, i) =>
page === 5 && i === 50 ? { event: "closed", actor: { login: "capped-closer" } } : { event: "labeled" },
);
return Response.json(events);
}
return new Response("unexpected", { status: 500 });
});
await expect(getLastCloserLogin(envWithKey(), 123, "owner/repo", 20)).resolves.toBe("capped-closer");
// Loop ran pages 1–10 and then exited via the cap, never requesting page 11.
expect(fetchedPages).toHaveLength(10);
expect(fetchedPages).not.toContain(11);
});
});

function generateRsaPrivateKeyPem(): string {
Expand Down
57 changes: 57 additions & 0 deletions test/unit/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,63 @@ NOVELTY_BONUS_SCALAR = 3
expect(bootstrap.warnings.join(" ")).toMatch(/fetch failed/i);
});

it("does not freeze a prior fallback snapshot — bootstraps fresh defaults instead", async () => {
const env = createTestEnv();
vi.stubGlobal("fetch", async () => new Response("gone", { status: 410 }));
// First refresh → fallback stored (no verified last-good yet).
const first = await refreshScoringModelSnapshot(env);
expect(first.sourceKind).toBe("fallback");
// Second refresh — constants still fail; lastGood exists but sourceKind === "fallback"
// → the guard (lastGood && sourceKind !== "fallback") is false → must NOT freeze → new fallback.
const second = await refreshScoringModelSnapshot(env);
expect(second.sourceKind).toBe("fallback");
expect(second.id).not.toBe(first.id);
expect(second.warnings.join(" ")).not.toMatch(/froze the last-good/i);
});

it("falls back to the mutable ref when the upstream SHA lookup throws (fetchUpstreamRefSha catch path)", async () => {
const env = createTestEnv({ GITTENSOR_UPSTREAM_REPO: "custom/upstream", GITTENSOR_UPSTREAM_REF: "test" });
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("github.com/ghapi") && url.includes("/commits/")) throw new Error("network failure");
if (url.includes("constants.py")) return new Response("MERGED_PR_BASE_SCORE = 25\n");
if (url.includes("programming_languages.json")) return Response.json({});
return new Response("not found", { status: 404 });
});
const snapshot = await refreshScoringModelSnapshot(env);
expect(snapshot.sourceUrl).toContain("/test/gittensor/constants.py");
expect((snapshot.payload as Record<string, unknown>).upstreamSourceSha).toBeUndefined();
expect(snapshot.sourceKind).toBe("raw-github");
});

it("falls back to the mutable ref when the SHA endpoint returns a non-string sha", async () => {
const env = createTestEnv({ GITTENSOR_UPSTREAM_REPO: "custom/upstream", GITTENSOR_UPSTREAM_REF: "test" });
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("github.com/ghapi") && url.includes("/commits/")) return Response.json({ sha: 42 });
if (url.includes("constants.py")) return new Response("MERGED_PR_BASE_SCORE = 25\n");
if (url.includes("programming_languages.json")) return Response.json({});
return new Response("not found", { status: 404 });
});
const snapshot = await refreshScoringModelSnapshot(env);
expect(snapshot.sourceUrl).toContain("/test/gittensor/constants.py");
expect((snapshot.payload as Record<string, unknown>).upstreamSourceSha).toBeUndefined();
});

it("falls back to the mutable ref when the SHA endpoint returns an empty sha string", async () => {
const env = createTestEnv({ GITTENSOR_UPSTREAM_REPO: "custom/upstream", GITTENSOR_UPSTREAM_REF: "test" });
vi.stubGlobal("fetch", async (input: RequestInfo | URL) => {
const url = input.toString();
if (url.includes("github.com/ghapi") && url.includes("/commits/")) return Response.json({ sha: "" });
if (url.includes("constants.py")) return new Response("MERGED_PR_BASE_SCORE = 25\n");
if (url.includes("programming_languages.json")) return Response.json({});
return new Response("not found", { status: 404 });
});
const snapshot = await refreshScoringModelSnapshot(env);
expect(snapshot.sourceUrl).toContain("/test/gittensor/constants.py");
expect((snapshot.payload as Record<string, unknown>).upstreamSourceSha).toBeUndefined();
});

it("uses saturation math as the active private preview model", () => {
const saturationSnapshot: ScoringModelSnapshotRecord = {
...snapshot,
Expand Down
Loading