2ea447a780
* refactor: replace narrow parameter types with context objects across action/ pass broader context objects (ToolContext, PromptContext, PostCleanupContext) to utility functions instead of cherry-picking fields into single-use interfaces. deletes 8 narrow types, simplifies call sites, and makes buildCommentFooter synchronous by reading ctx.runId/ctx.jobId directly instead of re-deriving from env vars and making an extra API call. Made-with: Cursor * fix: replace non-null assertion with local guard in validatePushDestination addresses review feedback — the function now validates pushUrl itself instead of relying on the caller's check, eliminating the ! assertion. Made-with: Cursor * revert: remove GH_TOKEN injection from restricted shell the original change exposed the git token in restricted-mode shell so `gh` CLI would work. this is a security regression for public repos: MCP tools are deliberately constrained (no merge, no release, no arbitrary API calls), but `gh api` with the token gives full GitHub API access to any prompt-injected agent. Made-with: Cursor
81 lines
2.8 KiB
TypeScript
81 lines
2.8 KiB
TypeScript
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
|
import { describe, expect, it } from "vitest";
|
|
import { acquireNewToken, createOctokit } from "../utils/github.ts";
|
|
import { fetchAndFormatPrDiff } from "./checkout.ts";
|
|
import type { ToolContext } from "./server.ts";
|
|
|
|
/**
|
|
* parses TOC entries like "- src/math.ts → lines 7-42" into structured data.
|
|
*/
|
|
function parseTocEntries(toc: string) {
|
|
const entries: Array<{ filename: string; startLine: number; endLine: number }> = [];
|
|
for (const line of toc.split("\n")) {
|
|
const match = line.match(/^- (.+) → lines (\d+)-(\d+)$/);
|
|
if (match) {
|
|
entries.push({
|
|
filename: match[1],
|
|
startLine: parseInt(match[2], 10),
|
|
endLine: parseInt(match[3], 10),
|
|
});
|
|
}
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
async function getToken(): Promise<string> {
|
|
// prefer explicit GH_TOKEN, fall back to acquiring one via GitHub App credentials
|
|
if (process.env.GH_TOKEN) return process.env.GH_TOKEN;
|
|
return await acquireNewToken();
|
|
}
|
|
|
|
describe("fetchAndFormatPrDiff", () => {
|
|
it(
|
|
"generates accurate TOC line numbers for pullfrog/test-repo#1",
|
|
{ timeout: 30000 },
|
|
async () => {
|
|
const token = await getToken();
|
|
const octokit = createOctokit(token);
|
|
const ctx = {
|
|
octokit,
|
|
repo: {
|
|
owner: "pullfrog",
|
|
name: "test-repo",
|
|
data: {} as RestEndpointMethodTypes["repos"]["get"]["response"]["data"],
|
|
},
|
|
} as ToolContext;
|
|
const result = await fetchAndFormatPrDiff(ctx, 1);
|
|
|
|
// verify content includes TOC at the start
|
|
expect(result.content.startsWith(result.toc)).toBe(true);
|
|
|
|
// parse TOC and validate every entry's line numbers against actual content
|
|
const contentLines = result.content.split("\n");
|
|
const tocEntries = parseTocEntries(result.toc);
|
|
expect(tocEntries.length).toBeGreaterThan(0);
|
|
|
|
for (const entry of tocEntries) {
|
|
// line numbers are 1-indexed, arrays are 0-indexed
|
|
const firstLine = contentLines[entry.startLine - 1];
|
|
expect(firstLine).toBeDefined();
|
|
// first line of each file section should be the diff header
|
|
expect(firstLine).toBe(`diff --git a/${entry.filename} b/${entry.filename}`);
|
|
|
|
// endLine should be within bounds
|
|
expect(entry.endLine).toBeLessThanOrEqual(contentLines.length);
|
|
}
|
|
|
|
// verify adjacent files don't overlap and are contiguous
|
|
for (let i = 1; i < tocEntries.length; i++) {
|
|
const prev = tocEntries[i - 1];
|
|
const curr = tocEntries[i];
|
|
// current file starts right after previous file ends
|
|
expect(curr.startLine).toBe(prev.endLine + 1);
|
|
}
|
|
|
|
// snapshot the full output for regression detection
|
|
expect(result.toc).toMatchSnapshot("toc");
|
|
expect(result.content).toMatchSnapshot("content");
|
|
}
|
|
);
|
|
});
|