@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 +21 -0
- package/README.md +352 -0
- package/__tests__/config.test.ts +99 -0
- package/__tests__/diff-parser.test.ts +137 -0
- package/__tests__/fixtures/mock-data.ts +120 -0
- package/__tests__/llm-client.test.ts +166 -0
- package/__tests__/rate-limiter.test.ts +95 -0
- package/__tests__/security-utils.test.ts +152 -0
- package/__tests__/security.test.ts +138 -0
- package/action.yml +65 -0
- package/dist/index.js +55824 -0
- package/dist/sourcemap-register.js +1 -0
- package/package.json +46 -0
- package/src/config.ts +201 -0
- package/src/conventions.ts +288 -0
- package/src/github.ts +180 -0
- package/src/llm-client.ts +210 -0
- package/src/main.ts +106 -0
- package/src/reviewer.ts +161 -0
- package/src/reviewers/security.ts +205 -0
- package/src/types.ts +114 -0
- package/src/utils/diff-parser.ts +177 -0
- package/src/utils/rate-limiter.ts +72 -0
- package/src/utils/security.ts +125 -0
- package/tsconfig.json +34 -0
- package/vitest.config.ts +16 -0
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
|
+
[](https://github.com/features/actions)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](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
|
+
});
|