test(mcp): make checkout/reviewComments tests offline (fixture-driven) (#575)

`mcp/checkout.test.ts` and `mcp/reviewComments.test.ts` previously hit
live GitHub on every run via `acquireNewToken()`, requiring `GH_TOKEN`
or `GITHUB_APP_ID` + `GITHUB_PRIVATE_KEY` in the env. that made them:

- cred-gated — the action runtime filters `_KEY$` / `_TOKEN$` from
  subprocess env, so the husky pre-push hook (which runs
  `pnpm -r test`) blocked Pullfrog agents from pushing branches. issues
  #562, #563, #564, #566 all hit this exact blocker and never got their
  fixes pushed.
- non-deterministic and slow (network round-trips for a snapshot test).

both tests are really snapshot tests of pure formatters
(`formatFilesWithLineNumbers`, plus `parseFilePatches` /
`buildThreadBlocks` / `formatReviewThreads` for review data). the live
fetches were just an inefficient way to obtain fixtures.

changes:

1. extract a pure `formatReviewData({ review, threads, prFiles, ... })`
   from `getReviewData` in `mcp/reviewComments.ts`. `getReviewData`
   becomes thin orchestration: fetch + call formatter. preserves the
   "skip listFiles when no threads" perf optimization.

2. add `action/mcp/__fixtures__/` with checked-in JSON captures for the
   three fixture test cases (pullfrog/test-repo#1 listFiles,
   pullfrog/scratch#49 review 3485940013, pullfrog/scratch#64 review
   3531000326). ~14KB total. fixtures store only the fields the
   formatter reads — volatile fields (sha, blob_url, etc.) are dropped.

3. rewrite both test files to load the fixtures and call the pure
   formatters directly. snapshot keys updated; snapshot content
   unchanged (verified by running existing snapshots against the
   refactored tests).

4. add `action/scripts/refresh-test-fixtures.ts` to re-fetch the
   fixtures from live GitHub on demand:
   `node action/scripts/refresh-test-fixtures.ts` (with creds in
   `.env` or env). re-run when the GitHub API response shape changes
   and review the snapshot diff.

trade-off: a silent change to GitHub's `pulls.listFiles` /
`pulls.getReview` / GraphQL `reviewThreads` response shape would no
longer break this test on every push. that tradeoff is worth it: shape
drift on those endpoints is rare (years between changes), and a
dedicated cron that runs the refresh script and opens a PR on diff is
a far better signal than a flaky cred-gated pre-push hook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
David Blass
2026-05-05 23:25:46 +00:00
committed by pullfrog[bot]
parent b6e2c61d30
commit ada5584737
9 changed files with 506 additions and 119 deletions
+30 -33
View File
@@ -1,43 +1,40 @@
import { Octokit } from "@octokit/rest";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { acquireNewToken } from "../utils/github.ts";
import { getReviewData } from "./reviewComments.ts";
import { type FormatReviewDataInput, formatReviewData } from "./reviewComments.ts";
async function getToken(): Promise<string> {
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
return await acquireNewToken();
// fixtures captured by action/scripts/refresh-test-fixtures.ts; re-run
// (with creds) when GitHub's review/threads/listFiles response shape
// changes, then review the snapshot diff.
type ReviewFixture = FormatReviewDataInput & {
owner: string;
name: string;
};
function loadFixture(file: string): ReviewFixture {
return JSON.parse(
readFileSync(resolve(import.meta.dirname, "__fixtures__", file), "utf-8")
) as ReviewFixture;
}
describe("getFormattedReviewThreads", () => {
it("formats thread blocks with TOC and correct line numbers", { timeout: 30000 }, async () => {
const token = await getToken();
const octokit = new Octokit({ auth: token });
describe("formatReviewData", () => {
it("formats thread blocks with TOC and correct line numbers", () => {
const fx = loadFixture("pullfrog-scratch-pr-49-review-3485940013.json");
const result = formatReviewData(fx);
expect(result).toBeDefined();
if (!result) return;
const { formatted } = (await getReviewData({
octokit,
owner: "pullfrog",
name: "scratch",
pullNumber: 49,
reviewId: 3485940013,
}))!;
expect(formatted.toc).toMatchSnapshot("toc");
expect(formatted.content).toMatchSnapshot("content");
expect(result.formatted.toc).toMatchSnapshot("toc");
expect(result.formatted.content).toMatchSnapshot("content");
});
it("formats body-only review", { timeout: 30000 }, async () => {
const token = await getToken();
const octokit = new Octokit({ auth: token });
it("formats body-only review", () => {
const fx = loadFixture("pullfrog-scratch-pr-64-review-3531000326.json");
const result = formatReviewData(fx);
expect(result).toBeDefined();
if (!result) return;
const { formatted } = (await getReviewData({
octokit,
owner: "pullfrog",
name: "scratch",
pullNumber: 64,
reviewId: 3531000326,
}))!;
expect(formatted.toc).toMatchSnapshot("toc");
expect(formatted.content).toMatchSnapshot("content");
expect(result.formatted.toc).toMatchSnapshot("toc");
expect(result.formatted.content).toMatchSnapshot("content");
});
});