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.
Files changed (174) hide show
  1. package/README.md +148 -7
  2. package/cheatsheet.md +440 -0
  3. package/dist/cli/commands/db.d.ts +10 -0
  4. package/dist/cli/commands/db.d.ts.map +1 -0
  5. package/dist/cli/commands/db.js +240 -0
  6. package/dist/cli/commands/db.js.map +1 -0
  7. package/dist/cli/commands/doctor.d.ts +18 -0
  8. package/dist/cli/commands/doctor.d.ts.map +1 -0
  9. package/dist/cli/commands/doctor.js +255 -0
  10. package/dist/cli/commands/doctor.js.map +1 -0
  11. package/dist/cli/commands/index.d.ts +3 -0
  12. package/dist/cli/commands/index.d.ts.map +1 -1
  13. package/dist/cli/commands/index.js +3 -0
  14. package/dist/cli/commands/index.js.map +1 -1
  15. package/dist/cli/commands/review.d.ts +31 -0
  16. package/dist/cli/commands/review.d.ts.map +1 -0
  17. package/dist/cli/commands/review.js +156 -0
  18. package/dist/cli/commands/review.js.map +1 -0
  19. package/dist/cli/index.d.ts.map +1 -1
  20. package/dist/cli/index.js +4 -1
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/interactive.d.ts.map +1 -1
  23. package/dist/cli/interactive.js +218 -61
  24. package/dist/cli/interactive.js.map +1 -1
  25. package/dist/generators/admin-wizard.d.ts +25 -0
  26. package/dist/generators/admin-wizard.d.ts.map +1 -0
  27. package/dist/generators/admin-wizard.js +123 -0
  28. package/dist/generators/admin-wizard.js.map +1 -0
  29. package/dist/generators/all.d.ts.map +1 -1
  30. package/dist/generators/all.js +10 -3
  31. package/dist/generators/all.js.map +1 -1
  32. package/dist/generators/database.d.ts +58 -0
  33. package/dist/generators/database.d.ts.map +1 -0
  34. package/dist/generators/database.js +229 -0
  35. package/dist/generators/database.js.map +1 -0
  36. package/dist/generators/fullstack.d.ts.map +1 -1
  37. package/dist/generators/fullstack.js +23 -7
  38. package/dist/generators/fullstack.js.map +1 -1
  39. package/dist/generators/index.d.ts +2 -0
  40. package/dist/generators/index.d.ts.map +1 -1
  41. package/dist/generators/index.js +2 -0
  42. package/dist/generators/index.js.map +1 -1
  43. package/dist/generators/templates/admin-wizard-python.d.ts +32 -0
  44. package/dist/generators/templates/admin-wizard-python.d.ts.map +1 -0
  45. package/dist/generators/templates/admin-wizard-python.js +425 -0
  46. package/dist/generators/templates/admin-wizard-python.js.map +1 -0
  47. package/dist/generators/templates/admin-wizard-react.d.ts +48 -0
  48. package/dist/generators/templates/admin-wizard-react.d.ts.map +1 -0
  49. package/dist/generators/templates/admin-wizard-react.js +554 -0
  50. package/dist/generators/templates/admin-wizard-react.js.map +1 -0
  51. package/dist/generators/templates/database-docker.d.ts +23 -0
  52. package/dist/generators/templates/database-docker.d.ts.map +1 -0
  53. package/dist/generators/templates/database-docker.js +221 -0
  54. package/dist/generators/templates/database-docker.js.map +1 -0
  55. package/dist/generators/templates/database-python.d.ts +54 -0
  56. package/dist/generators/templates/database-python.d.ts.map +1 -0
  57. package/dist/generators/templates/database-python.js +723 -0
  58. package/dist/generators/templates/database-python.js.map +1 -0
  59. package/dist/generators/templates/database-typescript.d.ts +34 -0
  60. package/dist/generators/templates/database-typescript.d.ts.map +1 -0
  61. package/dist/generators/templates/database-typescript.js +232 -0
  62. package/dist/generators/templates/database-typescript.js.map +1 -0
  63. package/dist/generators/templates/fullstack.d.ts.map +1 -1
  64. package/dist/generators/templates/fullstack.js +29 -0
  65. package/dist/generators/templates/fullstack.js.map +1 -1
  66. package/dist/generators/templates/index.d.ts +5 -0
  67. package/dist/generators/templates/index.d.ts.map +1 -1
  68. package/dist/generators/templates/index.js +5 -0
  69. package/dist/generators/templates/index.js.map +1 -1
  70. package/dist/state/index.d.ts +10 -0
  71. package/dist/state/index.d.ts.map +1 -1
  72. package/dist/state/index.js +21 -0
  73. package/dist/state/index.js.map +1 -1
  74. package/dist/types/audit.d.ts +623 -0
  75. package/dist/types/audit.d.ts.map +1 -0
  76. package/dist/types/audit.js +240 -0
  77. package/dist/types/audit.js.map +1 -0
  78. package/dist/types/database-runtime.d.ts +86 -0
  79. package/dist/types/database-runtime.d.ts.map +1 -0
  80. package/dist/types/database-runtime.js +61 -0
  81. package/dist/types/database-runtime.js.map +1 -0
  82. package/dist/types/database.d.ts +85 -0
  83. package/dist/types/database.d.ts.map +1 -0
  84. package/dist/types/database.js +71 -0
  85. package/dist/types/database.js.map +1 -0
  86. package/dist/types/index.d.ts +2 -0
  87. package/dist/types/index.d.ts.map +1 -1
  88. package/dist/types/index.js +4 -0
  89. package/dist/types/index.js.map +1 -1
  90. package/dist/types/workflow.d.ts +36 -0
  91. package/dist/types/workflow.d.ts.map +1 -1
  92. package/dist/types/workflow.js +7 -0
  93. package/dist/types/workflow.js.map +1 -1
  94. package/dist/workflow/audit-analyzer.d.ts +58 -0
  95. package/dist/workflow/audit-analyzer.d.ts.map +1 -0
  96. package/dist/workflow/audit-analyzer.js +420 -0
  97. package/dist/workflow/audit-analyzer.js.map +1 -0
  98. package/dist/workflow/audit-mode.d.ts +28 -0
  99. package/dist/workflow/audit-mode.d.ts.map +1 -0
  100. package/dist/workflow/audit-mode.js +169 -0
  101. package/dist/workflow/audit-mode.js.map +1 -0
  102. package/dist/workflow/audit-recovery.d.ts +61 -0
  103. package/dist/workflow/audit-recovery.d.ts.map +1 -0
  104. package/dist/workflow/audit-recovery.js +242 -0
  105. package/dist/workflow/audit-recovery.js.map +1 -0
  106. package/dist/workflow/audit-reporter.d.ts +65 -0
  107. package/dist/workflow/audit-reporter.d.ts.map +1 -0
  108. package/dist/workflow/audit-reporter.js +301 -0
  109. package/dist/workflow/audit-reporter.js.map +1 -0
  110. package/dist/workflow/audit-scanner.d.ts +87 -0
  111. package/dist/workflow/audit-scanner.d.ts.map +1 -0
  112. package/dist/workflow/audit-scanner.js +768 -0
  113. package/dist/workflow/audit-scanner.js.map +1 -0
  114. package/dist/workflow/db-setup-runner.d.ts +63 -0
  115. package/dist/workflow/db-setup-runner.d.ts.map +1 -0
  116. package/dist/workflow/db-setup-runner.js +336 -0
  117. package/dist/workflow/db-setup-runner.js.map +1 -0
  118. package/dist/workflow/db-state-machine.d.ts +30 -0
  119. package/dist/workflow/db-state-machine.d.ts.map +1 -0
  120. package/dist/workflow/db-state-machine.js +51 -0
  121. package/dist/workflow/db-state-machine.js.map +1 -0
  122. package/dist/workflow/index.d.ts +7 -0
  123. package/dist/workflow/index.d.ts.map +1 -1
  124. package/dist/workflow/index.js +7 -0
  125. package/dist/workflow/index.js.map +1 -1
  126. package/package.json +1 -1
  127. package/src/cli/commands/db.ts +281 -0
  128. package/src/cli/commands/doctor.ts +273 -0
  129. package/src/cli/commands/index.ts +3 -0
  130. package/src/cli/commands/review.ts +187 -0
  131. package/src/cli/index.ts +6 -0
  132. package/src/cli/interactive.ts +174 -4
  133. package/src/generators/admin-wizard.ts +146 -0
  134. package/src/generators/all.ts +10 -3
  135. package/src/generators/database.ts +286 -0
  136. package/src/generators/fullstack.ts +26 -9
  137. package/src/generators/index.ts +12 -0
  138. package/src/generators/templates/admin-wizard-python.ts +431 -0
  139. package/src/generators/templates/admin-wizard-react.ts +560 -0
  140. package/src/generators/templates/database-docker.ts +227 -0
  141. package/src/generators/templates/database-python.ts +734 -0
  142. package/src/generators/templates/database-typescript.ts +238 -0
  143. package/src/generators/templates/fullstack.ts +29 -0
  144. package/src/generators/templates/index.ts +5 -0
  145. package/src/state/index.ts +28 -0
  146. package/src/types/audit.ts +294 -0
  147. package/src/types/database-runtime.ts +69 -0
  148. package/src/types/database.ts +84 -0
  149. package/src/types/index.ts +29 -0
  150. package/src/types/workflow.ts +20 -0
  151. package/src/workflow/audit-analyzer.ts +491 -0
  152. package/src/workflow/audit-mode.ts +240 -0
  153. package/src/workflow/audit-recovery.ts +284 -0
  154. package/src/workflow/audit-reporter.ts +370 -0
  155. package/src/workflow/audit-scanner.ts +873 -0
  156. package/src/workflow/db-setup-runner.ts +391 -0
  157. package/src/workflow/db-state-machine.ts +58 -0
  158. package/src/workflow/index.ts +7 -0
  159. package/tests/cli/commands/review.test.ts +52 -0
  160. package/tests/generators/admin-wizard-orchestrator.test.ts +64 -0
  161. package/tests/generators/admin-wizard-templates.test.ts +366 -0
  162. package/tests/generators/cross-phase-integration.test.ts +383 -0
  163. package/tests/generators/database.test.ts +456 -0
  164. package/tests/generators/fe-be-db-integration.test.ts +613 -0
  165. package/tests/types/audit.test.ts +250 -0
  166. package/tests/types/database-runtime.test.ts +158 -0
  167. package/tests/types/database.test.ts +187 -0
  168. package/tests/workflow/audit-analyzer.test.ts +281 -0
  169. package/tests/workflow/audit-mode.test.ts +114 -0
  170. package/tests/workflow/audit-recovery.test.ts +237 -0
  171. package/tests/workflow/audit-reporter.test.ts +254 -0
  172. package/tests/workflow/audit-scanner.test.ts +270 -0
  173. package/tests/workflow/db-setup-runner.test.ts +211 -0
  174. package/tests/workflow/db-state-machine.test.ts +117 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Tests for the audit reporter module.
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import {
9
+ buildSummaryReport,
10
+ buildAuditReport,
11
+ writeAuditMarkdown,
12
+ writeAuditJson,
13
+ writeRecoveryMarkdown,
14
+ writeRecoveryJson,
15
+ } from '../../src/workflow/audit-reporter.js';
16
+ import type { ProjectScanResult, AuditFinding, SearchMetadata, RecoveryPlan } from '../../src/types/audit.js';
17
+ import type { ProjectState } from '../../src/types/workflow.js';
18
+
19
+ let tmpDir: string;
20
+
21
+ beforeEach(async () => {
22
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'audit-report-'));
23
+ });
24
+
25
+ afterEach(async () => {
26
+ await fs.rm(tmpDir, { recursive: true, force: true });
27
+ });
28
+
29
+ function makeScan(overrides: Partial<ProjectScanResult> = {}): ProjectScanResult {
30
+ return {
31
+ tree: 'src/',
32
+ components: [],
33
+ detectedComposition: ['frontend'],
34
+ stateLanguage: 'typescript',
35
+ compositionMismatch: false,
36
+ sourceFiles: [],
37
+ testFiles: [],
38
+ configFiles: ['package.json', 'docker-compose.yml'],
39
+ entryPoints: ['src/main.ts'],
40
+ routeFiles: ['src/routes.ts'],
41
+ dependencies: [
42
+ { file: 'package.json', type: 'package.json', dependencies: { react: '^18' }, devDependencies: { vitest: '^1' } },
43
+ ],
44
+ totalSourceFiles: 15,
45
+ totalTestFiles: 5,
46
+ totalLinesOfCode: 800,
47
+ totalLinesOfTests: 200,
48
+ language: 'typescript',
49
+ docsIndex: [],
50
+ keyFileSnippets: [],
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function makeState(): ProjectState {
56
+ return {
57
+ id: 'test-id',
58
+ name: 'Test Project',
59
+ idea: 'A test project',
60
+ language: 'typescript',
61
+ openaiModel: 'gpt-4',
62
+ phase: 'complete',
63
+ status: 'complete',
64
+ milestones: [],
65
+ currentMilestone: null,
66
+ currentTask: null,
67
+ consensusHistory: [],
68
+ createdAt: '2024-01-01T00:00:00Z',
69
+ updatedAt: '2024-01-01T00:00:00Z',
70
+ } as ProjectState;
71
+ }
72
+
73
+ function makeMeta(): SearchMetadata {
74
+ return {
75
+ serenaUsed: true,
76
+ serenaRetries: 0,
77
+ serenaErrors: [],
78
+ fallbackUsed: false,
79
+ fallbackTool: '',
80
+ searchQueries: ['audit-analysis'],
81
+ };
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // buildSummaryReport
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe('buildSummaryReport', () => {
89
+ it('should produce a correct summary', () => {
90
+ const summary = buildSummaryReport(makeScan(), makeState());
91
+ expect(summary.projectName).toBe('Test Project');
92
+ expect(summary.totalSourceFiles).toBe(15);
93
+ expect(summary.totalTestFiles).toBe(5);
94
+ expect(summary.dependencyCount).toBe(2); // react + vitest
95
+ expect(summary.hasDocker).toBe(true);
96
+ expect(summary.entryPointCount).toBe(1);
97
+ expect(summary.routeCount).toBe(1);
98
+ });
99
+
100
+ it('should include AI overview when provided', () => {
101
+ const summary = buildSummaryReport(makeScan(), makeState(), 'Looks good overall.');
102
+ expect(summary.aiOverview).toBe('Looks good overall.');
103
+ });
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // buildAuditReport
108
+ // ---------------------------------------------------------------------------
109
+
110
+ describe('buildAuditReport', () => {
111
+ it('should produce a report with correct severity counts', () => {
112
+ const findings: AuditFinding[] = [
113
+ { id: 'AUD-001', category: 'security', severity: 'critical', title: 'XSS', description: 'Found XSS.', evidence: [], recommendation: 'Sanitize.', autoFixable: false },
114
+ { id: 'AUD-002', category: 'test-coverage', severity: 'major', title: 'Low coverage', description: 'Only 30%.', evidence: [], recommendation: 'Add tests.', autoFixable: false },
115
+ { id: 'AUD-003', category: 'documentation', severity: 'info', title: 'Missing comments', description: 'No JSDoc.', evidence: [], recommendation: 'Add JSDoc.', autoFixable: true },
116
+ ];
117
+ const scores = { overallScore: 72, categoryScores: {} as Record<string, number> };
118
+ const report = buildAuditReport(
119
+ buildSummaryReport(makeScan(), makeState()),
120
+ findings, scores as any, makeMeta(), { strict: false }, 'run-123'
121
+ );
122
+ expect(report.criticalCount).toBe(1);
123
+ expect(report.majorCount).toBe(1);
124
+ expect(report.infoCount).toBe(1);
125
+ expect(report.auditRunId).toBe('run-123');
126
+ expect(report.recommendation).toBe('fix-and-recheck');
127
+ });
128
+
129
+ it('should recommend pass when no critical and few major findings', () => {
130
+ const findings: AuditFinding[] = [
131
+ { id: 'AUD-001', category: 'documentation', severity: 'minor', title: 'Minor', description: 'Small.', evidence: [], recommendation: 'OK.', autoFixable: true },
132
+ ];
133
+ const scores = { overallScore: 95, categoryScores: {} as Record<string, number> };
134
+ const report = buildAuditReport(
135
+ buildSummaryReport(makeScan(), makeState()),
136
+ findings, scores as any, makeMeta(), { strict: false }, 'run-456'
137
+ );
138
+ expect(report.recommendation).toBe('pass');
139
+ });
140
+
141
+ it('should recommend major-rework for many critical findings', () => {
142
+ const findings = Array.from({ length: 4 }, (_, i) => ({
143
+ id: `AUD-${i}`,
144
+ category: 'security' as const,
145
+ severity: 'critical' as const,
146
+ title: `Critical ${i}`,
147
+ description: 'Bad.',
148
+ evidence: [],
149
+ recommendation: 'Fix.',
150
+ autoFixable: false,
151
+ }));
152
+ const scores = { overallScore: 30, categoryScores: {} as Record<string, number> };
153
+ const report = buildAuditReport(
154
+ buildSummaryReport(makeScan(), makeState()),
155
+ findings, scores as any, makeMeta(), { strict: false }, 'run-789'
156
+ );
157
+ expect(report.recommendation).toBe('major-rework');
158
+ });
159
+
160
+ it('should include search metadata in report', () => {
161
+ const meta = makeMeta();
162
+ meta.serenaUsed = true;
163
+ meta.serenaRetries = 1;
164
+ const report = buildAuditReport(
165
+ buildSummaryReport(makeScan(), makeState()),
166
+ [], { overallScore: 100, categoryScores: {} as any }, meta, { strict: false }, 'run-x'
167
+ );
168
+ expect(report.searchMetadata.serenaUsed).toBe(true);
169
+ expect(report.searchMetadata.serenaRetries).toBe(1);
170
+ });
171
+ });
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // File writers
175
+ // ---------------------------------------------------------------------------
176
+
177
+ describe('writeAuditMarkdown', () => {
178
+ it('should write a valid markdown file', async () => {
179
+ const report = buildAuditReport(
180
+ buildSummaryReport(makeScan(), makeState()),
181
+ [], { overallScore: 100, categoryScores: {} as any }, makeMeta(), { strict: false }, 'run-1'
182
+ );
183
+ const filePath = await writeAuditMarkdown(tmpDir, report);
184
+ expect(filePath).toContain('popeye.audit.md');
185
+ const content = await fs.readFile(filePath, 'utf-8');
186
+ expect(content).toContain('# Audit Report');
187
+ expect(content).toContain('Test Project');
188
+ });
189
+ });
190
+
191
+ describe('writeAuditJson', () => {
192
+ it('should write valid JSON', async () => {
193
+ const report = buildAuditReport(
194
+ buildSummaryReport(makeScan(), makeState()),
195
+ [], { overallScore: 100, categoryScores: {} as any }, makeMeta(), { strict: false }, 'run-2'
196
+ );
197
+ const filePath = await writeAuditJson(tmpDir, report);
198
+ const content = await fs.readFile(filePath, 'utf-8');
199
+ const parsed = JSON.parse(content);
200
+ expect(parsed.projectName).toBe('Test Project');
201
+ expect(parsed.overallScore).toBe(100);
202
+ });
203
+ });
204
+
205
+ describe('writeRecoveryMarkdown', () => {
206
+ it('should write recovery plan markdown', async () => {
207
+ const recovery: RecoveryPlan = {
208
+ generatedAt: new Date().toISOString(),
209
+ auditScore: 60,
210
+ auditRunId: 'run-3',
211
+ totalFindings: 5,
212
+ criticalFindings: 1,
213
+ milestones: [
214
+ {
215
+ name: '[RECOVERY] Critical Fixes',
216
+ description: 'Fix critical issues.',
217
+ tasks: [
218
+ {
219
+ name: 'Fix SQL injection',
220
+ description: 'Sanitize inputs.',
221
+ findingIds: ['AUD-001'],
222
+ acceptanceCriteria: ['No raw SQL in handlers'],
223
+ appTarget: 'backend',
224
+ },
225
+ ],
226
+ },
227
+ ],
228
+ estimatedEffort: '2-4 hours',
229
+ };
230
+ const filePath = await writeRecoveryMarkdown(tmpDir, recovery);
231
+ const content = await fs.readFile(filePath, 'utf-8');
232
+ expect(content).toContain('# Recovery Plan');
233
+ expect(content).toContain('[RECOVERY] Critical Fixes');
234
+ expect(content).toContain('Fix SQL injection');
235
+ });
236
+ });
237
+
238
+ describe('writeRecoveryJson', () => {
239
+ it('should write valid recovery JSON', async () => {
240
+ const recovery: RecoveryPlan = {
241
+ generatedAt: new Date().toISOString(),
242
+ auditScore: 60,
243
+ auditRunId: 'run-4',
244
+ totalFindings: 2,
245
+ criticalFindings: 0,
246
+ milestones: [],
247
+ estimatedEffort: '1 hour',
248
+ };
249
+ const filePath = await writeRecoveryJson(tmpDir, recovery);
250
+ const content = await fs.readFile(filePath, 'utf-8');
251
+ const parsed = JSON.parse(content);
252
+ expect(parsed.auditRunId).toBe('run-4');
253
+ });
254
+ });
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Tests for the audit scanner module.
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
5
+ import { promises as fs } from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import {
9
+ detectWorkspaceComposition,
10
+ scanComponent,
11
+ readPriorityDocs,
12
+ buildWiringMatrix,
13
+ scanProject,
14
+ countLines,
15
+ } from '../../src/workflow/audit-scanner.js';
16
+
17
+ let tmpDir: string;
18
+
19
+ beforeEach(async () => {
20
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'audit-scan-'));
21
+ });
22
+
23
+ afterEach(async () => {
24
+ await fs.rm(tmpDir, { recursive: true, force: true });
25
+ });
26
+
27
+ /**
28
+ * Helper to create a file in the tmp directory, including intermediate dirs.
29
+ */
30
+ async function createFile(relativePath: string, content = ''): Promise<void> {
31
+ const abs = path.join(tmpDir, relativePath);
32
+ await fs.mkdir(path.dirname(abs), { recursive: true });
33
+ await fs.writeFile(abs, content);
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // detectWorkspaceComposition
38
+ // ---------------------------------------------------------------------------
39
+
40
+ describe('detectWorkspaceComposition', () => {
41
+ it('should detect a fullstack workspace (frontend + backend)', async () => {
42
+ await createFile('apps/frontend/package.json', '{}');
43
+ await createFile('apps/backend/requirements.txt', 'fastapi\n');
44
+
45
+ const kinds = await detectWorkspaceComposition(tmpDir);
46
+ expect(kinds).toContain('frontend');
47
+ expect(kinds).toContain('backend');
48
+ });
49
+
50
+ it('should detect a single python project', async () => {
51
+ await createFile('requirements.txt', 'flask\n');
52
+
53
+ const kinds = await detectWorkspaceComposition(tmpDir);
54
+ expect(kinds).toContain('backend');
55
+ expect(kinds).not.toContain('frontend');
56
+ });
57
+
58
+ it('should detect a single typescript project', async () => {
59
+ await createFile('package.json', '{}');
60
+
61
+ const kinds = await detectWorkspaceComposition(tmpDir);
62
+ expect(kinds).toContain('frontend');
63
+ expect(kinds).not.toContain('backend');
64
+ });
65
+
66
+ it('should detect website component', async () => {
67
+ await createFile('apps/frontend/package.json', '{}');
68
+ await createFile('apps/backend/requirements.txt', 'fastapi\n');
69
+ await createFile('apps/website/package.json', '{}');
70
+
71
+ const kinds = await detectWorkspaceComposition(tmpDir);
72
+ expect(kinds).toContain('website');
73
+ });
74
+
75
+ it('should detect infra from docker-compose.yml', async () => {
76
+ await createFile('package.json', '{}');
77
+ await createFile('docker-compose.yml', 'version: "3"\n');
78
+
79
+ const kinds = await detectWorkspaceComposition(tmpDir);
80
+ expect(kinds).toContain('infra');
81
+ });
82
+
83
+ it('should detect shared from packages/ directory', async () => {
84
+ await createFile('package.json', '{}');
85
+ await createFile('packages/shared/index.ts', '');
86
+
87
+ const kinds = await detectWorkspaceComposition(tmpDir);
88
+ expect(kinds).toContain('shared');
89
+ });
90
+
91
+ it('should return empty array for empty directory', async () => {
92
+ const kinds = await detectWorkspaceComposition(tmpDir);
93
+ expect(kinds).toEqual([]);
94
+ });
95
+ });
96
+
97
+ // ---------------------------------------------------------------------------
98
+ // scanComponent
99
+ // ---------------------------------------------------------------------------
100
+
101
+ describe('scanComponent', () => {
102
+ it('should scan a typescript component and find source/test files', async () => {
103
+ await createFile('src/main.tsx', 'export default function App() {}');
104
+ await createFile('src/utils.ts', 'export const x = 1;');
105
+ await createFile('tests/main.test.tsx', 'test("works", () => {})');
106
+ await createFile('package.json', JSON.stringify({
107
+ dependencies: { react: '^18.0.0' },
108
+ }));
109
+
110
+ const result = await scanComponent(tmpDir, 'frontend', 'typescript');
111
+ expect(result.kind).toBe('frontend');
112
+ expect(result.framework).toBe('react');
113
+ expect(result.sourceFiles.length).toBeGreaterThanOrEqual(2);
114
+ expect(result.testFiles.length).toBeGreaterThanOrEqual(1);
115
+ expect(result.entryPoints).toContain('src/main.tsx');
116
+ });
117
+
118
+ it('should scan a python backend component', async () => {
119
+ await createFile('app.py', 'from fastapi import FastAPI\napp = FastAPI()\n');
120
+ await createFile('routes.py', 'pass');
121
+ await createFile('requirements.txt', 'fastapi==0.100.0\n');
122
+
123
+ const result = await scanComponent(tmpDir, 'backend', 'python');
124
+ expect(result.kind).toBe('backend');
125
+ expect(result.framework).toBe('fastapi');
126
+ expect(result.entryPoints).toContain('app.py');
127
+ expect(result.routeFiles).toContain('routes.py');
128
+ });
129
+
130
+ it('should handle empty directory', async () => {
131
+ const result = await scanComponent(tmpDir, 'frontend', 'typescript');
132
+ expect(result.sourceFiles).toEqual([]);
133
+ expect(result.testFiles).toEqual([]);
134
+ });
135
+ });
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // readPriorityDocs
139
+ // ---------------------------------------------------------------------------
140
+
141
+ describe('readPriorityDocs', () => {
142
+ it('should read CLAUDE.md first, then README, then other docs', async () => {
143
+ await createFile('CLAUDE.md', '# Claude Instructions');
144
+ await createFile('README.md', '# My Project');
145
+ await createFile('CONTRIBUTING.md', '# Contributing');
146
+ await createFile('docs/architecture.md', '# Architecture');
147
+
148
+ const result = await readPriorityDocs(tmpDir);
149
+ expect(result.claudeMd).toContain('Claude Instructions');
150
+ expect(result.readme).toContain('My Project');
151
+ expect(result.docsIndex[0]).toBe('CLAUDE.md');
152
+ expect(result.docsIndex[1]).toBe('README.md');
153
+ expect(result.docsIndex).toContain('CONTRIBUTING.md');
154
+ expect(result.docsIndex).toContain(path.join('docs', 'architecture.md'));
155
+ });
156
+
157
+ it('should handle missing docs gracefully', async () => {
158
+ const result = await readPriorityDocs(tmpDir);
159
+ expect(result.claudeMd).toBeUndefined();
160
+ expect(result.readme).toBeUndefined();
161
+ expect(result.docsIndex).toEqual([]);
162
+ });
163
+ });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // buildWiringMatrix
167
+ // ---------------------------------------------------------------------------
168
+
169
+ describe('buildWiringMatrix', () => {
170
+ it('should detect CORS mismatch', async () => {
171
+ await createFile('.env.example', 'VITE_API_URL=http://localhost:8000\nDATABASE_URL=postgres://x\n');
172
+ await createFile('apps/backend/main.py', `
173
+ cors_origins = ["http://localhost:5173"]
174
+ app.add_middleware(CORSMiddleware, allow_origins=cors_origins)
175
+ `);
176
+
177
+ const components = [
178
+ {
179
+ kind: 'frontend' as const,
180
+ rootDir: 'apps/frontend',
181
+ language: 'typescript' as const,
182
+ entryPoints: [],
183
+ routeFiles: [],
184
+ testFiles: [],
185
+ sourceFiles: [],
186
+ dependencyManifests: [],
187
+ },
188
+ {
189
+ kind: 'backend' as const,
190
+ rootDir: 'apps/backend',
191
+ language: 'python' as const,
192
+ entryPoints: [],
193
+ routeFiles: [],
194
+ testFiles: [],
195
+ sourceFiles: [],
196
+ dependencyManifests: [],
197
+ },
198
+ ];
199
+
200
+ const wiring = await buildWiringMatrix(tmpDir, components);
201
+ expect(wiring.frontendApiBaseEnvKeys).toContain('VITE_API_URL');
202
+ expect(wiring.frontendApiBaseResolved).toBe('http://localhost:8000');
203
+ expect(wiring.backendCorsOrigins).toContain('http://localhost:5173');
204
+ // The FE expects :8000 but CORS only has :5173 — mismatch detected
205
+ expect(wiring.potentialMismatches.length).toBeGreaterThanOrEqual(1);
206
+ });
207
+
208
+ it('should return empty matrix when no env keys found', async () => {
209
+ const wiring = await buildWiringMatrix(tmpDir, []);
210
+ expect(wiring.frontendApiBaseEnvKeys).toEqual([]);
211
+ expect(wiring.potentialMismatches).toEqual([]);
212
+ });
213
+ });
214
+
215
+ // ---------------------------------------------------------------------------
216
+ // countLines
217
+ // ---------------------------------------------------------------------------
218
+
219
+ describe('countLines', () => {
220
+ it('should count lines in source and test files', async () => {
221
+ await createFile('src/index.ts', 'line1\nline2\nline3\n');
222
+ await createFile('tests/index.test.ts', 'test1\ntest2\n');
223
+
224
+ const source = [{ path: 'src/index.ts', extension: '.ts' }];
225
+ const tests = [{ path: 'tests/index.test.ts', extension: '.ts' }];
226
+ const result = await countLines(source, tests, tmpDir);
227
+ expect(result.code).toBe(4); // 3 lines + trailing newline = 4
228
+ expect(result.tests).toBe(3);
229
+ });
230
+ });
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // scanProject (integration)
234
+ // ---------------------------------------------------------------------------
235
+
236
+ describe('scanProject', () => {
237
+ it('should produce a complete scan result for a typescript project', async () => {
238
+ await createFile('package.json', JSON.stringify({
239
+ name: 'test-project',
240
+ dependencies: { react: '^18.0.0' },
241
+ }));
242
+ await createFile('src/main.tsx', 'export default function App() { return <div/>; }');
243
+ await createFile('src/utils.ts', 'export const add = (a: number, b: number) => a + b;');
244
+ await createFile('tests/utils.test.ts', 'test("add", () => {})');
245
+ await createFile('README.md', '# Test Project');
246
+
247
+ const result = await scanProject(tmpDir, 'typescript');
248
+ expect(result.stateLanguage).toBe('typescript');
249
+ expect(result.totalSourceFiles).toBeGreaterThanOrEqual(2);
250
+ expect(result.totalTestFiles).toBeGreaterThanOrEqual(1);
251
+ expect(result.readmeContent).toContain('Test Project');
252
+ expect(result.components.length).toBeGreaterThanOrEqual(1);
253
+ expect(result.tree).toBeTruthy();
254
+ });
255
+
256
+ it('should detect composition mismatch', async () => {
257
+ // State says fullstack but only a frontend package.json exists
258
+ await createFile('package.json', '{}');
259
+ const result = await scanProject(tmpDir, 'fullstack');
260
+ expect(result.compositionMismatch).toBe(true);
261
+ });
262
+
263
+ it('should call progress callback', async () => {
264
+ await createFile('package.json', '{}');
265
+ const messages: string[] = [];
266
+ await scanProject(tmpDir, 'typescript', (msg) => messages.push(msg));
267
+ expect(messages.length).toBeGreaterThan(0);
268
+ expect(messages.some((m) => m.includes('Scan complete'))).toBe(true);
269
+ });
270
+ });