Files
pr-agent-server/server/e2e.ts
T
asepharyana 055501aed1 feat(server): Bun/TypeScript re-implementation of PR review engine
Reimplements the pr_agent review pipeline (previously Python package) in
Bun/TypeScript, verified end-to-end against a real GitHub App + 9router:

- src/diff.ts: git patch processing (extend_patch, line-numbered hunks,
  token-budget generate_full_patch, generated/invalid file filters)
- src/review.ts: orchestrator (fetch PR/diff -> render prompts -> LLM ->
  YAML parse -> markdown render -> publish), fallback models
- src/github.ts: GitHub App auth via @octokit/auth-app (JWT -> installation
  token, custom authStrategy for Bun), PR/diff/languages/comments REST
- src/prompts.ts: verbatim pr_reviewer_prompts.toml port (nunjucks templates)
- src/yaml.ts: resilient load_yaml with 9 fallback fixers
- src/markdown.ts: convert_to_markdown_v2 port ('PR Reviewer Guide' comment)
- src/llm.ts: 9router OpenAI-compatible chat completions (non-stream,
  temp omitted for claude-opus-5), timeout 600s
- src/token.ts: js-tiktoken o200k counting
- src/index.ts: webhook server (HMAC verify, segment analytics, /health,
  /api/metrics) + cli.ts one-shot review
- e2e.ts: real-world harness (bun e2e --repo o/r --pr N [--publish])

Verified: bun test 13/13, tsc clean, E2E published a real review comment
(## PR Reviewer Guide) on asepharyana/nextjs-template#19 via GitHub App
MythEclipseBotReview + claude-opus-5 through 9router.

Python run_server.py + pr_agent remain for the live queue worker; server/
is the replacement path.
2026-09-21 12:50:10 +07:00

79 lines
2.9 KiB
TypeScript

// E2E test harness — runs the full review pipeline against the REAL GitHub
// App + 9router. Not part of `bun test` (needs secrets); run explicitly.
//
// bun e2e --repo <owner/repo> --pr <number>
//
// Validates: GitHub App auth (JWT+installation token), diff fetch, token
// budget, prompt render, LLM call via 9router, YAML parse, markdown render.
// Does NOT publish a comment unless --publish is passed.
import { loadSecrets } from "./src/secrets";
import { loadConfig } from "./src/config";
import { GitHubProvider } from "./src/github";
import { runReview } from "./src/review";
import { countTokens } from "./src/token";
const args = process.argv.slice(2);
const repoArg = args[args.indexOf("--repo") + 1];
const prArg = Number(args[args.indexOf("--pr") + 1]);
const publish = args.includes("--publish");
if (!repoArg || !prArg) {
console.error("usage: bun e2e --repo owner/repo --pr N [--publish]");
process.exit(2);
}
const secrets = loadSecrets({
appDir: process.env.PR_AGENT_APP_DIR ?? "/var/lib/pr-agent-server",
});
const cfg = loadConfig();
cfg.llm.baseUrl = secrets.baseUrl;
cfg.llm.apiKey = secrets.omniKey;
cfg.github.appId = secrets.appId;
cfg.github.privateKey = secrets.privateKey;
const gh = new GitHubProvider(cfg, repoArg.split("/")[0], repoArg.split("/")[1], prArg, secrets.privateKey);
const startedAt = Date.now();
console.log(`\n=== E2E review ${repoArg}#${prArg} (publish=${publish}) ===`);
// sanity: hitting the GitHub API to prove App auth works before the long LLM call
try {
const pr = await gh.getPr();
console.log(`PR ${pr.number}: "${pr.title}" (${pr.additions}+ / ${pr.deletions}-, ${pr.changedFiles} files)`);
} catch (err) {
console.error("GitHub App auth / PR fetch failed:", err instanceof Error ? err.message : err);
process.exit(1);
}
try {
const res = await runReview(
cfg,
repoArg.split("/")[0],
repoArg.split("/")[1],
prArg,
secrets.privateKey,
{ publish },
);
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
console.log(`\n=== DONE in ${elapsed}s ===`);
console.log("status:", res.status);
if (res.markdown) {
console.log(`comment length: ${res.markdown.length} chars`);
console.log(`comment tokens: ${countTokens(res.markdown)}`);
console.log("--- comment head ---");
console.log(res.markdown.slice(0, 600));
console.log("--- comment tail ---");
console.log(res.markdown.slice(-600));
} else {
console.log("WARNING: markdown is empty/missing");
console.log("markdown present:", !!res.markdown, "| data present:", !!res.data);
}
console.log("model:", res.model, "| promptTokens:", res.promptTokens, "| completionTokens:", res.completionTokens);
process.exit(res.status === "success" ? 0 : 1);
} catch (err) {
const elapsed = ((Date.now() - startedAt) / 1000).toFixed(1);
console.error(`\n=== FAILED after ${elapsed}s ===`);
console.error(err instanceof Error ? err.stack ?? err.message : err);
process.exit(1);
}