@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.
@@ -0,0 +1,120 @@
1
+ import { FileDiff } from "../../src/types";
2
+
3
+ export const SAMPLE_DIFF_SINGLE_FILE: FileDiff = {
4
+ filename: "src/auth.ts",
5
+ patch: `@@ -1,5 +1,12 @@
6
+ import { Request, Response } from 'express';
7
+ -import { verifyToken } from './jwt';
8
+ +import { verifyToken } from './jwt';
9
+ +
10
+ +const ADMIN_PASSWORD = "super_secret_123";
11
+ +
12
+ +function login(req: Request, res: Response) {
13
+ + const query = "SELECT * FROM users WHERE name = '" + req.body.username + "'";
14
+ + db.query(query);
15
+ + if (req.body.password === ADMIN_PASSWORD) {
16
+ + res.redirect(req.query.returnUrl);
17
+ }
18
+ +}`,
19
+ additions: 8,
20
+ deletions: 1,
21
+ changeType: "modified",
22
+ };
23
+
24
+ export const SAMPLE_DIFF_MULTI_FILE: FileDiff[] = [
25
+ {
26
+ filename: "src/auth.ts",
27
+ patch: `@@ -1,3 +1,8 @@
28
+ +const API_KEY = "sk-1234567890abcdef";
29
+ +
30
+ +function getUser(id) {
31
+ + return eval("users[" + id + "]");
32
+ +}`,
33
+ additions: 5,
34
+ deletions: 0,
35
+ changeType: "added",
36
+ },
37
+ {
38
+ filename: "src/utils.ts",
39
+ patch: `@@ -10,3 +10,5 @@
40
+ export function formatName(name: string): string {
41
+ - return name.trim();
42
+ + // TODO: fix security issue with name handling
43
+ + return name.trim();
44
+ }`,
45
+ additions: 2,
46
+ deletions: 1,
47
+ changeType: "modified",
48
+ },
49
+ {
50
+ filename: "package-lock.json",
51
+ patch: `@@ -1,1 +1,2 @@
52
+ +{"locked": true}`,
53
+ additions: 1,
54
+ deletions: 0,
55
+ changeType: "modified",
56
+ },
57
+ {
58
+ filename: "src/styles.css",
59
+ patch: `@@ -1,1 +1,2 @@
60
+ +.button { color: red; }`,
61
+ additions: 1,
62
+ deletions: 0,
63
+ changeType: "modified",
64
+ },
65
+ ];
66
+
67
+ export const SAMPLE_PATCH_MULTILINE = `@@ -5,10 +5,15 @@
68
+ function processData(data: any) {
69
+ - return data;
70
+ + if (!data) {
71
+ + return null;
72
+ + }
73
+ + console.log("Processing data with password", data.password);
74
+ + const result = eval(data.formula);
75
+ + return result;
76
+ }`;
77
+
78
+ export const MOCK_LLM_RESPONSE = {
79
+ comments: [
80
+ {
81
+ line: 6,
82
+ severity: "critical" as const,
83
+ category: "security" as const,
84
+ message: "The eval() call allows arbitrary code execution. Never eval user input.",
85
+ },
86
+ {
87
+ line: 5,
88
+ severity: "critical" as const,
89
+ category: "security" as const,
90
+ message: "Avoid logging sensitive data like passwords.",
91
+ },
92
+ {
93
+ line: 3,
94
+ severity: "warning" as const,
95
+ category: "bug" as const,
96
+ message: "Returning null instead of throwing or providing a default may cause downstream errors.",
97
+ },
98
+ ],
99
+ score: 35,
100
+ summary: "Critical security issues found: eval() usage and hardcoded secrets. Score: 35/100",
101
+ };
102
+
103
+ export const MOCK_PR_PAYLOAD = {
104
+ action: "opened",
105
+ number: 42,
106
+ pull_request: {
107
+ number: 42,
108
+ head: {
109
+ sha: "abc123def456789012345678901234567890abcd",
110
+ ref: "feature/review-agent",
111
+ },
112
+ base: {
113
+ ref: "main",
114
+ },
115
+ },
116
+ repository: {
117
+ owner: { login: "testowner" },
118
+ name: "testrepo",
119
+ },
120
+ };
@@ -0,0 +1,166 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { LLMClient } from "../src/llm-client";
3
+ import { ReviewAgentConfig, FileDiff } from "../src/types";
4
+
5
+ const mockConfig: ReviewAgentConfig = {
6
+ llm: {
7
+ provider: "openai",
8
+ apiKey: "test-key-12345678",
9
+ model: "gpt-4o",
10
+ baseUrl: "https://api.openai.com/v1",
11
+ },
12
+ review: {
13
+ severity: "info",
14
+ maxComments: 50,
15
+ reviewType: "comment",
16
+ languageHints: [],
17
+ learnConventions: true,
18
+ },
19
+ ignore: { paths: [], extensions: [] },
20
+ rules: [],
21
+ };
22
+
23
+ const mockDiff: FileDiff = {
24
+ filename: "src/app.ts",
25
+ patch: `@@ -1,3 +1,5 @@
26
+ import express from 'express';
27
+ +const app = express();
28
+ +app.listen(3000);`,
29
+ additions: 2,
30
+ deletions: 0,
31
+ changeType: "modified",
32
+ };
33
+
34
+ describe("LLMClient", () => {
35
+ let client: LLMClient;
36
+
37
+ beforeEach(() => {
38
+ client = new LLMClient(mockConfig);
39
+ });
40
+
41
+ it("creates client with provided config", () => {
42
+ expect(client).toBeDefined();
43
+ });
44
+
45
+ it("parses valid JSON response correctly", async () => {
46
+ // Access the private parseResponse method via any
47
+ const parseResponse = (client as any).parseResponse.bind(client);
48
+
49
+ const validResponse = JSON.stringify({
50
+ comments: [
51
+ {
52
+ line: 2,
53
+ severity: "warning",
54
+ category: "performance",
55
+ message: "Consider adding error handling.",
56
+ },
57
+ ],
58
+ score: 80,
59
+ summary: "Good code with minor issues.",
60
+ });
61
+
62
+ const result = parseResponse(validResponse);
63
+ expect(result.comments).toHaveLength(1);
64
+ expect(result.comments[0].line).toBe(2);
65
+ expect(result.score).toBe(80);
66
+ expect(result.summary).toBe("Good code with minor issues.");
67
+ });
68
+
69
+ it("handles empty comments array", async () => {
70
+ const parseResponse = (client as any).parseResponse.bind(client);
71
+
72
+ const response = JSON.stringify({
73
+ comments: [],
74
+ score: 95,
75
+ summary: "Clean code!",
76
+ });
77
+
78
+ const result = parseResponse(response);
79
+ expect(result.comments).toEqual([]);
80
+ expect(result.score).toBe(95);
81
+ });
82
+
83
+ it("handles malformed JSON response gracefully", async () => {
84
+ const parseResponse = (client as any).parseResponse.bind(client);
85
+
86
+ const result = parseResponse("not valid json");
87
+ expect(result.comments).toEqual([]);
88
+ expect(result.score).toBe(75);
89
+ });
90
+
91
+ it("clamps score to 0-100 range", async () => {
92
+ const parseResponse = (client as any).parseResponse.bind(client);
93
+
94
+ const result1 = parseResponse(
95
+ JSON.stringify({ comments: [], score: 150, summary: "" })
96
+ );
97
+ expect(result1.score).toBe(100);
98
+
99
+ const result2 = parseResponse(
100
+ JSON.stringify({ comments: [], score: -10, summary: "" })
101
+ );
102
+ expect(result2.score).toBe(0);
103
+ });
104
+
105
+ it("filters out comments with missing required fields", async () => {
106
+ const parseResponse = (client as any).parseResponse.bind(client);
107
+
108
+ const response = JSON.stringify({
109
+ comments: [
110
+ { line: 1, severity: "warning", category: "bug", message: "valid" },
111
+ { line: 2, severity: "warning" }, // missing category and message
112
+ { severity: "info", category: "style", message: "no line" }, // missing line
113
+ { line: "not a number", severity: "info", category: "style", message: "bad line" },
114
+ ],
115
+ score: 60,
116
+ summary: "Some issues.",
117
+ });
118
+
119
+ const result = parseResponse(response);
120
+ expect(result.comments).toHaveLength(1);
121
+ expect(result.comments[0].message).toBe("valid");
122
+ });
123
+
124
+ it("builds system prompt with conventions", () => {
125
+ const buildSystemPrompt = (client as any).buildSystemPrompt.bind(client);
126
+
127
+ const prompt = buildSystemPrompt(
128
+ "typescript",
129
+ [
130
+ {
131
+ language: "typescript",
132
+ patterns: ["ES module imports"],
133
+ namingStyle: "camelCase",
134
+ examples: ["import { foo } from './bar'"],
135
+ },
136
+ ],
137
+ mockConfig
138
+ );
139
+
140
+ expect(prompt).toContain("code reviewer");
141
+ expect(prompt).toContain("typescript");
142
+ expect(prompt).toContain("camelCase");
143
+ expect(prompt).toContain("ES module imports");
144
+ });
145
+
146
+ it("builds system prompt with custom rules", () => {
147
+ const buildSystemPrompt = (client as any).buildSystemPrompt.bind(client);
148
+
149
+ const configWithRules: ReviewAgentConfig = {
150
+ ...mockConfig,
151
+ rules: [
152
+ {
153
+ name: "no-console",
154
+ pattern: "console\\.log",
155
+ message: "Use logger instead of console.log",
156
+ severity: "warning",
157
+ category: "convention",
158
+ },
159
+ ],
160
+ };
161
+
162
+ const prompt = buildSystemPrompt("javascript", [], configWithRules);
163
+ expect(prompt).toContain("no-console");
164
+ expect(prompt).toContain("logger instead");
165
+ });
166
+ });
@@ -0,0 +1,95 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { RateLimiter, RetryHandler } from "../src/utils/rate-limiter";
3
+
4
+ describe("RateLimiter", () => {
5
+ it("allows requests within concurrency limit", async () => {
6
+ const limiter = new RateLimiter(3, 0);
7
+
8
+ const p1 = limiter.acquire();
9
+ const p2 = limiter.acquire();
10
+ const p3 = limiter.acquire();
11
+
12
+ await Promise.all([p1, p2, p3]);
13
+
14
+ expect(limiter.activeRequests).toBe(3);
15
+
16
+ limiter.release();
17
+ limiter.release();
18
+ limiter.release();
19
+
20
+ expect(limiter.activeRequests).toBe(0);
21
+ });
22
+
23
+ it("queues requests over concurrency limit", async () => {
24
+ const limiter = new RateLimiter(1, 0);
25
+ const order: number[] = [];
26
+
27
+ const run = async (id: number) => {
28
+ await limiter.acquire();
29
+ order.push(id);
30
+ limiter.release();
31
+ };
32
+
33
+ await Promise.all([run(1), run(2), run(3)]);
34
+
35
+ expect(order).toEqual([1, 2, 3]);
36
+ });
37
+
38
+ it("reports pending count correctly", () => {
39
+ const limiter = new RateLimiter(1, 100000);
40
+
41
+ expect(limiter.pendingCount).toBe(0);
42
+ expect(limiter.activeRequests).toBe(0);
43
+ });
44
+
45
+ it("respects minimum interval between calls", async () => {
46
+ const limiter = new RateLimiter(2, 50);
47
+ const timestamps: number[] = [];
48
+
49
+ const measure = async () => {
50
+ await limiter.acquire();
51
+ timestamps.push(Date.now());
52
+ // Hold for a moment
53
+ await new Promise<void>((r) => setTimeout(r, 10));
54
+ limiter.release();
55
+ };
56
+
57
+ await Promise.all([measure(), measure()]);
58
+
59
+ // Both should have completed
60
+ expect(timestamps.length).toBe(2);
61
+ });
62
+ });
63
+
64
+ describe("RetryHandler", () => {
65
+ it("returns result on first success", async () => {
66
+ const result = await RetryHandler.withRetry(() => Promise.resolve(42));
67
+ expect(result).toBe(42);
68
+ });
69
+
70
+ it("retries on failure and eventually succeeds", async () => {
71
+ let attempts = 0;
72
+ const result = await RetryHandler.withRetry(() => {
73
+ attempts++;
74
+ if (attempts < 3) {
75
+ throw new Error("fail");
76
+ }
77
+ return Promise.resolve("success");
78
+ }, 3, 10);
79
+
80
+ expect(result).toBe("success");
81
+ expect(attempts).toBe(3);
82
+ });
83
+
84
+ it("throws after max retries exceeded", async () => {
85
+ await expect(
86
+ RetryHandler.withRetry(() => Promise.reject(new Error("always fail")), 2, 10)
87
+ ).rejects.toThrow("always fail");
88
+ });
89
+
90
+ it("works with zero retries", async () => {
91
+ await expect(
92
+ RetryHandler.withRetry(() => Promise.reject(new Error("fail")), 0, 10)
93
+ ).rejects.toThrow("fail");
94
+ });
95
+ });
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ sanitizeForLog,
4
+ truncateString,
5
+ validateApiKey,
6
+ formatCommentBody,
7
+ buildSummaryComment,
8
+ } from "../src/utils/security";
9
+
10
+ describe("sanitizeForLog", () => {
11
+ it("redacts GitHub tokens", () => {
12
+ const input = 'token=ghp_1234567890abcdefghijklmnopqrstuvwxyz';
13
+ const result = sanitizeForLog(input);
14
+ expect(result).not.toContain("ghp_1234567890");
15
+ expect(result).toContain("[REDACTED]");
16
+ });
17
+
18
+ it("redacts OpenAI API keys", () => {
19
+ const input = 'key=sk-abcdefghijklmnopqrstuvwxyz1234567890';
20
+ const result = sanitizeForLog(input);
21
+ expect(result).not.toContain("sk-abcdefghijklmno");
22
+ expect(result).toContain("[REDACTED]");
23
+ });
24
+
25
+ it("redacts generic password assignments", () => {
26
+ const input = 'password: "mySecretPassword123"';
27
+ const result = sanitizeForLog(input);
28
+ expect(result).not.toContain("mySecretPassword123");
29
+ expect(result).toContain("[REDACTED]");
30
+ });
31
+
32
+ it("redacts JWT tokens", () => {
33
+ const input = "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.abc123";
34
+ const result = sanitizeForLog(input);
35
+ expect(result).toContain("[REDACTED]");
36
+ });
37
+
38
+ it("leaves safe strings untouched", () => {
39
+ const input = "User logged in successfully from 192.168.1.1";
40
+ const result = sanitizeForLog(input);
41
+ expect(result).toBe(input);
42
+ });
43
+
44
+ it("handles empty string", () => {
45
+ expect(sanitizeForLog("")).toBe("");
46
+ });
47
+ });
48
+
49
+ describe("truncateString", () => {
50
+ it("does not truncate strings within limit", () => {
51
+ expect(truncateString("hello", 10)).toBe("hello");
52
+ });
53
+
54
+ it("truncates long strings", () => {
55
+ const result = truncateString("a".repeat(100), 50);
56
+ expect(result.length).toBeLessThan(100);
57
+ expect(result).toContain("truncated");
58
+ });
59
+
60
+ it("handles exact length", () => {
61
+ expect(truncateString("hello", 5)).toBe("hello");
62
+ });
63
+ });
64
+
65
+ describe("validateApiKey", () => {
66
+ it("throws for missing OpenAI key", () => {
67
+ expect(() => validateApiKey("", "openai")).toThrow("required");
68
+ });
69
+
70
+ it("throws for missing Anthropic key", () => {
71
+ expect(() => validateApiKey("", "anthropic")).toThrow("required");
72
+ });
73
+
74
+ it("allows Ollama without key", () => {
75
+ expect(() => validateApiKey("", "ollama")).not.toThrow();
76
+ });
77
+
78
+ it("throws for very short key", () => {
79
+ expect(() => validateApiKey("abc", "openai")).toThrow("too short");
80
+ });
81
+
82
+ it("accepts valid key", () => {
83
+ expect(() => validateApiKey("sk-validapikey123456", "openai")).not.toThrow();
84
+ });
85
+ });
86
+
87
+ describe("formatCommentBody", () => {
88
+ it("includes severity emoji", () => {
89
+ const result = formatCommentBody("test message", "critical", "security");
90
+ expect(result).toContain("🔴");
91
+ });
92
+
93
+ it("includes category label", () => {
94
+ const result = formatCommentBody("test message", "warning", "performance");
95
+ expect(result).toContain("Performance");
96
+ });
97
+
98
+ it("includes the message", () => {
99
+ const result = formatCommentBody("Fix this bug", "info", "bug");
100
+ expect(result).toContain("Fix this bug");
101
+ });
102
+
103
+ it("includes ReviewAgent attribution", () => {
104
+ const result = formatCommentBody("test", "info", "style");
105
+ expect(result).toContain("ReviewAgent");
106
+ });
107
+ });
108
+
109
+ describe("buildSummaryComment", () => {
110
+ it("builds a summary with score and breakdown", () => {
111
+ const result = buildSummaryComment(
112
+ 75,
113
+ { bug: 2, security: 1, performance: 0, style: 3, convention: 0 },
114
+ "Found some issues.",
115
+ 5,
116
+ 6
117
+ );
118
+
119
+ expect(result).toContain("75/100");
120
+ expect(result).toContain("Bug");
121
+ expect(result).toContain("Security");
122
+ expect(result).toContain("Style");
123
+ expect(result).toContain("5");
124
+ expect(result).toContain("6");
125
+ });
126
+
127
+ it("handles clean review with no issues", () => {
128
+ const result = buildSummaryComment(
129
+ 100,
130
+ { bug: 0, security: 0, performance: 0, style: 0, convention: 0 },
131
+ "All good!",
132
+ 3,
133
+ 0
134
+ );
135
+
136
+ expect(result).toContain("100/100");
137
+ expect(result).toContain("Great");
138
+ expect(result).toContain("No issues found");
139
+ });
140
+
141
+ it("shows appropriate label for low scores", () => {
142
+ const result = buildSummaryComment(
143
+ 25,
144
+ { bug: 5, security: 3, performance: 2, style: 1, convention: 1 },
145
+ "Major issues.",
146
+ 10,
147
+ 12
148
+ );
149
+
150
+ expect(result).toContain("Poor");
151
+ });
152
+ });
@@ -0,0 +1,138 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { scanForSecurityIssues } from "../src/reviewers/security";
3
+ import { FileDiff, ReviewAgentConfig } from "../src/types";
4
+
5
+ const mockConfig: ReviewAgentConfig = {
6
+ llm: { provider: "openai", apiKey: "test", model: "gpt-4o", baseUrl: "" },
7
+ review: { severity: "info", maxComments: 50, reviewType: "comment", languageHints: [], learnConventions: true },
8
+ ignore: { paths: [], extensions: [] },
9
+ rules: [],
10
+ };
11
+
12
+ function makeDiff(patch: string, filename = "src/code.ts"): FileDiff {
13
+ return {
14
+ filename,
15
+ patch,
16
+ additions: 5,
17
+ deletions: 0,
18
+ changeType: "modified",
19
+ };
20
+ }
21
+
22
+ describe("scanForSecurityIssues", () => {
23
+ it("detects eval() usage", () => {
24
+ const diff = makeDiff(`@@ -1 +1,2 @@
25
+ +const result = eval(userInput);`);
26
+ const issues = scanForSecurityIssues(diff, mockConfig);
27
+ expect(issues.length).toBeGreaterThanOrEqual(1);
28
+ expect(issues.some((i) => i.body.includes("eval()"))).toBe(true);
29
+ expect(issues.some((i) => i.category === "security")).toBe(true);
30
+ });
31
+
32
+ it("detects SQL injection patterns", () => {
33
+ const diff = makeDiff(`@@ -1 +1,2 @@
34
+ +const query = "SELECT * FROM users WHERE id = " + userId;
35
+ +db.query(query);`);
36
+ const issues = scanForSecurityIssues(diff, mockConfig);
37
+ expect(issues.some((i) => i.body.toLowerCase().includes("sql"))).toBe(true);
38
+ });
39
+
40
+ it("detects hardcoded secrets", () => {
41
+ const diff = makeDiff(`@@ -1 +1,2 @@
42
+ +const password = "my_super_secret_password";`);
43
+ const issues = scanForSecurityIssues(diff, mockConfig);
44
+ expect(issues.some((i) => i.body.includes("Hardcoded secret"))).toBe(true);
45
+ expect(issues.some((i) => i.severity === "critical")).toBe(true);
46
+ });
47
+
48
+ it("detects innerHTML assignment", () => {
49
+ const diff = makeDiff(`@@ -1 +1,2 @@
50
+ +element.innerHTML = userInput;`);
51
+ const issues = scanForSecurityIssues(diff, mockConfig);
52
+ expect(issues.some((i) => i.body.includes("innerHTML"))).toBe(true);
53
+ });
54
+
55
+ it("detects unsafe redirects", () => {
56
+ const diff = makeDiff(`@@ -1 +1,2 @@
57
+ +res.redirect(req.query.returnUrl);`);
58
+ const issues = scanForSecurityIssues(diff, mockConfig);
59
+ expect(issues.some((i) => i.body.includes("redirect"))).toBe(true);
60
+ });
61
+
62
+ it("detects empty catch blocks", () => {
63
+ const diff = makeDiff(`@@ -1 +1,3 @@
64
+ +try { doSomething(); } catch(e) {}`);
65
+ const issues = scanForSecurityIssues(diff, mockConfig);
66
+ expect(issues.some((i) => i.body.includes("Empty catch"))).toBe(true);
67
+ });
68
+
69
+ it("detects console.log with sensitive data", () => {
70
+ const diff = makeDiff(`@@ -1 +1,2 @@
71
+ +console.log("User token:", user.token);`);
72
+ const issues = scanForSecurityIssues(diff, mockConfig);
73
+ expect(issues.some((i) => i.body.includes("sensitive data"))).toBe(true);
74
+ });
75
+
76
+ it("detects exec/spawn with string concatenation", () => {
77
+ const diff = makeDiff(`@@ -1 +1,2 @@
78
+ +exec("ls " + userInput);`);
79
+ const issues = scanForSecurityIssues(diff, mockConfig);
80
+ expect(issues.some((i) => i.body.includes("command injection"))).toBe(true);
81
+ });
82
+
83
+ it("does not flag clean code", () => {
84
+ const diff = makeDiff(`@@ -1 +1,3 @@
85
+ +function add(a: number, b: number): number {
86
+ + return a + b;
87
+ +}`);
88
+ const issues = scanForSecurityIssues(diff, mockConfig);
89
+ expect(issues).toEqual([]);
90
+ });
91
+
92
+ it("respects severity threshold", () => {
93
+ const strictConfig: ReviewAgentConfig = {
94
+ ...mockConfig,
95
+ review: { ...mockConfig.review, severity: "critical" },
96
+ };
97
+ const diff = makeDiff(`@@ -1 +1,2 @@
98
+ +console.log("User token:", user.token);`);
99
+ const issues = scanForSecurityIssues(diff, strictConfig);
100
+ // console.log with sensitive is critical, should still be caught
101
+ expect(issues.some((i) => i.severity === "critical")).toBe(true);
102
+ });
103
+
104
+ it("detects HTTP URLs in fetch", () => {
105
+ const diff = makeDiff(`@@ -1 +1,2 @@
106
+ +fetch("http://api.example.com/data");`);
107
+ const issues = scanForSecurityIssues(diff, mockConfig);
108
+ expect(issues.some((i) => i.body.includes("HTTPS"))).toBe(true);
109
+ });
110
+
111
+ it("applies custom security rules", () => {
112
+ const configWithRules: ReviewAgentConfig = {
113
+ ...mockConfig,
114
+ rules: [
115
+ {
116
+ name: "no-process-env",
117
+ pattern: "process\\.env\\.SECRET",
118
+ message: "Use config module instead of process.env.SECRET",
119
+ severity: "critical",
120
+ category: "security",
121
+ },
122
+ ],
123
+ };
124
+ const diff = makeDiff(`@@ -1 +1,2 @@
125
+ +const key = process.env.SECRET;`);
126
+ const issues = scanForSecurityIssues(diff, configWithRules);
127
+ expect(issues.some((i) => i.body.includes("config module"))).toBe(true);
128
+ });
129
+
130
+ it("ignores removed lines", () => {
131
+ const diff = makeDiff(`@@ -1,2 +1 @@
132
+ -const password = "old_secret";
133
+ -const result = eval(oldCode);`);
134
+ const issues = scanForSecurityIssues(diff, mockConfig);
135
+ // Removed lines should not be flagged
136
+ expect(issues).toEqual([]);
137
+ });
138
+ });