popeye-cli 1.7.0 → 1.9.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/README.md +148 -7
- package/cheatsheet.md +440 -0
- package/dist/cli/commands/db.d.ts +10 -0
- package/dist/cli/commands/db.d.ts.map +1 -0
- package/dist/cli/commands/db.js +240 -0
- package/dist/cli/commands/db.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +18 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +255 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/index.d.ts +3 -0
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +3 -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 +4 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +218 -61
- package/dist/cli/interactive.js.map +1 -1
- package/dist/generators/admin-wizard.d.ts +25 -0
- package/dist/generators/admin-wizard.d.ts.map +1 -0
- package/dist/generators/admin-wizard.js +123 -0
- package/dist/generators/admin-wizard.js.map +1 -0
- package/dist/generators/all.d.ts.map +1 -1
- package/dist/generators/all.js +10 -3
- package/dist/generators/all.js.map +1 -1
- package/dist/generators/database.d.ts +58 -0
- package/dist/generators/database.d.ts.map +1 -0
- package/dist/generators/database.js +229 -0
- package/dist/generators/database.js.map +1 -0
- package/dist/generators/fullstack.d.ts.map +1 -1
- package/dist/generators/fullstack.js +23 -7
- package/dist/generators/fullstack.js.map +1 -1
- package/dist/generators/index.d.ts +2 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +2 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
- package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-python.js +425 -0
- package/dist/generators/templates/admin-wizard-python.js.map +1 -0
- package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
- package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
- package/dist/generators/templates/admin-wizard-react.js +554 -0
- package/dist/generators/templates/admin-wizard-react.js.map +1 -0
- package/dist/generators/templates/database-docker.d.ts +23 -0
- package/dist/generators/templates/database-docker.d.ts.map +1 -0
- package/dist/generators/templates/database-docker.js +221 -0
- package/dist/generators/templates/database-docker.js.map +1 -0
- package/dist/generators/templates/database-python.d.ts +54 -0
- package/dist/generators/templates/database-python.d.ts.map +1 -0
- package/dist/generators/templates/database-python.js +723 -0
- package/dist/generators/templates/database-python.js.map +1 -0
- package/dist/generators/templates/database-typescript.d.ts +34 -0
- package/dist/generators/templates/database-typescript.d.ts.map +1 -0
- package/dist/generators/templates/database-typescript.js +232 -0
- package/dist/generators/templates/database-typescript.js.map +1 -0
- package/dist/generators/templates/fullstack.d.ts.map +1 -1
- package/dist/generators/templates/fullstack.js +29 -0
- package/dist/generators/templates/fullstack.js.map +1 -1
- package/dist/generators/templates/index.d.ts +5 -0
- package/dist/generators/templates/index.d.ts.map +1 -1
- package/dist/generators/templates/index.js +5 -0
- package/dist/generators/templates/index.js.map +1 -1
- package/dist/state/index.d.ts +10 -0
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +21 -0
- package/dist/state/index.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/database-runtime.d.ts +86 -0
- package/dist/types/database-runtime.d.ts.map +1 -0
- package/dist/types/database-runtime.js +61 -0
- package/dist/types/database-runtime.js.map +1 -0
- package/dist/types/database.d.ts +85 -0
- package/dist/types/database.d.ts.map +1 -0
- package/dist/types/database.js +71 -0
- package/dist/types/database.js.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +4 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/workflow.d.ts +36 -0
- package/dist/types/workflow.d.ts.map +1 -1
- package/dist/types/workflow.js +7 -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 +420 -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/db-setup-runner.d.ts +63 -0
- package/dist/workflow/db-setup-runner.d.ts.map +1 -0
- package/dist/workflow/db-setup-runner.js +336 -0
- package/dist/workflow/db-setup-runner.js.map +1 -0
- package/dist/workflow/db-state-machine.d.ts +30 -0
- package/dist/workflow/db-state-machine.d.ts.map +1 -0
- package/dist/workflow/db-state-machine.js +51 -0
- package/dist/workflow/db-state-machine.js.map +1 -0
- package/dist/workflow/index.d.ts +7 -0
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +7 -0
- package/dist/workflow/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/db.ts +281 -0
- package/src/cli/commands/doctor.ts +273 -0
- package/src/cli/commands/index.ts +3 -0
- package/src/cli/commands/review.ts +187 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/interactive.ts +174 -4
- package/src/generators/admin-wizard.ts +146 -0
- package/src/generators/all.ts +10 -3
- package/src/generators/database.ts +286 -0
- package/src/generators/fullstack.ts +26 -9
- package/src/generators/index.ts +12 -0
- package/src/generators/templates/admin-wizard-python.ts +431 -0
- package/src/generators/templates/admin-wizard-react.ts +560 -0
- package/src/generators/templates/database-docker.ts +227 -0
- package/src/generators/templates/database-python.ts +734 -0
- package/src/generators/templates/database-typescript.ts +238 -0
- package/src/generators/templates/fullstack.ts +29 -0
- package/src/generators/templates/index.ts +5 -0
- package/src/state/index.ts +28 -0
- package/src/types/audit.ts +294 -0
- package/src/types/database-runtime.ts +69 -0
- package/src/types/database.ts +84 -0
- package/src/types/index.ts +29 -0
- package/src/types/workflow.ts +20 -0
- package/src/workflow/audit-analyzer.ts +491 -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/db-setup-runner.ts +391 -0
- package/src/workflow/db-state-machine.ts +58 -0
- package/src/workflow/index.ts +7 -0
- package/tests/cli/commands/review.test.ts +52 -0
- package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
- package/tests/generators/admin-wizard-templates.test.ts +366 -0
- package/tests/generators/cross-phase-integration.test.ts +383 -0
- package/tests/generators/database.test.ts +456 -0
- package/tests/generators/fe-be-db-integration.test.ts +613 -0
- package/tests/types/audit.test.ts +250 -0
- package/tests/types/database-runtime.test.ts +158 -0
- package/tests/types/database.test.ts +187 -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/tests/workflow/db-setup-runner.test.ts +211 -0
- package/tests/workflow/db-state-machine.test.ts +117 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the audit mode orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* These tests mock the AI analyzer and state modules to validate
|
|
5
|
+
* the orchestration flow without requiring real AI calls.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import type { AuditModeRunOptions } from '../../src/workflow/audit-mode.js';
|
|
9
|
+
|
|
10
|
+
// Mock the external dependencies before importing the module under test
|
|
11
|
+
vi.mock('../../src/state/index.js', () => ({
|
|
12
|
+
loadProject: vi.fn().mockResolvedValue({
|
|
13
|
+
id: 'test-id',
|
|
14
|
+
name: 'Test Project',
|
|
15
|
+
idea: 'A test project',
|
|
16
|
+
language: 'typescript',
|
|
17
|
+
openaiModel: 'gpt-4',
|
|
18
|
+
phase: 'complete',
|
|
19
|
+
status: 'complete',
|
|
20
|
+
specification: 'Build a todo app',
|
|
21
|
+
milestones: [],
|
|
22
|
+
currentMilestone: null,
|
|
23
|
+
currentTask: null,
|
|
24
|
+
consensusHistory: [],
|
|
25
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
26
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
27
|
+
}),
|
|
28
|
+
updateState: vi.fn().mockResolvedValue({}),
|
|
29
|
+
addMilestones: vi.fn().mockResolvedValue({}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('../../src/adapters/claude.js', () => ({
|
|
33
|
+
executePrompt: vi.fn().mockResolvedValue({
|
|
34
|
+
success: true,
|
|
35
|
+
response: '```json\n[]\n```',
|
|
36
|
+
toolCalls: [],
|
|
37
|
+
}),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Import after mocks are set up
|
|
41
|
+
const { runAuditMode } = await import('../../src/workflow/audit-mode.js');
|
|
42
|
+
const { loadProject, updateState } = await import('../../src/state/index.js');
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function makeOptions(overrides: Partial<AuditModeRunOptions> = {}): AuditModeRunOptions {
|
|
49
|
+
return {
|
|
50
|
+
projectDir: '/tmp/fake-project',
|
|
51
|
+
depth: 2,
|
|
52
|
+
runTests: false,
|
|
53
|
+
strict: false,
|
|
54
|
+
format: 'json',
|
|
55
|
+
autoRecover: false,
|
|
56
|
+
target: 'all',
|
|
57
|
+
...overrides,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// runAuditMode
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
describe('runAuditMode', () => {
|
|
66
|
+
it('should return error result when project cannot be loaded', async () => {
|
|
67
|
+
vi.mocked(loadProject).mockRejectedValueOnce(new Error('Project not found'));
|
|
68
|
+
|
|
69
|
+
const result = await runAuditMode(makeOptions());
|
|
70
|
+
expect(result.success).toBe(false);
|
|
71
|
+
expect(result.error).toContain('Project not found');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should call progress callbacks through stages', async () => {
|
|
75
|
+
const messages: Array<{ stage: string; msg: string }> = [];
|
|
76
|
+
const result = await runAuditMode(
|
|
77
|
+
makeOptions({
|
|
78
|
+
onProgress: (stage, msg) => messages.push({ stage, msg }),
|
|
79
|
+
})
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Should have stage-1, stage-2, and stage-3 messages
|
|
83
|
+
expect(messages.some((m) => m.stage === 'stage-1')).toBe(true);
|
|
84
|
+
expect(messages.some((m) => m.stage === 'stage-2')).toBe(true);
|
|
85
|
+
expect(messages.some((m) => m.stage === 'stage-3')).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should update state with audit report path', async () => {
|
|
89
|
+
await runAuditMode(makeOptions({ format: 'json' }));
|
|
90
|
+
expect(updateState).toHaveBeenCalledWith(
|
|
91
|
+
'/tmp/fake-project',
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
auditRunId: expect.any(String),
|
|
94
|
+
auditLastRunAt: expect.any(String),
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should not generate recovery when no trigger conditions met', async () => {
|
|
100
|
+
const result = await runAuditMode(makeOptions());
|
|
101
|
+
// With empty findings from mock, no recovery should be triggered
|
|
102
|
+
expect(result.recovery).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should have a valid audit report structure', async () => {
|
|
106
|
+
const result = await runAuditMode(makeOptions());
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(result.audit).toBeDefined();
|
|
109
|
+
expect(result.audit.projectName).toBe('Test Project');
|
|
110
|
+
expect(result.audit.auditRunId).toBeTruthy();
|
|
111
|
+
expect(result.summary).toBeDefined();
|
|
112
|
+
expect(result.summary.projectName).toBe('Test Project');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the audit recovery module.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
shouldTriggerRecovery,
|
|
7
|
+
generateRecoveryPlan,
|
|
8
|
+
recoveryToMilestones,
|
|
9
|
+
} from '../../src/workflow/audit-recovery.js';
|
|
10
|
+
import type { ProjectAuditReport, AuditFinding, SearchMetadata, AuditCategory } from '../../src/types/audit.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal audit report for testing.
|
|
14
|
+
*/
|
|
15
|
+
function makeReport(overrides: Partial<ProjectAuditReport> = {}): ProjectAuditReport {
|
|
16
|
+
return {
|
|
17
|
+
projectName: 'Test',
|
|
18
|
+
language: 'typescript',
|
|
19
|
+
auditedAt: '2024-01-01T00:00:00Z',
|
|
20
|
+
auditRunId: 'run-test',
|
|
21
|
+
summary: {
|
|
22
|
+
projectName: 'Test',
|
|
23
|
+
language: 'typescript',
|
|
24
|
+
totalSourceFiles: 10,
|
|
25
|
+
totalTestFiles: 3,
|
|
26
|
+
totalLinesOfCode: 500,
|
|
27
|
+
totalLinesOfTests: 100,
|
|
28
|
+
componentCount: 1,
|
|
29
|
+
detectedComposition: ['frontend'],
|
|
30
|
+
entryPointCount: 1,
|
|
31
|
+
routeCount: 1,
|
|
32
|
+
dependencyCount: 5,
|
|
33
|
+
hasDocker: false,
|
|
34
|
+
hasEnvExample: true,
|
|
35
|
+
hasCiConfig: false,
|
|
36
|
+
},
|
|
37
|
+
findings: [],
|
|
38
|
+
overallScore: 90,
|
|
39
|
+
categoryScores: {} as Record<AuditCategory, number>,
|
|
40
|
+
criticalCount: 0,
|
|
41
|
+
majorCount: 0,
|
|
42
|
+
minorCount: 0,
|
|
43
|
+
infoCount: 0,
|
|
44
|
+
passedChecks: [],
|
|
45
|
+
searchMetadata: {
|
|
46
|
+
serenaUsed: false,
|
|
47
|
+
serenaRetries: 0,
|
|
48
|
+
serenaErrors: [],
|
|
49
|
+
fallbackUsed: false,
|
|
50
|
+
fallbackTool: '',
|
|
51
|
+
searchQueries: [],
|
|
52
|
+
},
|
|
53
|
+
recommendation: 'pass',
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeFinding(overrides: Partial<AuditFinding> = {}): AuditFinding {
|
|
59
|
+
return {
|
|
60
|
+
id: 'AUD-001',
|
|
61
|
+
category: 'test-coverage',
|
|
62
|
+
severity: 'major',
|
|
63
|
+
title: 'Low test coverage',
|
|
64
|
+
description: 'Coverage is below threshold.',
|
|
65
|
+
evidence: [{ file: 'src/auth/login.ts', description: 'No tests' }],
|
|
66
|
+
recommendation: 'Add tests for auth module.',
|
|
67
|
+
autoFixable: false,
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// shouldTriggerRecovery
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
describe('shouldTriggerRecovery', () => {
|
|
77
|
+
it('should trigger when critical findings exist', () => {
|
|
78
|
+
const report = makeReport({ criticalCount: 1, overallScore: 80 });
|
|
79
|
+
expect(shouldTriggerRecovery(report, false)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should trigger in strict mode when major findings exist', () => {
|
|
83
|
+
const report = makeReport({ majorCount: 1, criticalCount: 0, overallScore: 85 });
|
|
84
|
+
expect(shouldTriggerRecovery(report, true)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should NOT trigger in non-strict mode for major-only findings with high score', () => {
|
|
88
|
+
const report = makeReport({ majorCount: 1, criticalCount: 0, overallScore: 85 });
|
|
89
|
+
expect(shouldTriggerRecovery(report, false)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should trigger when score is low and actionable findings exist', () => {
|
|
93
|
+
const findings = [makeFinding({ autoFixable: true })];
|
|
94
|
+
const report = makeReport({ overallScore: 60, findings, criticalCount: 0, majorCount: 0 });
|
|
95
|
+
expect(shouldTriggerRecovery(report, false)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should trigger when score is low and category is actionable', () => {
|
|
99
|
+
const findings = [makeFinding({ category: 'integration-wiring', autoFixable: false })];
|
|
100
|
+
const report = makeReport({ overallScore: 60, findings, criticalCount: 0, majorCount: 0 });
|
|
101
|
+
expect(shouldTriggerRecovery(report, false)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should NOT trigger for info-only findings', () => {
|
|
105
|
+
const findings = [makeFinding({ severity: 'info', category: 'documentation' })];
|
|
106
|
+
const report = makeReport({ overallScore: 90, findings, infoCount: 1, criticalCount: 0, majorCount: 0 });
|
|
107
|
+
expect(shouldTriggerRecovery(report, false)).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should NOT trigger when everything passes', () => {
|
|
111
|
+
const report = makeReport({ overallScore: 95, criticalCount: 0, majorCount: 0 });
|
|
112
|
+
expect(shouldTriggerRecovery(report, false)).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// generateRecoveryPlan
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
describe('generateRecoveryPlan', () => {
|
|
121
|
+
it('should create separate milestone for critical findings', () => {
|
|
122
|
+
const findings = [
|
|
123
|
+
makeFinding({ id: 'AUD-001', severity: 'critical', title: 'XSS vulnerability' }),
|
|
124
|
+
makeFinding({ id: 'AUD-002', severity: 'major', title: 'Low coverage' }),
|
|
125
|
+
];
|
|
126
|
+
const report = makeReport({ findings, criticalCount: 1, majorCount: 1 });
|
|
127
|
+
const plan = generateRecoveryPlan(report);
|
|
128
|
+
|
|
129
|
+
expect(plan.milestones.length).toBeGreaterThanOrEqual(2);
|
|
130
|
+
expect(plan.milestones[0].name).toContain('[RECOVERY] Critical Fixes');
|
|
131
|
+
expect(plan.milestones[0].tasks).toHaveLength(1);
|
|
132
|
+
expect(plan.milestones[1].name).toContain('[RECOVERY] Major Improvements');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should include polish milestone for auto-fixable minor findings', () => {
|
|
136
|
+
const findings = [
|
|
137
|
+
makeFinding({ id: 'AUD-001', severity: 'minor', autoFixable: true, title: 'Missing README' }),
|
|
138
|
+
];
|
|
139
|
+
const report = makeReport({ findings, minorCount: 1 });
|
|
140
|
+
const plan = generateRecoveryPlan(report);
|
|
141
|
+
|
|
142
|
+
const polishMs = plan.milestones.find((m) => m.name.includes('Polish'));
|
|
143
|
+
expect(polishMs).toBeDefined();
|
|
144
|
+
expect(polishMs?.tasks).toHaveLength(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should NOT include non-autoFixable minor findings in polish', () => {
|
|
148
|
+
const findings = [
|
|
149
|
+
makeFinding({ id: 'AUD-001', severity: 'minor', autoFixable: false }),
|
|
150
|
+
];
|
|
151
|
+
const report = makeReport({ findings, minorCount: 1 });
|
|
152
|
+
const plan = generateRecoveryPlan(report);
|
|
153
|
+
|
|
154
|
+
expect(plan.milestones).toHaveLength(0);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('should set appTarget on every recovery task', () => {
|
|
158
|
+
const findings = [
|
|
159
|
+
makeFinding({
|
|
160
|
+
id: 'AUD-001',
|
|
161
|
+
severity: 'critical',
|
|
162
|
+
evidence: [{ file: 'apps/backend/main.py', description: 'Issue here' }],
|
|
163
|
+
}),
|
|
164
|
+
];
|
|
165
|
+
const report = makeReport({ findings, criticalCount: 1 });
|
|
166
|
+
const plan = generateRecoveryPlan(report);
|
|
167
|
+
|
|
168
|
+
for (const ms of plan.milestones) {
|
|
169
|
+
for (const task of ms.tasks) {
|
|
170
|
+
expect(task.appTarget).toBeTruthy();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
expect(plan.milestones[0].tasks[0].appTarget).toBe('backend');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should provide estimated effort', () => {
|
|
177
|
+
const findings = [
|
|
178
|
+
makeFinding({ id: 'AUD-001', severity: 'critical' }),
|
|
179
|
+
];
|
|
180
|
+
const report = makeReport({ findings, criticalCount: 1 });
|
|
181
|
+
const plan = generateRecoveryPlan(report);
|
|
182
|
+
expect(plan.estimatedEffort).toBeTruthy();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// recoveryToMilestones
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
describe('recoveryToMilestones', () => {
|
|
191
|
+
it('should convert recovery milestones to the correct shape', () => {
|
|
192
|
+
const findings = [
|
|
193
|
+
makeFinding({ id: 'AUD-001', severity: 'critical', title: 'Fix auth' }),
|
|
194
|
+
];
|
|
195
|
+
const report = makeReport({ findings, criticalCount: 1 });
|
|
196
|
+
const plan = generateRecoveryPlan(report);
|
|
197
|
+
const milestones = recoveryToMilestones(plan, 'typescript');
|
|
198
|
+
|
|
199
|
+
expect(milestones).toHaveLength(1);
|
|
200
|
+
expect(milestones[0].name).toContain('[RECOVERY]');
|
|
201
|
+
expect(milestones[0].status).toBe('pending');
|
|
202
|
+
expect(milestones[0].tasks.length).toBeGreaterThan(0);
|
|
203
|
+
|
|
204
|
+
for (const task of milestones[0].tasks) {
|
|
205
|
+
expect(task.status).toBe('pending');
|
|
206
|
+
expect(task.name).toBeTruthy();
|
|
207
|
+
expect(task.description).toBeTruthy();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should preserve [RECOVERY] prefix on milestone names', () => {
|
|
212
|
+
const findings = [
|
|
213
|
+
makeFinding({ id: 'AUD-001', severity: 'major' }),
|
|
214
|
+
makeFinding({ id: 'AUD-002', severity: 'minor', autoFixable: true }),
|
|
215
|
+
];
|
|
216
|
+
const report = makeReport({ findings, majorCount: 1, minorCount: 1 });
|
|
217
|
+
const plan = generateRecoveryPlan(report);
|
|
218
|
+
const milestones = recoveryToMilestones(plan, 'python');
|
|
219
|
+
|
|
220
|
+
for (const ms of milestones) {
|
|
221
|
+
expect(ms.name.startsWith('[RECOVERY]')).toBe(true);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('should include acceptance criteria in task description', () => {
|
|
226
|
+
const findings = [
|
|
227
|
+
makeFinding({ id: 'AUD-001', severity: 'critical', recommendation: 'Use parameterized queries' }),
|
|
228
|
+
];
|
|
229
|
+
const report = makeReport({ findings, criticalCount: 1 });
|
|
230
|
+
const plan = generateRecoveryPlan(report);
|
|
231
|
+
const milestones = recoveryToMilestones(plan, 'typescript');
|
|
232
|
+
|
|
233
|
+
const taskDesc = milestones[0].tasks[0].description;
|
|
234
|
+
expect(taskDesc).toContain('Acceptance criteria');
|
|
235
|
+
expect(taskDesc).toContain('parameterized queries');
|
|
236
|
+
});
|
|
237
|
+
});
|