popeye-cli 1.8.0 → 1.9.1
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/README.md +47 -3
- package/cheatsheet.md +33 -0
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +1 -0
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/commands/review.d.ts +31 -0
- package/dist/cli/commands/review.d.ts.map +1 -0
- package/dist/cli/commands/review.js +156 -0
- package/dist/cli/commands/review.js.map +1 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +122 -61
- package/dist/cli/interactive.js.map +1 -1
- package/dist/types/audit.d.ts +623 -0
- package/dist/types/audit.d.ts.map +1 -0
- package/dist/types/audit.js +240 -0
- package/dist/types/audit.js.map +1 -0
- package/dist/types/workflow.d.ts +15 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +5 -0
- package/dist/types/workflow.js.map +1 -1
- package/dist/workflow/audit-analyzer.d.ts +58 -0
- package/dist/workflow/audit-analyzer.d.ts.map +1 -0
- package/dist/workflow/audit-analyzer.js +438 -0
- package/dist/workflow/audit-analyzer.js.map +1 -0
- package/dist/workflow/audit-mode.d.ts +28 -0
- package/dist/workflow/audit-mode.d.ts.map +1 -0
- package/dist/workflow/audit-mode.js +169 -0
- package/dist/workflow/audit-mode.js.map +1 -0
- package/dist/workflow/audit-recovery.d.ts +61 -0
- package/dist/workflow/audit-recovery.d.ts.map +1 -0
- package/dist/workflow/audit-recovery.js +242 -0
- package/dist/workflow/audit-recovery.js.map +1 -0
- package/dist/workflow/audit-reporter.d.ts +65 -0
- package/dist/workflow/audit-reporter.d.ts.map +1 -0
- package/dist/workflow/audit-reporter.js +301 -0
- package/dist/workflow/audit-reporter.js.map +1 -0
- package/dist/workflow/audit-scanner.d.ts +87 -0
- package/dist/workflow/audit-scanner.d.ts.map +1 -0
- package/dist/workflow/audit-scanner.js +768 -0
- package/dist/workflow/audit-scanner.js.map +1 -0
- package/dist/workflow/index.d.ts +5 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +5 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/index.ts +1 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/interactive.ts +72 -4
- package/src/types/audit.ts +294 -0
- package/src/types/workflow.ts +15 -0
- package/src/workflow/audit-analyzer.ts +510 -0
- package/src/workflow/audit-mode.ts +240 -0
- package/src/workflow/audit-recovery.ts +284 -0
- package/src/workflow/audit-reporter.ts +370 -0
- package/src/workflow/audit-scanner.ts +873 -0
- package/src/workflow/index.ts +5 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/types/audit.test.ts +250 -0
- package/tests/workflow/audit-analyzer.test.ts +281 -0
- package/tests/workflow/audit-mode.test.ts +114 -0
- package/tests/workflow/audit-recovery.test.ts +237 -0
- package/tests/workflow/audit-reporter.test.ts +254 -0
- package/tests/workflow/audit-scanner.test.ts +270 -0
package/src/workflow/index.ts
CHANGED
|
@@ -57,6 +57,11 @@ export * from './website-strategy.js';
|
|
|
57
57
|
export * from './overview.js';
|
|
58
58
|
export * from './db-state-machine.js';
|
|
59
59
|
export * from './db-setup-runner.js';
|
|
60
|
+
export * from './audit-scanner.js';
|
|
61
|
+
export * from './audit-analyzer.js';
|
|
62
|
+
export * from './audit-reporter.js';
|
|
63
|
+
export * from './audit-recovery.js';
|
|
64
|
+
export * from './audit-mode.js';
|
|
60
65
|
|
|
61
66
|
/**
|
|
62
67
|
* Workflow options
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the review CLI command.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import { createReviewCommand } from '../../../src/cli/commands/review.js';
|
|
6
|
+
|
|
7
|
+
describe('createReviewCommand', () => {
|
|
8
|
+
it('should create a command named review', () => {
|
|
9
|
+
const cmd = createReviewCommand();
|
|
10
|
+
expect(cmd.name()).toBe('review');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('should have audit as an alias', () => {
|
|
14
|
+
const cmd = createReviewCommand();
|
|
15
|
+
expect(cmd.aliases()).toContain('audit');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should accept the depth option', () => {
|
|
19
|
+
const cmd = createReviewCommand();
|
|
20
|
+
const depthOpt = cmd.options.find((o) => o.long === '--depth');
|
|
21
|
+
expect(depthOpt).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should accept the strict option', () => {
|
|
25
|
+
const cmd = createReviewCommand();
|
|
26
|
+
const strictOpt = cmd.options.find((o) => o.long === '--strict');
|
|
27
|
+
expect(strictOpt).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should accept the format option', () => {
|
|
31
|
+
const cmd = createReviewCommand();
|
|
32
|
+
const formatOpt = cmd.options.find((o) => o.long === '--format');
|
|
33
|
+
expect(formatOpt).toBeDefined();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should accept the no-recover option', () => {
|
|
37
|
+
const cmd = createReviewCommand();
|
|
38
|
+
const recoverOpt = cmd.options.find((o) => o.long === '--no-recover');
|
|
39
|
+
expect(recoverOpt).toBeDefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should accept the target option', () => {
|
|
43
|
+
const cmd = createReviewCommand();
|
|
44
|
+
const targetOpt = cmd.options.find((o) => o.long === '--target');
|
|
45
|
+
expect(targetOpt).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should have a description', () => {
|
|
49
|
+
const cmd = createReviewCommand();
|
|
50
|
+
expect(cmd.description()).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for audit type schemas.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
AuditSeveritySchema,
|
|
7
|
+
AuditCategorySchema,
|
|
8
|
+
ComponentKindSchema,
|
|
9
|
+
AuditEvidenceSchema,
|
|
10
|
+
DependencyManifestSchema,
|
|
11
|
+
FileEntrySchema,
|
|
12
|
+
ComponentScanSchema,
|
|
13
|
+
WiringMismatchSchema,
|
|
14
|
+
WiringMatrixSchema,
|
|
15
|
+
SearchMetadataSchema,
|
|
16
|
+
AuditFindingSchema,
|
|
17
|
+
ProjectSummaryReportSchema,
|
|
18
|
+
ProjectAuditReportSchema,
|
|
19
|
+
RecoveryTaskSchema,
|
|
20
|
+
RecoveryMilestoneSchema,
|
|
21
|
+
RecoveryPlanSchema,
|
|
22
|
+
AuditModeOptionsSchema,
|
|
23
|
+
} from '../../src/types/audit.js';
|
|
24
|
+
|
|
25
|
+
describe('AuditSeveritySchema', () => {
|
|
26
|
+
it('should accept valid severity values', () => {
|
|
27
|
+
expect(AuditSeveritySchema.parse('critical')).toBe('critical');
|
|
28
|
+
expect(AuditSeveritySchema.parse('major')).toBe('major');
|
|
29
|
+
expect(AuditSeveritySchema.parse('minor')).toBe('minor');
|
|
30
|
+
expect(AuditSeveritySchema.parse('info')).toBe('info');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should reject invalid values', () => {
|
|
34
|
+
expect(() => AuditSeveritySchema.parse('high')).toThrow();
|
|
35
|
+
expect(() => AuditSeveritySchema.parse('')).toThrow();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('AuditCategorySchema', () => {
|
|
40
|
+
it('should accept all valid categories', () => {
|
|
41
|
+
const categories = [
|
|
42
|
+
'feature-completeness',
|
|
43
|
+
'integration-wiring',
|
|
44
|
+
'test-coverage',
|
|
45
|
+
'config-deployment',
|
|
46
|
+
'dependency-sanity',
|
|
47
|
+
'consistency',
|
|
48
|
+
'security',
|
|
49
|
+
'documentation',
|
|
50
|
+
];
|
|
51
|
+
for (const cat of categories) {
|
|
52
|
+
expect(AuditCategorySchema.parse(cat)).toBe(cat);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should reject invalid category', () => {
|
|
57
|
+
expect(() => AuditCategorySchema.parse('performance')).toThrow();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('ComponentKindSchema', () => {
|
|
62
|
+
it('should accept valid kinds', () => {
|
|
63
|
+
expect(ComponentKindSchema.parse('frontend')).toBe('frontend');
|
|
64
|
+
expect(ComponentKindSchema.parse('backend')).toBe('backend');
|
|
65
|
+
expect(ComponentKindSchema.parse('website')).toBe('website');
|
|
66
|
+
expect(ComponentKindSchema.parse('shared')).toBe('shared');
|
|
67
|
+
expect(ComponentKindSchema.parse('infra')).toBe('infra');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should reject unknown kind', () => {
|
|
71
|
+
expect(() => ComponentKindSchema.parse('mobile')).toThrow();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('ComponentScanSchema', () => {
|
|
76
|
+
const validComponent = {
|
|
77
|
+
kind: 'frontend',
|
|
78
|
+
rootDir: 'apps/frontend',
|
|
79
|
+
language: 'typescript',
|
|
80
|
+
framework: 'react',
|
|
81
|
+
entryPoints: ['src/main.tsx'],
|
|
82
|
+
routeFiles: ['src/App.tsx'],
|
|
83
|
+
testFiles: [{ path: 'tests/App.test.tsx', lines: 50 }],
|
|
84
|
+
sourceFiles: [{ path: 'src/main.tsx', lines: 20, extension: '.tsx' }],
|
|
85
|
+
dependencyManifests: [
|
|
86
|
+
{ file: 'package.json', type: 'package.json', dependencies: { react: '^18.0.0' } },
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
it('should validate a complete component scan', () => {
|
|
91
|
+
const result = ComponentScanSchema.parse(validComponent);
|
|
92
|
+
expect(result.kind).toBe('frontend');
|
|
93
|
+
expect(result.rootDir).toBe('apps/frontend');
|
|
94
|
+
expect(result.sourceFiles).toHaveLength(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should accept minimal component scan', () => {
|
|
98
|
+
const minimal = {
|
|
99
|
+
kind: 'backend',
|
|
100
|
+
rootDir: '.',
|
|
101
|
+
language: 'python',
|
|
102
|
+
entryPoints: [],
|
|
103
|
+
routeFiles: [],
|
|
104
|
+
testFiles: [],
|
|
105
|
+
sourceFiles: [],
|
|
106
|
+
dependencyManifests: [],
|
|
107
|
+
};
|
|
108
|
+
const result = ComponentScanSchema.parse(minimal);
|
|
109
|
+
expect(result.framework).toBeUndefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should reject invalid language', () => {
|
|
113
|
+
expect(() =>
|
|
114
|
+
ComponentScanSchema.parse({ ...validComponent, language: 'rust' })
|
|
115
|
+
).toThrow();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('WiringMatrixSchema', () => {
|
|
120
|
+
it('should validate a complete wiring matrix', () => {
|
|
121
|
+
const wiring = {
|
|
122
|
+
frontendApiBaseEnvKeys: ['VITE_API_URL'],
|
|
123
|
+
frontendApiBaseResolved: 'http://localhost:3000',
|
|
124
|
+
backendCorsOrigins: ['http://localhost:5173'],
|
|
125
|
+
backendApiPrefix: '/api',
|
|
126
|
+
potentialMismatches: [
|
|
127
|
+
{
|
|
128
|
+
type: 'cors-origin-mismatch',
|
|
129
|
+
details: 'Frontend origin not in CORS list',
|
|
130
|
+
evidence: [{ file: '.env.example', snippet: 'VITE_API_URL=http://localhost:3000' }],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
const result = WiringMatrixSchema.parse(wiring);
|
|
135
|
+
expect(result.potentialMismatches).toHaveLength(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should accept empty mismatches', () => {
|
|
139
|
+
const wiring = {
|
|
140
|
+
frontendApiBaseEnvKeys: [],
|
|
141
|
+
potentialMismatches: [],
|
|
142
|
+
};
|
|
143
|
+
const result = WiringMatrixSchema.parse(wiring);
|
|
144
|
+
expect(result.potentialMismatches).toHaveLength(0);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('SearchMetadataSchema', () => {
|
|
149
|
+
it('should validate search metadata', () => {
|
|
150
|
+
const meta = {
|
|
151
|
+
serenaUsed: true,
|
|
152
|
+
serenaRetries: 1,
|
|
153
|
+
serenaErrors: ['timeout'],
|
|
154
|
+
fallbackUsed: true,
|
|
155
|
+
fallbackTool: 'grep',
|
|
156
|
+
searchQueries: ['find_symbol UserService'],
|
|
157
|
+
};
|
|
158
|
+
const result = SearchMetadataSchema.parse(meta);
|
|
159
|
+
expect(result.serenaUsed).toBe(true);
|
|
160
|
+
expect(result.serenaRetries).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should require all fields', () => {
|
|
164
|
+
expect(() => SearchMetadataSchema.parse({ serenaUsed: true })).toThrow();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('AuditFindingSchema', () => {
|
|
169
|
+
const validFinding = {
|
|
170
|
+
id: 'AUD-001',
|
|
171
|
+
category: 'test-coverage',
|
|
172
|
+
severity: 'major',
|
|
173
|
+
title: 'No tests for auth module',
|
|
174
|
+
description: 'The authentication module has zero test files.',
|
|
175
|
+
evidence: [{ file: 'src/auth/login.ts', description: 'No test file found' }],
|
|
176
|
+
recommendation: 'Add unit tests for auth handlers',
|
|
177
|
+
autoFixable: false,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
it('should validate a complete finding', () => {
|
|
181
|
+
const result = AuditFindingSchema.parse(validFinding);
|
|
182
|
+
expect(result.id).toBe('AUD-001');
|
|
183
|
+
expect(result.autoFixable).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should reject missing required fields', () => {
|
|
187
|
+
const { recommendation, ...incomplete } = validFinding;
|
|
188
|
+
expect(() => AuditFindingSchema.parse(incomplete)).toThrow();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should reject invalid severity', () => {
|
|
192
|
+
expect(() =>
|
|
193
|
+
AuditFindingSchema.parse({ ...validFinding, severity: 'high' })
|
|
194
|
+
).toThrow();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('RecoveryTaskSchema', () => {
|
|
199
|
+
it('should require appTarget', () => {
|
|
200
|
+
const task = {
|
|
201
|
+
name: 'Fix CORS config',
|
|
202
|
+
description: 'Update backend CORS settings',
|
|
203
|
+
findingIds: ['AUD-003'],
|
|
204
|
+
acceptanceCriteria: ['CORS allows frontend origin'],
|
|
205
|
+
appTarget: 'backend',
|
|
206
|
+
};
|
|
207
|
+
const result = RecoveryTaskSchema.parse(task);
|
|
208
|
+
expect(result.appTarget).toBe('backend');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should reject missing appTarget', () => {
|
|
212
|
+
const task = {
|
|
213
|
+
name: 'Fix something',
|
|
214
|
+
description: 'A task',
|
|
215
|
+
findingIds: ['AUD-001'],
|
|
216
|
+
acceptanceCriteria: ['Fixed'],
|
|
217
|
+
};
|
|
218
|
+
expect(() => RecoveryTaskSchema.parse(task)).toThrow();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('AuditModeOptionsSchema', () => {
|
|
223
|
+
it('should apply defaults', () => {
|
|
224
|
+
const result = AuditModeOptionsSchema.parse({ projectDir: '/tmp/proj' });
|
|
225
|
+
expect(result.depth).toBe(2);
|
|
226
|
+
expect(result.runTests).toBe(true);
|
|
227
|
+
expect(result.strict).toBe(false);
|
|
228
|
+
expect(result.format).toBe('both');
|
|
229
|
+
expect(result.autoRecover).toBe(true);
|
|
230
|
+
expect(result.target).toBe('all');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('should accept overrides', () => {
|
|
234
|
+
const result = AuditModeOptionsSchema.parse({
|
|
235
|
+
projectDir: '/tmp/proj',
|
|
236
|
+
depth: 3,
|
|
237
|
+
strict: true,
|
|
238
|
+
target: 'frontend',
|
|
239
|
+
});
|
|
240
|
+
expect(result.depth).toBe(3);
|
|
241
|
+
expect(result.strict).toBe(true);
|
|
242
|
+
expect(result.target).toBe('frontend');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should reject depth out of range', () => {
|
|
246
|
+
expect(() =>
|
|
247
|
+
AuditModeOptionsSchema.parse({ projectDir: '/tmp/proj', depth: 5 })
|
|
248
|
+
).toThrow();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the audit analyzer module.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
buildAnalysisPrompt,
|
|
7
|
+
parseAuditFindings,
|
|
8
|
+
calculateAuditScores,
|
|
9
|
+
} from '../../src/workflow/audit-analyzer.js';
|
|
10
|
+
import type { ProjectScanResult } from '../../src/types/audit.js';
|
|
11
|
+
import type { ProjectState } from '../../src/types/workflow.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Minimal scan result for testing prompts.
|
|
15
|
+
*/
|
|
16
|
+
function makeScan(overrides: Partial<ProjectScanResult> = {}): ProjectScanResult {
|
|
17
|
+
return {
|
|
18
|
+
tree: 'src/\n main.ts',
|
|
19
|
+
components: [],
|
|
20
|
+
detectedComposition: ['frontend'],
|
|
21
|
+
stateLanguage: 'typescript',
|
|
22
|
+
compositionMismatch: false,
|
|
23
|
+
sourceFiles: [],
|
|
24
|
+
testFiles: [],
|
|
25
|
+
configFiles: [],
|
|
26
|
+
entryPoints: [],
|
|
27
|
+
routeFiles: [],
|
|
28
|
+
dependencies: [],
|
|
29
|
+
totalSourceFiles: 10,
|
|
30
|
+
totalTestFiles: 3,
|
|
31
|
+
totalLinesOfCode: 500,
|
|
32
|
+
totalLinesOfTests: 100,
|
|
33
|
+
language: 'typescript',
|
|
34
|
+
docsIndex: [],
|
|
35
|
+
keyFileSnippets: [],
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Minimal project state for testing.
|
|
42
|
+
*/
|
|
43
|
+
function makeState(overrides: Partial<ProjectState> = {}): ProjectState {
|
|
44
|
+
return {
|
|
45
|
+
id: 'test-id',
|
|
46
|
+
name: 'Test Project',
|
|
47
|
+
idea: 'A test project',
|
|
48
|
+
language: 'typescript',
|
|
49
|
+
openaiModel: 'gpt-4',
|
|
50
|
+
phase: 'complete',
|
|
51
|
+
status: 'complete',
|
|
52
|
+
milestones: [],
|
|
53
|
+
currentMilestone: null,
|
|
54
|
+
currentTask: null,
|
|
55
|
+
consensusHistory: [],
|
|
56
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
57
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
58
|
+
...overrides,
|
|
59
|
+
} as ProjectState;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// buildAnalysisPrompt
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe('buildAnalysisPrompt', () => {
|
|
67
|
+
it('should include project name and language', () => {
|
|
68
|
+
const prompt = buildAnalysisPrompt(makeScan(), makeState(), 2, false);
|
|
69
|
+
expect(prompt).toContain('Test Project');
|
|
70
|
+
expect(prompt).toContain('typescript');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should include component structure section', () => {
|
|
74
|
+
const scan = makeScan({
|
|
75
|
+
components: [
|
|
76
|
+
{
|
|
77
|
+
kind: 'frontend',
|
|
78
|
+
rootDir: 'apps/frontend',
|
|
79
|
+
language: 'typescript',
|
|
80
|
+
framework: 'react',
|
|
81
|
+
entryPoints: ['src/main.tsx'],
|
|
82
|
+
routeFiles: [],
|
|
83
|
+
testFiles: [],
|
|
84
|
+
sourceFiles: [],
|
|
85
|
+
dependencyManifests: [],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
const prompt = buildAnalysisPrompt(scan, makeState(), 2, false);
|
|
90
|
+
expect(prompt).toContain('frontend');
|
|
91
|
+
expect(prompt).toContain('react');
|
|
92
|
+
expect(prompt).toContain('apps/frontend');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should include wiring matrix when present', () => {
|
|
96
|
+
const scan = makeScan({
|
|
97
|
+
wiring: {
|
|
98
|
+
frontendApiBaseEnvKeys: ['VITE_API_URL'],
|
|
99
|
+
frontendApiBaseResolved: 'http://localhost:3000',
|
|
100
|
+
backendCorsOrigins: ['http://localhost:5173'],
|
|
101
|
+
backendApiPrefix: '/api',
|
|
102
|
+
potentialMismatches: [],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const prompt = buildAnalysisPrompt(scan, makeState(), 2, false);
|
|
106
|
+
expect(prompt).toContain('VITE_API_URL');
|
|
107
|
+
expect(prompt).toContain('Wiring Matrix');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should include CLAUDE.md content when present', () => {
|
|
111
|
+
const scan = makeScan({ claudeMdContent: '# Project Rules\nFollow PEP8.' });
|
|
112
|
+
const prompt = buildAnalysisPrompt(scan, makeState(), 2, false);
|
|
113
|
+
expect(prompt).toContain('Project Rules');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should include depth-2 checks', () => {
|
|
117
|
+
const prompt = buildAnalysisPrompt(makeScan(), makeState(), 2, false);
|
|
118
|
+
expect(prompt).toContain('Depth-2 Checks');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should include depth-3 checks at depth 3', () => {
|
|
122
|
+
const prompt = buildAnalysisPrompt(makeScan(), makeState(), 3, false);
|
|
123
|
+
expect(prompt).toContain('Depth-3 Checks');
|
|
124
|
+
expect(prompt).toContain('OWASP');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should indicate strict mode when enabled', () => {
|
|
128
|
+
const prompt = buildAnalysisPrompt(makeScan(), makeState(), 2, true);
|
|
129
|
+
expect(prompt).toContain('STRICT MODE');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should include milestone status', () => {
|
|
133
|
+
const state = makeState({
|
|
134
|
+
milestones: [
|
|
135
|
+
{
|
|
136
|
+
id: 'm1',
|
|
137
|
+
name: 'Setup',
|
|
138
|
+
description: 'Initial setup',
|
|
139
|
+
status: 'complete',
|
|
140
|
+
tasks: [
|
|
141
|
+
{ id: 't1', name: 'Init', description: 'Init project', status: 'complete' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
const prompt = buildAnalysisPrompt(makeScan(), state, 2, false);
|
|
147
|
+
expect(prompt).toContain('Setup: complete');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// parseAuditFindings
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
describe('parseAuditFindings', () => {
|
|
156
|
+
it('should parse valid JSON findings from code fences', () => {
|
|
157
|
+
const raw = '```json\n[\n {\n "id": "AUD-001",\n "category": "test-coverage",\n "severity": "major",\n "title": "No tests",\n "description": "Missing tests.",\n "evidence": [],\n "recommendation": "Add tests.",\n "autoFixable": false\n }\n]\n```';
|
|
158
|
+
const findings = parseAuditFindings(raw);
|
|
159
|
+
expect(findings).toHaveLength(1);
|
|
160
|
+
expect(findings[0].id).toBe('AUD-001');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should parse JSON without code fences', () => {
|
|
164
|
+
const raw = '[{"id":"AUD-001","category":"security","severity":"critical","title":"SQL Injection","description":"Raw SQL used.","evidence":[],"recommendation":"Use ORM.","autoFixable":false}]';
|
|
165
|
+
const findings = parseAuditFindings(raw);
|
|
166
|
+
expect(findings).toHaveLength(1);
|
|
167
|
+
expect(findings[0].category).toBe('security');
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should handle malformed JSON gracefully', () => {
|
|
171
|
+
const findings = parseAuditFindings('This is not JSON at all.');
|
|
172
|
+
expect(findings).toEqual([]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should skip individual malformed findings', () => {
|
|
176
|
+
const raw = JSON.stringify([
|
|
177
|
+
{
|
|
178
|
+
id: 'AUD-001',
|
|
179
|
+
category: 'test-coverage',
|
|
180
|
+
severity: 'major',
|
|
181
|
+
title: 'Valid',
|
|
182
|
+
description: 'Valid finding.',
|
|
183
|
+
evidence: [],
|
|
184
|
+
recommendation: 'Fix it.',
|
|
185
|
+
autoFixable: false,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'AUD-002',
|
|
189
|
+
// Missing required fields
|
|
190
|
+
title: 'Invalid',
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
const findings = parseAuditFindings(raw);
|
|
194
|
+
expect(findings).toHaveLength(1);
|
|
195
|
+
expect(findings[0].id).toBe('AUD-001');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should handle non-array JSON', () => {
|
|
199
|
+
const findings = parseAuditFindings('{"not": "an array"}');
|
|
200
|
+
expect(findings).toEqual([]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('should extract JSON array embedded in surrounding text', () => {
|
|
204
|
+
const raw = 'Here are the findings:\n[{"id":"AUD-001","category":"documentation","severity":"info","title":"Missing changelog","description":"No CHANGELOG.md","evidence":[],"recommendation":"Add one.","autoFixable":true}]\nEnd of findings.';
|
|
205
|
+
const findings = parseAuditFindings(raw);
|
|
206
|
+
expect(findings).toHaveLength(1);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// calculateAuditScores
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
describe('calculateAuditScores', () => {
|
|
215
|
+
it('should return 100 when no findings', () => {
|
|
216
|
+
const { overallScore, categoryScores } = calculateAuditScores([], makeScan());
|
|
217
|
+
expect(overallScore).toBe(100);
|
|
218
|
+
expect(categoryScores['test-coverage']).toBe(100);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should deduct for critical findings', () => {
|
|
222
|
+
const findings = [
|
|
223
|
+
{
|
|
224
|
+
id: 'AUD-001',
|
|
225
|
+
category: 'security' as const,
|
|
226
|
+
severity: 'critical' as const,
|
|
227
|
+
title: 'SQL Injection',
|
|
228
|
+
description: 'Found raw SQL.',
|
|
229
|
+
evidence: [],
|
|
230
|
+
recommendation: 'Use ORM.',
|
|
231
|
+
autoFixable: false,
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
const { overallScore, categoryScores } = calculateAuditScores(findings, makeScan());
|
|
235
|
+
expect(categoryScores['security']).toBe(80); // 100 - 20
|
|
236
|
+
expect(overallScore).toBeLessThan(100);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should not go below 0', () => {
|
|
240
|
+
const findings = Array.from({ length: 10 }, (_, i) => ({
|
|
241
|
+
id: `AUD-${i}`,
|
|
242
|
+
category: 'security' as const,
|
|
243
|
+
severity: 'critical' as const,
|
|
244
|
+
title: `Finding ${i}`,
|
|
245
|
+
description: 'Critical issue.',
|
|
246
|
+
evidence: [],
|
|
247
|
+
recommendation: 'Fix it.',
|
|
248
|
+
autoFixable: false,
|
|
249
|
+
}));
|
|
250
|
+
const { categoryScores } = calculateAuditScores(findings, makeScan());
|
|
251
|
+
expect(categoryScores['security']).toBe(0);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle mixed severity findings', () => {
|
|
255
|
+
const findings = [
|
|
256
|
+
{
|
|
257
|
+
id: 'AUD-001',
|
|
258
|
+
category: 'test-coverage' as const,
|
|
259
|
+
severity: 'major' as const,
|
|
260
|
+
title: 'Low coverage',
|
|
261
|
+
description: 'Only 20% covered.',
|
|
262
|
+
evidence: [],
|
|
263
|
+
recommendation: 'Add tests.',
|
|
264
|
+
autoFixable: false,
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
id: 'AUD-002',
|
|
268
|
+
category: 'test-coverage' as const,
|
|
269
|
+
severity: 'info' as const,
|
|
270
|
+
title: 'Snapshot tests used',
|
|
271
|
+
description: 'Consider replacing with assertions.',
|
|
272
|
+
evidence: [],
|
|
273
|
+
recommendation: 'Optional.',
|
|
274
|
+
autoFixable: false,
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
const { categoryScores } = calculateAuditScores(findings, makeScan());
|
|
278
|
+
// major = -10, info = 0
|
|
279
|
+
expect(categoryScores['test-coverage']).toBe(90);
|
|
280
|
+
});
|
|
281
|
+
});
|