@theihtisham/review-agent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ReviewAgent Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,352 @@
1
+ # ReviewAgent
2
+
3
+ **Your senior dev in a GitHub Action — AI reviews every PR with line-by-line comments, catches bugs and security holes before merge.**
4
+
5
+ [![GitHub Action](https://img.shields.io/badge/GitHub-Action-blue?logo=github)](https://github.com/features/actions)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-Strict-3178C6?logo=typescript)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
8
+ [![Vitest](https://img.shields.io/badge/Tested%20with-Vitest-6E9F18?logo=vitest)](https://vitest.dev/)
9
+
10
+ ---
11
+
12
+ ## What It Does
13
+
14
+ ReviewAgent watches every pull request and automatically posts a **line-by-line code review** using AI. It catches bugs, security vulnerabilities, performance issues, and style violations — then posts them as GitHub review comments on the exact lines that need attention.
15
+
16
+ ### The Review
17
+
18
+ Every review includes:
19
+
20
+ | Category | What It Catches |
21
+ |----------|----------------|
22
+ | **Bugs** | Null/undefined access, off-by-one errors, race conditions, unhandled edge cases, logic errors |
23
+ | **Security** | SQL injection, XSS, hardcoded secrets, eval() usage, command injection, OWASP Top 10 |
24
+ | **Performance** | N+1 queries, memory leaks, inefficient algorithms, unnecessary re-renders |
25
+ | **Style** | Naming, formatting, readability, code organization |
26
+ | **Convention** | Violations of your repo's own patterns and naming styles |
27
+
28
+ ### The Output
29
+
30
+ Each review produces:
31
+
32
+ - **Line-by-line comments** on the exact lines with issues
33
+ - **Severity tags** — critical / warning / info
34
+ - **Category labels** — bug / security / performance / style / convention
35
+ - **Overall quality score** (0-100)
36
+ - **Summary comment** with breakdown table
37
+
38
+ ---
39
+
40
+ ## Demo
41
+
42
+ Here's what a ReviewAgent review looks like on a real PR:
43
+
44
+ ### PR introduces a login endpoint with security issues:
45
+
46
+ ```typescript
47
+ // src/auth.ts
48
+ const API_KEY = "sk-1234567890abcdef";
49
+
50
+ function login(req: Request, res: Response) {
51
+ const query = "SELECT * FROM users WHERE name = '" + req.body.username + "'";
52
+ db.query(query);
53
+ if (req.body.password === ADMIN_PASSWORD) {
54
+ res.redirect(req.query.returnUrl);
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### ReviewAgent posts these inline comments:
60
+
61
+ > **Line 2** — `[Security] (critical)` Hardcoded secret detected. Move this to an environment variable or secret manager.
62
+ > *OWASP: A07:2021-Identification and Authentication Failures*
63
+
64
+ > **Line 5** — `[Security] (critical)` Potential SQL injection: avoid string concatenation in queries. Use parameterized queries instead.
65
+ > *OWASP: A03:2021-Injection*
66
+
67
+ > **Line 7** — `[Security] (critical)` Potential open redirect. Validate and whitelist redirect targets.
68
+ > *OWASP: A01:2021-Broken Access Control*
69
+
70
+ ### And a summary comment:
71
+
72
+ ```
73
+ ## 🔴 ReviewAgent Code Review Summary
74
+
75
+ **Score: 25/100** — Poor
76
+
77
+ Critical security issues found: SQL injection, hardcoded secrets, and open redirect.
78
+
79
+ | Category | Count |
80
+ |----------|-------|
81
+ | 🐛 Bug | 1 |
82
+ | 🔒 Security | 3 |
83
+ | ⚡ Performance | 0 |
84
+ | 🎨 Style | 1 |
85
+
86
+ ### Stats
87
+ - **Files reviewed:** 3
88
+ - **Comments posted:** 5
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Installation
94
+
95
+ Add ReviewAgent to any repository in **5 lines of YAML**:
96
+
97
+ ```yaml
98
+ # .github/workflows/review.yml
99
+ name: AI Code Review
100
+ on: [pull_request]
101
+ jobs:
102
+ review:
103
+ runs-on: ubuntu-latest
104
+ steps:
105
+ - uses: actions/checkout@v4
106
+ - uses: reviewagent/review-agent@v1
107
+ with:
108
+ github-token: ${{ secrets.GITHUB_TOKEN }}
109
+ llm-api-key: ${{ secrets.OPENAI_API_KEY }}
110
+ ```
111
+
112
+ That's it. Every PR now gets an AI code review.
113
+
114
+ ---
115
+
116
+ ## Configuration
117
+
118
+ ### Action Inputs
119
+
120
+ | Input | Default | Description |
121
+ |-------|---------|-------------|
122
+ | `github-token` | *required* | GitHub token for API access (`secrets.GITHUB_TOKEN` or a PAT) |
123
+ | `llm-provider` | `openai` | LLM provider: `openai`, `anthropic`, or `ollama` |
124
+ | `llm-api-key` | `""` | API key for the LLM (omit for Ollama) |
125
+ | `llm-model` | `gpt-4o` | Model name (e.g., `gpt-4o`, `claude-sonnet-4-20250514`, `llama3.1`) |
126
+ | `llm-base-url` | auto | Custom API endpoint (required for self-hosted models) |
127
+ | `config-path` | `.reviewagent.yml` | Path to config file in the repo |
128
+ | `severity` | `warning` | Minimum severity to report: `critical`, `warning`, `info` |
129
+ | `max-comments` | `50` | Maximum review comments per PR |
130
+ | `review-type` | `comment` | GitHub review type: `approve`, `request-changes`, `comment` |
131
+ | `language-hints` | `""` | Comma-separated languages (e.g., `typescript,python`) |
132
+ | `learn-conventions` | `true` | Learn repo conventions from existing code |
133
+
134
+ ### Using with OpenAI
135
+
136
+ ```yaml
137
+ - uses: reviewagent/review-agent@v1
138
+ with:
139
+ github-token: ${{ secrets.GITHUB_TOKEN }}
140
+ llm-provider: openai
141
+ llm-api-key: ${{ secrets.OPENAI_API_KEY }}
142
+ llm-model: gpt-4o
143
+ ```
144
+
145
+ ### Using with Anthropic
146
+
147
+ ```yaml
148
+ - uses: reviewagent/review-agent@v1
149
+ with:
150
+ github-token: ${{ secrets.GITHUB_TOKEN }}
151
+ llm-provider: anthropic
152
+ llm-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
153
+ llm-model: claude-sonnet-4-20250514
154
+ ```
155
+
156
+ ### Using with Ollama (Free, Self-Hosted)
157
+
158
+ ```yaml
159
+ - uses: reviewagent/review-agent@v1
160
+ with:
161
+ github-token: ${{ secrets.GITHUB_TOKEN }}
162
+ llm-provider: ollama
163
+ llm-model: llama3.1
164
+ llm-base-url: http://your-ollama-host:11434/v1
165
+ ```
166
+
167
+ No API key needed. Run Ollama on any machine with a GPU and point the action at it.
168
+
169
+ ---
170
+
171
+ ## Custom Rules (`.reviewagent.yml`)
172
+
173
+ Create a `.reviewagent.yml` file in your repo root to customize ReviewAgent:
174
+
175
+ ```yaml
176
+ # .reviewagent.yml
177
+
178
+ # Custom review rules
179
+ rules:
180
+ - name: "no-console-log"
181
+ pattern: "console\\.log"
182
+ message: "Use the logger module instead of console.log"
183
+ severity: warning
184
+ category: convention
185
+
186
+ - name: "no-any-type"
187
+ pattern: ":\\s*any\\b"
188
+ message: "Avoid 'any' type. Use a specific type or 'unknown'."
189
+ severity: warning
190
+ category: convention
191
+
192
+ - name: "require-error-boundary"
193
+ pattern: "export\\s+default\\s+function\\s+\\w+"
194
+ message: "Top-level components should be wrapped in an ErrorBoundary."
195
+ severity: info
196
+ category: convention
197
+
198
+ # Additional paths to ignore
199
+ ignore:
200
+ paths:
201
+ - "proto/**"
202
+ - "**/*.generated.ts"
203
+ extensions:
204
+ - ".proto"
205
+ ```
206
+
207
+ ### Rule Fields
208
+
209
+ | Field | Required | Description |
210
+ |-------|----------|-------------|
211
+ | `name` | Yes | Unique rule identifier |
212
+ | `pattern` | Yes | Regex pattern to match in changed lines |
213
+ | `message` | Yes | Message shown in the review comment |
214
+ | `severity` | Yes | `critical`, `warning`, or `info` |
215
+ | `category` | Yes | `bug`, `security`, `performance`, `style`, or `convention` |
216
+
217
+ ---
218
+
219
+ ## Architecture
220
+
221
+ ```
222
+ PR opened/updated
223
+ |
224
+ v
225
+ GitHub Action triggered
226
+ |
227
+ v
228
+ Parse action inputs + .reviewagent.yml
229
+ |
230
+ v
231
+ Fetch PR diff (only changed files)
232
+ |
233
+ v
234
+ Filter out ignored files
235
+ (node_modules, generated, binaries, etc.)
236
+ |
237
+ +--> Static Security Scanner (local, fast)
238
+ | - 14 OWASP-aware patterns
239
+ | - Custom regex rules from config
240
+ |
241
+ +--> LLM Deep Review (AI-powered)
242
+ | - Per-file analysis with diff context
243
+ | - Repo conventions injected in prompt
244
+ | - JSON-structured response
245
+ |
246
+ v
247
+ Merge & deduplicate findings
248
+ |
249
+ v
250
+ Sort by severity (critical first)
251
+ |
252
+ v
253
+ Post GitHub Review
254
+ (inline comments + summary + score)
255
+ ```
256
+
257
+ ### Key Design Decisions
258
+
259
+ - **Diff-aware**: Only reviews lines that changed. No noise from untouched code.
260
+ - **Two-pass review**: Fast static scan for known patterns, then deep LLM analysis for nuanced issues.
261
+ - **Convention learning**: Reads your existing codebase to learn naming styles and patterns before reviewing.
262
+ - **Rate limiting**: Built-in rate limiter prevents API abuse (configurable concurrency and intervals).
263
+ - **Fallback**: If inline review fails (e.g., outdated diff), posts as a regular PR comment.
264
+
265
+ ---
266
+
267
+ ## Security
268
+
269
+ ReviewAgent takes security seriously:
270
+
271
+ - **Never logs code content** — all diffs are sanitized before logging
272
+ - **API keys via secrets only** — keys are masked in all GitHub Actions output
273
+ - **Input validation** — all action inputs are validated before use
274
+ - **No data storage** — code is sent to the LLM provider for analysis and not stored
275
+ - **Secret redaction** — log sanitizer catches accidental secret leaks in output
276
+
277
+ ### Recommendations
278
+
279
+ - Use `secrets.GITHUB_TOKEN` (automatic) or a fine-grained PAT with minimal permissions
280
+ - Store LLM API keys in GitHub Secrets, never in workflow files
281
+ - For self-hosted Ollama, use a private network or VPN
282
+
283
+ ---
284
+
285
+ ## Development
286
+
287
+ ```bash
288
+ # Install dependencies
289
+ npm install
290
+
291
+ # Run tests
292
+ npm test
293
+
294
+ # Run tests with coverage
295
+ npm run test:coverage
296
+
297
+ # Type check
298
+ npm run lint
299
+
300
+ # Build for production
301
+ npm run build
302
+
303
+ # Full check (lint + test + build)
304
+ npm run all
305
+ ```
306
+
307
+ ### Project Structure
308
+
309
+ ```
310
+ 11-review-agent/
311
+ src/
312
+ main.ts # Action entry point
313
+ config.ts # Input parsing, config building
314
+ types.ts # TypeScript type definitions
315
+ github.ts # GitHub API client (reviews, diffs)
316
+ llm-client.ts # LLM client (OpenAI, Anthropic, Ollama)
317
+ reviewer.ts # Core review orchestrator
318
+ conventions.ts # Repo convention learning
319
+ reviewers/
320
+ security.ts # Static security pattern scanner
321
+ utils/
322
+ diff-parser.ts # Patch parsing, line extraction
323
+ rate-limiter.ts # Rate limiting and retry logic
324
+ security.ts # Sanitization, formatting utilities
325
+ __tests__/
326
+ config.test.ts
327
+ diff-parser.test.ts
328
+ security.test.ts
329
+ rate-limiter.test.ts
330
+ security-utils.test.ts
331
+ llm-client.test.ts
332
+ fixtures/
333
+ mock-data.ts
334
+ action.yml # GitHub Action definition
335
+ package.json
336
+ tsconfig.json
337
+ vitest.config.ts
338
+ LICENSE
339
+ README.md
340
+ ```
341
+
342
+ ---
343
+
344
+ ## License
345
+
346
+ [MIT](LICENSE) — use it however you want.
347
+
348
+ ---
349
+
350
+ <p align="center">
351
+ <strong>ReviewAgent</strong> — Ship better code, faster.
352
+ </p>
@@ -0,0 +1,99 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parseActionInputs,
4
+ buildConfig,
5
+ severityMeetsThreshold,
6
+ } from "../src/config";
7
+ import { ReviewAgentConfig, Severity } from "../src/types";
8
+
9
+ describe("severityMeetsThreshold", () => {
10
+ it("returns true when severity equals threshold", () => {
11
+ expect(severityMeetsThreshold("critical", "critical")).toBe(true);
12
+ expect(severityMeetsThreshold("warning", "warning")).toBe(true);
13
+ expect(severityMeetsThreshold("info", "info")).toBe(true);
14
+ });
15
+
16
+ it("returns true when severity is above threshold", () => {
17
+ expect(severityMeetsThreshold("critical", "warning")).toBe(true);
18
+ expect(severityMeetsThreshold("critical", "info")).toBe(true);
19
+ expect(severityMeetsThreshold("warning", "info")).toBe(true);
20
+ });
21
+
22
+ it("returns false when severity is below threshold", () => {
23
+ expect(severityMeetsThreshold("warning", "critical")).toBe(false);
24
+ expect(severityMeetsThreshold("info", "critical")).toBe(false);
25
+ expect(severityMeetsThreshold("info", "warning")).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe("buildConfig", () => {
30
+ const baseInputs = {
31
+ githubToken: "ghp_test123",
32
+ llmProvider: "openai" as const,
33
+ llmApiKey: "sk-testapikey123",
34
+ llmModel: "gpt-4o",
35
+ llmBaseUrl: "",
36
+ configPath: ".reviewagent.yml",
37
+ severity: "warning" as Severity,
38
+ maxComments: 50,
39
+ reviewType: "comment" as const,
40
+ languageHints: [] as string[],
41
+ learnConventions: true,
42
+ };
43
+
44
+ it("builds config with defaults when no config file exists", () => {
45
+ const config = buildConfig(baseInputs, "/nonexistent/workspace");
46
+
47
+ expect(config.llm.provider).toBe("openai");
48
+ expect(config.llm.model).toBe("gpt-4o");
49
+ expect(config.review.severity).toBe("warning");
50
+ expect(config.review.maxComments).toBe(50);
51
+ expect(config.ignore.paths).toContain("node_modules/**");
52
+ expect(config.ignore.extensions).toContain(".png");
53
+ expect(config.rules).toEqual([]);
54
+ });
55
+
56
+ it("sets correct default base URL for ollama", () => {
57
+ const inputs = { ...baseInputs, llmProvider: "ollama" as const, llmApiKey: "" };
58
+ const config = buildConfig(inputs, "/nonexistent/workspace");
59
+
60
+ expect(config.llm.baseUrl).toBe("http://localhost:11434/v1");
61
+ });
62
+
63
+ it("preserves custom base URL when provided", () => {
64
+ const inputs = {
65
+ ...baseInputs,
66
+ llmBaseUrl: "https://custom.api.example.com/v1",
67
+ };
68
+ const config = buildConfig(inputs, "/nonexistent/workspace");
69
+
70
+ expect(config.llm.baseUrl).toBe("https://custom.api.example.com/v1");
71
+ });
72
+
73
+ it("includes default ignore patterns", () => {
74
+ const config = buildConfig(baseInputs, "/nonexistent/workspace");
75
+
76
+ expect(config.ignore.paths).toContain("node_modules/**");
77
+ expect(config.ignore.paths).toContain("dist/**");
78
+ expect(config.ignore.paths).toContain("**/*.min.js");
79
+ expect(config.ignore.paths).toContain("**/*.generated.*");
80
+ expect(config.ignore.extensions).toContain(".woff");
81
+ expect(config.ignore.extensions).toContain(".svg");
82
+ });
83
+ });
84
+
85
+ describe("parseActionInputs", () => {
86
+ // Note: These tests would require mocking @actions/core
87
+ // They validate the validation logic indirectly through buildConfig
88
+
89
+ it("rejects invalid provider", () => {
90
+ // We test the validation function logic directly
91
+ const validProviders = ["openai", "anthropic", "ollama"];
92
+ expect(validProviders.includes("invalid" as never)).toBe(false);
93
+ });
94
+
95
+ it("rejects invalid severity", () => {
96
+ const validSeverities: Severity[] = ["critical", "warning", "info"];
97
+ expect(validSeverities.includes("invalid" as Severity)).toBe(false);
98
+ });
99
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parsePatch,
4
+ getChangedLines,
5
+ shouldIgnoreFile,
6
+ getFileLanguage,
7
+ formatDiffForReview,
8
+ } from "../src/utils/diff-parser";
9
+ import { FileDiff, ReviewAgentConfig } from "../src/types";
10
+ import { SAMPLE_DIFF_SINGLE_FILE, SAMPLE_PATCH_MULTILINE } from "./fixtures/mock-data";
11
+
12
+ const mockConfig: ReviewAgentConfig = {
13
+ llm: { provider: "openai", apiKey: "test", model: "gpt-4o", baseUrl: "https://api.openai.com/v1" },
14
+ review: { severity: "warning", maxComments: 50, reviewType: "comment", languageHints: [], learnConventions: true },
15
+ ignore: {
16
+ paths: ["node_modules/**", "dist/**", "**/*.min.js", "**/*.generated.*"],
17
+ extensions: [".png", ".jpg", ".svg", ".woff"],
18
+ },
19
+ rules: [],
20
+ };
21
+
22
+ describe("parsePatch", () => {
23
+ it("parses a single hunk", () => {
24
+ const hunks = parsePatch(SAMPLE_DIFF_SINGLE_FILE.patch);
25
+ expect(hunks.length).toBeGreaterThanOrEqual(1);
26
+ expect(hunks[0].newStart).toBe(1);
27
+ expect(hunks[0].lines.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ it("returns empty array for empty patch", () => {
31
+ expect(parsePatch("")).toEqual([]);
32
+ });
33
+
34
+ it("parses multi-line patches correctly", () => {
35
+ const hunks = parsePatch(SAMPLE_PATCH_MULTILINE);
36
+ expect(hunks.length).toBe(1);
37
+ expect(hunks[0].oldStart).toBe(5);
38
+ expect(hunks[0].newStart).toBe(5);
39
+ });
40
+ });
41
+
42
+ describe("getChangedLines", () => {
43
+ it("returns only added lines with correct line numbers", () => {
44
+ const changed = getChangedLines(SAMPLE_DIFF_SINGLE_FILE.patch);
45
+ expect(changed.size).toBeGreaterThan(0);
46
+
47
+ const values = Array.from(changed.values());
48
+ const hasSecretLine = values.some((v) => v.includes("ADMIN_PASSWORD"));
49
+ expect(hasSecretLine).toBe(true);
50
+ });
51
+
52
+ it("returns empty map for empty patch", () => {
53
+ expect(getChangedLines("").size).toBe(0);
54
+ });
55
+ });
56
+
57
+ describe("shouldIgnoreFile", () => {
58
+ it("ignores node_modules paths", () => {
59
+ expect(shouldIgnoreFile("node_modules/foo/bar.js", mockConfig)).toBe(true);
60
+ });
61
+
62
+ it("ignores dist paths", () => {
63
+ expect(shouldIgnoreFile("dist/bundle.js", mockConfig)).toBe(true);
64
+ });
65
+
66
+ it("ignores minified files", () => {
67
+ expect(shouldIgnoreFile("vendor/lib.min.js", mockConfig)).toBe(true);
68
+ });
69
+
70
+ it("ignores generated files", () => {
71
+ expect(shouldIgnoreFile("src/api.generated.ts", mockConfig)).toBe(true);
72
+ });
73
+
74
+ it("ignores binary extensions", () => {
75
+ expect(shouldIgnoreFile("assets/logo.png", mockConfig)).toBe(true);
76
+ expect(shouldIgnoreFile("assets/icon.svg", mockConfig)).toBe(true);
77
+ });
78
+
79
+ it("does not ignore normal source files", () => {
80
+ expect(shouldIgnoreFile("src/auth.ts", mockConfig)).toBe(false);
81
+ expect(shouldIgnoreFile("src/utils.js", mockConfig)).toBe(false);
82
+ expect(shouldIgnoreFile("app.py", mockConfig)).toBe(false);
83
+ });
84
+ });
85
+
86
+ describe("getFileLanguage", () => {
87
+ it("detects TypeScript", () => {
88
+ expect(getFileLanguage("src/app.ts")).toBe("typescript");
89
+ expect(getFileLanguage("src/component.tsx")).toBe("typescript");
90
+ });
91
+
92
+ it("detects JavaScript", () => {
93
+ expect(getFileLanguage("src/index.js")).toBe("javascript");
94
+ expect(getFileLanguage("src/view.jsx")).toBe("javascript");
95
+ });
96
+
97
+ it("detects Python", () => {
98
+ expect(getFileLanguage("app.py")).toBe("python");
99
+ });
100
+
101
+ it("detects Go", () => {
102
+ expect(getFileLanguage("main.go")).toBe("go");
103
+ });
104
+
105
+ it("detects Rust", () => {
106
+ expect(getFileLanguage("src/main.rs")).toBe("rust");
107
+ });
108
+
109
+ it("returns unknown for unrecognized extensions", () => {
110
+ expect(getFileLanguage("data.xyz")).toBe("unknown");
111
+ });
112
+
113
+ it("detects Dockerfile", () => {
114
+ expect(getFileLanguage("Dockerfile")).toBe("dockerfile");
115
+ });
116
+
117
+ it("detects Makefile", () => {
118
+ expect(getFileLanguage("Makefile")).toBe("makefile");
119
+ });
120
+ });
121
+
122
+ describe("formatDiffForReview", () => {
123
+ it("includes filename in formatted output", () => {
124
+ const result = formatDiffForReview(SAMPLE_DIFF_SINGLE_FILE);
125
+ expect(result).toContain("src/auth.ts");
126
+ });
127
+
128
+ it("includes change stats", () => {
129
+ const result = formatDiffForReview(SAMPLE_DIFF_SINGLE_FILE);
130
+ expect(result).toContain("+8/-1");
131
+ });
132
+
133
+ it("includes diff content", () => {
134
+ const result = formatDiffForReview(SAMPLE_DIFF_SINGLE_FILE);
135
+ expect(result).toContain("@@");
136
+ });
137
+ });