cc-reviewer 1.0.4 → 1.1.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,4 @@
1
+ /**
2
+ * Tests for pipeline.ts - verification and security features
3
+ */
4
+ export {};
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Tests for pipeline.ts - verification and security features
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'fs';
6
+ import { join } from 'path';
7
+ import { tmpdir } from 'os';
8
+ import { FileCache, verifyFinding } from '../pipeline.js';
9
+ // =============================================================================
10
+ // TEST SETUP
11
+ // =============================================================================
12
+ const TEST_DIR = join(tmpdir(), 'pipeline-test-' + Date.now());
13
+ function createTestFile(relativePath, content) {
14
+ const fullPath = join(TEST_DIR, relativePath);
15
+ const dir = fullPath.substring(0, fullPath.lastIndexOf('/'));
16
+ if (dir && !existsSync(dir)) {
17
+ mkdirSync(dir, { recursive: true });
18
+ }
19
+ writeFileSync(fullPath, content);
20
+ }
21
+ function createTestFinding(overrides = {}) {
22
+ return {
23
+ id: 'test-1',
24
+ category: 'correctness',
25
+ severity: 'medium',
26
+ confidence: 0.8,
27
+ title: 'Test finding',
28
+ description: 'Test description',
29
+ ...overrides,
30
+ };
31
+ }
32
+ // =============================================================================
33
+ // FILE CACHE TESTS
34
+ // =============================================================================
35
+ describe('FileCache', () => {
36
+ beforeEach(() => {
37
+ mkdirSync(TEST_DIR, { recursive: true });
38
+ createTestFile('existing.ts', 'line 1\nline 2\nline 3\n');
39
+ createTestFile('subdir/nested.ts', 'nested content');
40
+ });
41
+ afterEach(() => {
42
+ rmSync(TEST_DIR, { recursive: true, force: true });
43
+ });
44
+ it('should return true for existing files', () => {
45
+ const cache = new FileCache(TEST_DIR);
46
+ expect(cache.exists('existing.ts')).toBe(true);
47
+ });
48
+ it('should return false for non-existing files', () => {
49
+ const cache = new FileCache(TEST_DIR);
50
+ expect(cache.exists('nonexistent.ts')).toBe(false);
51
+ });
52
+ it('should cache file existence checks', () => {
53
+ const cache = new FileCache(TEST_DIR);
54
+ // First call
55
+ cache.exists('existing.ts');
56
+ cache.exists('nonexistent.ts');
57
+ // Stats should show 1 file checked (non-existent cached as null)
58
+ const stats = cache.getStats();
59
+ expect(stats.filesChecked).toBe(1); // Only non-existent is cached on exists()
60
+ });
61
+ it('should return file content', () => {
62
+ const cache = new FileCache(TEST_DIR);
63
+ const content = cache.getContent('existing.ts');
64
+ expect(content).toBe('line 1\nline 2\nline 3\n');
65
+ });
66
+ it('should return null for non-existing file content', () => {
67
+ const cache = new FileCache(TEST_DIR);
68
+ const content = cache.getContent('nonexistent.ts');
69
+ expect(content).toBeNull();
70
+ });
71
+ it('should cache file content', () => {
72
+ const cache = new FileCache(TEST_DIR);
73
+ // Read twice
74
+ cache.getContent('existing.ts');
75
+ cache.getContent('existing.ts');
76
+ const stats = cache.getStats();
77
+ expect(stats.filesLoaded).toBe(1);
78
+ });
79
+ it('should return lines array', () => {
80
+ const cache = new FileCache(TEST_DIR);
81
+ const lines = cache.getLines('existing.ts');
82
+ expect(lines).toEqual(['line 1', 'line 2', 'line 3', '']);
83
+ });
84
+ it('should return correct line count', () => {
85
+ const cache = new FileCache(TEST_DIR);
86
+ const count = cache.getLineCount('existing.ts');
87
+ expect(count).toBe(4); // 3 lines + empty line from trailing newline
88
+ });
89
+ it('should handle nested paths', () => {
90
+ const cache = new FileCache(TEST_DIR);
91
+ expect(cache.exists('subdir/nested.ts')).toBe(true);
92
+ expect(cache.getContent('subdir/nested.ts')).toBe('nested content');
93
+ });
94
+ });
95
+ // =============================================================================
96
+ // PATH TRAVERSAL TESTS
97
+ // =============================================================================
98
+ describe('Path Traversal Protection', () => {
99
+ beforeEach(() => {
100
+ mkdirSync(TEST_DIR, { recursive: true });
101
+ createTestFile('safe.ts', 'safe content\nline 2\nline 3');
102
+ });
103
+ afterEach(() => {
104
+ rmSync(TEST_DIR, { recursive: true, force: true });
105
+ });
106
+ it('should block ../../../etc/passwd traversal', async () => {
107
+ const finding = createTestFinding({
108
+ location: { file: '../../../etc/passwd', line_start: 1 },
109
+ });
110
+ const result = await verifyFinding(finding, TEST_DIR);
111
+ expect(result.verification.fileExists).toBe(false);
112
+ expect(result.verification.verificationNotes).toContain('Path traversal blocked');
113
+ expect(result.adjustedConfidence).toBeLessThan(0.1);
114
+ });
115
+ it('should block absolute path /etc/passwd', async () => {
116
+ const finding = createTestFinding({
117
+ location: { file: '/etc/passwd', line_start: 1 },
118
+ });
119
+ const result = await verifyFinding(finding, TEST_DIR);
120
+ expect(result.verification.fileExists).toBe(false);
121
+ expect(result.verification.verificationNotes).toContain('Path traversal blocked');
122
+ });
123
+ it('should block ../ at start of path', async () => {
124
+ const finding = createTestFinding({
125
+ location: { file: '../sibling/file.ts', line_start: 1 },
126
+ });
127
+ const result = await verifyFinding(finding, TEST_DIR);
128
+ expect(result.verification.fileExists).toBe(false);
129
+ expect(result.verification.verificationNotes).toContain('Path traversal blocked');
130
+ });
131
+ it('should allow valid relative paths', async () => {
132
+ const finding = createTestFinding({
133
+ location: { file: 'safe.ts', line_start: 1 },
134
+ });
135
+ const result = await verifyFinding(finding, TEST_DIR);
136
+ expect(result.verification.fileExists).toBe(true);
137
+ expect(result.verification.lineValid).toBe(true);
138
+ });
139
+ it('should allow paths with ./ prefix', async () => {
140
+ const finding = createTestFinding({
141
+ location: { file: './safe.ts', line_start: 1 },
142
+ });
143
+ const result = await verifyFinding(finding, TEST_DIR);
144
+ expect(result.verification.fileExists).toBe(true);
145
+ });
146
+ });
147
+ // =============================================================================
148
+ // VERIFY FINDING TESTS
149
+ // =============================================================================
150
+ describe('verifyFinding', () => {
151
+ beforeEach(() => {
152
+ mkdirSync(TEST_DIR, { recursive: true });
153
+ createTestFile('test.ts', 'const x = 1;\nconst y = 2;\nconst z = 3;');
154
+ });
155
+ afterEach(() => {
156
+ rmSync(TEST_DIR, { recursive: true, force: true });
157
+ });
158
+ it('should verify existing file without location', async () => {
159
+ const finding = createTestFinding(); // No location
160
+ const result = await verifyFinding(finding, TEST_DIR);
161
+ expect(result.verification.fileExists).toBe(true);
162
+ expect(result.verification.lineValid).toBe(true);
163
+ });
164
+ it('should verify existing file with valid line', async () => {
165
+ const finding = createTestFinding({
166
+ location: { file: 'test.ts', line_start: 2 },
167
+ });
168
+ const result = await verifyFinding(finding, TEST_DIR);
169
+ expect(result.verification.fileExists).toBe(true);
170
+ expect(result.verification.lineValid).toBe(true);
171
+ });
172
+ it('should detect non-existing file', async () => {
173
+ const finding = createTestFinding({
174
+ location: { file: 'nonexistent.ts', line_start: 1 },
175
+ });
176
+ const result = await verifyFinding(finding, TEST_DIR);
177
+ expect(result.verification.fileExists).toBe(false);
178
+ expect(result.adjustedConfidence).toBeLessThan(finding.confidence);
179
+ });
180
+ it('should detect invalid line number', async () => {
181
+ const finding = createTestFinding({
182
+ location: { file: 'test.ts', line_start: 999 },
183
+ });
184
+ const result = await verifyFinding(finding, TEST_DIR);
185
+ expect(result.verification.fileExists).toBe(true);
186
+ expect(result.verification.lineValid).toBe(false);
187
+ expect(result.verification.verificationNotes).toContain('exceeds file length');
188
+ });
189
+ it('should verify matching evidence', async () => {
190
+ const finding = createTestFinding({
191
+ location: { file: 'test.ts', line_start: 2 },
192
+ evidence: 'const y = 2',
193
+ });
194
+ const result = await verifyFinding(finding, TEST_DIR);
195
+ expect(result.verification.codeSnippetMatches).toBe(true);
196
+ expect(result.adjustedConfidence).toBeGreaterThanOrEqual(finding.confidence);
197
+ });
198
+ it('should detect non-matching evidence', async () => {
199
+ const finding = createTestFinding({
200
+ location: { file: 'test.ts', line_start: 2 },
201
+ evidence: 'completely different code',
202
+ });
203
+ const result = await verifyFinding(finding, TEST_DIR);
204
+ expect(result.verification.codeSnippetMatches).toBe(false);
205
+ expect(result.adjustedConfidence).toBeLessThan(finding.confidence);
206
+ });
207
+ it('should use cache when provided', async () => {
208
+ const cache = new FileCache(TEST_DIR);
209
+ const finding = createTestFinding({
210
+ location: { file: 'test.ts', line_start: 1 },
211
+ });
212
+ // Verify multiple times with same cache
213
+ await verifyFinding(finding, TEST_DIR, cache);
214
+ await verifyFinding(finding, TEST_DIR, cache);
215
+ await verifyFinding(finding, TEST_DIR, cache);
216
+ const stats = cache.getStats();
217
+ expect(stats.filesLoaded).toBe(1);
218
+ });
219
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for schema.ts - JSON Schema and Zod validation consistency
3
+ */
4
+ export {};
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Tests for schema.ts - JSON Schema and Zod validation consistency
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { ReviewFinding, CodeLocation, getReviewOutputJsonSchema, parseReviewOutput, } from '../schema.js';
6
+ // =============================================================================
7
+ // ZOD SCHEMA TESTS
8
+ // =============================================================================
9
+ describe('CodeLocation Schema', () => {
10
+ it('should require file field', () => {
11
+ const result = CodeLocation.safeParse({});
12
+ expect(result.success).toBe(false);
13
+ });
14
+ it('should accept file-only location', () => {
15
+ const result = CodeLocation.safeParse({ file: 'test.ts' });
16
+ expect(result.success).toBe(true);
17
+ });
18
+ it('should accept full location', () => {
19
+ const result = CodeLocation.safeParse({
20
+ file: 'test.ts',
21
+ line_start: 10,
22
+ line_end: 20,
23
+ column_start: 0,
24
+ column_end: 50,
25
+ });
26
+ expect(result.success).toBe(true);
27
+ });
28
+ it('should reject negative line numbers', () => {
29
+ const result = CodeLocation.safeParse({
30
+ file: 'test.ts',
31
+ line_start: -1,
32
+ });
33
+ expect(result.success).toBe(false);
34
+ });
35
+ });
36
+ describe('ReviewFinding Schema', () => {
37
+ const validFinding = {
38
+ id: 'find-1',
39
+ category: 'security',
40
+ severity: 'high',
41
+ confidence: 0.9,
42
+ title: 'SQL Injection',
43
+ description: 'User input is not sanitized',
44
+ };
45
+ it('should accept valid finding', () => {
46
+ const result = ReviewFinding.safeParse(validFinding);
47
+ expect(result.success).toBe(true);
48
+ });
49
+ it('should accept finding with location', () => {
50
+ const result = ReviewFinding.safeParse({
51
+ ...validFinding,
52
+ location: { file: 'db.ts', line_start: 42 },
53
+ });
54
+ expect(result.success).toBe(true);
55
+ });
56
+ it('should reject invalid severity', () => {
57
+ const result = ReviewFinding.safeParse({
58
+ ...validFinding,
59
+ severity: 'extreme', // invalid
60
+ });
61
+ expect(result.success).toBe(false);
62
+ });
63
+ it('should reject confidence > 1', () => {
64
+ const result = ReviewFinding.safeParse({
65
+ ...validFinding,
66
+ confidence: 1.5,
67
+ });
68
+ expect(result.success).toBe(false);
69
+ });
70
+ it('should reject confidence < 0', () => {
71
+ const result = ReviewFinding.safeParse({
72
+ ...validFinding,
73
+ confidence: -0.1,
74
+ });
75
+ expect(result.success).toBe(false);
76
+ });
77
+ it('should validate CWE ID format', () => {
78
+ const validCwe = ReviewFinding.safeParse({
79
+ ...validFinding,
80
+ cwe_id: 'CWE-89',
81
+ });
82
+ expect(validCwe.success).toBe(true);
83
+ const invalidCwe = ReviewFinding.safeParse({
84
+ ...validFinding,
85
+ cwe_id: 'CWE89', // missing dash
86
+ });
87
+ expect(invalidCwe.success).toBe(false);
88
+ });
89
+ });
90
+ // =============================================================================
91
+ // JSON SCHEMA CONSISTENCY TESTS
92
+ // =============================================================================
93
+ describe('JSON Schema Consistency', () => {
94
+ it('should have file as required in location', () => {
95
+ const schema = getReviewOutputJsonSchema();
96
+ const findingSchema = schema.properties.findings.items;
97
+ const locationSchema = findingSchema.properties.location;
98
+ expect(locationSchema.required).toContain('file');
99
+ });
100
+ it('should include column fields in location', () => {
101
+ const schema = getReviewOutputJsonSchema();
102
+ const findingSchema = schema.properties.findings.items;
103
+ const locationSchema = findingSchema.properties.location;
104
+ expect(locationSchema.properties).toHaveProperty('column_start');
105
+ expect(locationSchema.properties).toHaveProperty('column_end');
106
+ });
107
+ it('should have all severity levels', () => {
108
+ const schema = getReviewOutputJsonSchema();
109
+ const severityEnum = schema.properties.findings.items.properties.severity.enum;
110
+ expect(severityEnum).toContain('critical');
111
+ expect(severityEnum).toContain('high');
112
+ expect(severityEnum).toContain('medium');
113
+ expect(severityEnum).toContain('low');
114
+ expect(severityEnum).toContain('info');
115
+ });
116
+ it('should have confidence constraints', () => {
117
+ const schema = getReviewOutputJsonSchema();
118
+ const confidenceSchema = schema.properties.findings.items.properties.confidence;
119
+ expect(confidenceSchema.minimum).toBe(0);
120
+ expect(confidenceSchema.maximum).toBe(1);
121
+ });
122
+ });
123
+ // =============================================================================
124
+ // PARSE OUTPUT TESTS
125
+ // =============================================================================
126
+ describe('parseReviewOutput', () => {
127
+ const validOutput = {
128
+ reviewer: 'test',
129
+ findings: [],
130
+ agreements: [],
131
+ disagreements: [],
132
+ alternatives: [],
133
+ risk_assessment: {
134
+ overall_level: 'low',
135
+ score: 20,
136
+ summary: 'Low risk',
137
+ top_concerns: [],
138
+ },
139
+ };
140
+ it('should parse valid JSON string', () => {
141
+ const result = parseReviewOutput(JSON.stringify(validOutput));
142
+ expect(result).not.toBeNull();
143
+ expect(result?.reviewer).toBe('test');
144
+ });
145
+ it('should extract JSON from markdown code blocks', () => {
146
+ const markdown = `Here is the review:
147
+
148
+ \`\`\`json
149
+ ${JSON.stringify(validOutput)}
150
+ \`\`\`
151
+
152
+ That's all.`;
153
+ const result = parseReviewOutput(markdown);
154
+ expect(result).not.toBeNull();
155
+ });
156
+ it('should return null for invalid JSON', () => {
157
+ const result = parseReviewOutput('not valid json');
158
+ expect(result).toBeNull();
159
+ });
160
+ it('should return null for incomplete output', () => {
161
+ const incomplete = { reviewer: 'test' }; // missing required fields
162
+ const result = parseReviewOutput(JSON.stringify(incomplete));
163
+ expect(result).toBeNull();
164
+ });
165
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Base Adapter Interface for AI Reviewers
3
+ *
4
+ * This provides a generic interface that any AI CLI can implement.
5
+ * Makes it easy to add new models (Ollama, Azure, etc.) without
6
+ * changing the core orchestration logic.
7
+ */
8
+ import { ReviewOutput } from '../schema.js';
9
+ import { FocusArea, OutputType, ReasoningEffort } from '../types.js';
10
+ export interface ReviewerCapabilities {
11
+ /** Display name for this reviewer */
12
+ name: string;
13
+ /** Short description of the reviewer's strengths */
14
+ description: string;
15
+ /** Focus areas this reviewer excels at */
16
+ strengths: FocusArea[];
17
+ /** Focus areas this reviewer is weaker at */
18
+ weaknesses: FocusArea[];
19
+ /** Whether the reviewer can read files from the filesystem */
20
+ hasFilesystemAccess: boolean;
21
+ /** Whether the reviewer supports JSON structured output */
22
+ supportsStructuredOutput: boolean;
23
+ /** Maximum context window size (tokens) */
24
+ maxContextTokens: number;
25
+ /** Supported reasoning effort levels (if applicable) */
26
+ reasoningLevels?: ReasoningEffort[];
27
+ }
28
+ export interface ReviewRequest {
29
+ /** Working directory containing the code */
30
+ workingDir: string;
31
+ /** Claude Code's output to review */
32
+ ccOutput: string;
33
+ /** Type of output being reviewed */
34
+ outputType: OutputType;
35
+ /** Specific files that CC analyzed */
36
+ analyzedFiles?: string[];
37
+ /** Areas to focus the review on */
38
+ focusAreas?: FocusArea[];
39
+ /** Custom instructions from the user */
40
+ customPrompt?: string;
41
+ /** Reasoning effort level (for models that support it) */
42
+ reasoningEffort?: ReasoningEffort;
43
+ /** Expert role configuration (optional override) */
44
+ expertRole?: ExpertRole;
45
+ }
46
+ export interface ExpertRole {
47
+ name: string;
48
+ description: string;
49
+ systemPrompt: string;
50
+ focusAreas: FocusArea[];
51
+ evaluationCriteria: string[];
52
+ }
53
+ export declare const EXPERT_ROLES: Record<string, ExpertRole>;
54
+ /**
55
+ * Select the best expert role based on requested focus areas
56
+ */
57
+ export declare function selectExpertRole(focusAreas?: FocusArea[]): ExpertRole;
58
+ export interface ReviewSuccess {
59
+ success: true;
60
+ output: ReviewOutput;
61
+ rawOutput?: string;
62
+ executionTimeMs: number;
63
+ }
64
+ export interface ReviewFailure {
65
+ success: false;
66
+ error: ReviewError;
67
+ suggestion?: string;
68
+ rawOutput?: string;
69
+ executionTimeMs: number;
70
+ }
71
+ export type ReviewResult = ReviewSuccess | ReviewFailure;
72
+ export interface ReviewError {
73
+ type: 'cli_not_found' | 'timeout' | 'rate_limit' | 'auth_error' | 'invalid_response' | 'cli_error' | 'parse_error';
74
+ message: string;
75
+ details?: Record<string, unknown>;
76
+ }
77
+ /**
78
+ * Base interface that all reviewer adapters must implement.
79
+ * This allows easy addition of new AI CLIs without changing orchestration logic.
80
+ */
81
+ export interface ReviewerAdapter {
82
+ /** Unique identifier for this adapter */
83
+ readonly id: string;
84
+ /** Get capabilities and metadata for this reviewer */
85
+ getCapabilities(): ReviewerCapabilities;
86
+ /** Check if the CLI is available and properly configured */
87
+ isAvailable(): Promise<boolean>;
88
+ /** Run a review and return structured output */
89
+ runReview(request: ReviewRequest): Promise<ReviewResult>;
90
+ /**
91
+ * Optional: Run peer review of another model's output
92
+ * Future capability - not currently implemented by any adapter
93
+ */
94
+ runPeerReview?(originalRequest: ReviewRequest, reviewToScore: ReviewOutput): Promise<ReviewResult>;
95
+ }
96
+ export declare function registerAdapter(adapter: ReviewerAdapter): void;
97
+ export declare function getAdapter(id: string): ReviewerAdapter | undefined;
98
+ export declare function getAllAdapters(): ReviewerAdapter[];
99
+ export declare function getAvailableAdapters(): Promise<ReviewerAdapter[]>;
100
+ /**
101
+ * Select the best available adapter for given focus areas
102
+ */
103
+ export declare function selectBestAdapter(focusAreas?: FocusArea[]): Promise<ReviewerAdapter | null>;