@hustle-together/api-dev-tools 3.0.0 → 3.2.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 (38) hide show
  1. package/README.md +71 -0
  2. package/bin/cli.js +184 -14
  3. package/demo/audio/generate-all-narrations.js +124 -59
  4. package/demo/audio/generate-narration.js +120 -56
  5. package/demo/audio/narration-adam-timing.json +3086 -2077
  6. package/demo/audio/narration-adam.mp3 +0 -0
  7. package/demo/audio/narration-creature-timing.json +3094 -2085
  8. package/demo/audio/narration-creature.mp3 +0 -0
  9. package/demo/audio/narration-gaming-timing.json +3091 -2082
  10. package/demo/audio/narration-gaming.mp3 +0 -0
  11. package/demo/audio/narration-hope-timing.json +3072 -2063
  12. package/demo/audio/narration-hope.mp3 +0 -0
  13. package/demo/audio/narration-mark-timing.json +3090 -2081
  14. package/demo/audio/narration-mark.mp3 +0 -0
  15. package/demo/audio/voices-manifest.json +16 -16
  16. package/demo/workflow-demo.html +1528 -411
  17. package/hooks/api-workflow-check.py +2 -0
  18. package/hooks/enforce-deep-research.py +180 -0
  19. package/hooks/enforce-disambiguation.py +149 -0
  20. package/hooks/enforce-documentation.py +187 -0
  21. package/hooks/enforce-environment.py +249 -0
  22. package/hooks/enforce-interview.py +64 -1
  23. package/hooks/enforce-refactor.py +187 -0
  24. package/hooks/enforce-research.py +93 -46
  25. package/hooks/enforce-schema.py +186 -0
  26. package/hooks/enforce-scope.py +156 -0
  27. package/hooks/enforce-tdd-red.py +246 -0
  28. package/hooks/enforce-verify.py +186 -0
  29. package/hooks/verify-after-green.py +136 -6
  30. package/package.json +2 -1
  31. package/scripts/collect-test-results.ts +404 -0
  32. package/scripts/extract-parameters.ts +483 -0
  33. package/scripts/generate-test-manifest.ts +520 -0
  34. package/templates/CLAUDE-SECTION.md +84 -0
  35. package/templates/api-dev-state.json +45 -5
  36. package/templates/api-test/page.tsx +315 -0
  37. package/templates/api-test/test-structure/route.ts +269 -0
  38. package/templates/settings.json +36 -0
@@ -13,17 +13,23 @@
13
13
  "status": "not_started",
14
14
  "clarified": null,
15
15
  "search_variations": [],
16
+ "user_question_asked": false,
17
+ "user_selected": null,
16
18
  "description": "Pre-research disambiguation to clarify ambiguous requests"
17
19
  },
18
20
  "scope": {
19
21
  "status": "not_started",
20
22
  "confirmed": false,
23
+ "user_question_asked": false,
24
+ "user_confirmed": false,
21
25
  "description": "Initial scope understanding and confirmation"
22
26
  },
23
27
  "research_initial": {
24
28
  "status": "not_started",
25
29
  "sources": [],
26
- "summary_approved": false,
30
+ "summary_shown": false,
31
+ "user_question_asked": false,
32
+ "user_approved": false,
27
33
  "description": "Context7/WebSearch research for live documentation"
28
34
  },
29
35
  "interview": {
@@ -32,6 +38,8 @@
32
38
  "user_question_count": 0,
33
39
  "structured_question_count": 0,
34
40
  "decisions": {},
41
+ "user_question_asked": false,
42
+ "user_completed": false,
35
43
  "description": "Structured interview about requirements (generated FROM research)"
36
44
  },
37
45
  "research_deep": {
@@ -39,27 +47,40 @@
39
47
  "sources": [],
40
48
  "proposed_searches": [],
41
49
  "approved_searches": [],
50
+ "executed_searches": [],
42
51
  "skipped_searches": [],
52
+ "proposals_shown": false,
53
+ "user_question_asked": false,
54
+ "user_approved": false,
43
55
  "description": "Deep dive based on interview answers (adaptive, not shotgun)"
44
56
  },
45
57
  "schema_creation": {
46
58
  "status": "not_started",
47
59
  "schema_file": null,
48
- "schema_approved": false,
60
+ "fields_count": 0,
61
+ "schema_shown": false,
62
+ "user_question_asked": false,
63
+ "user_confirmed": false,
49
64
  "description": "Zod schema creation from research"
50
65
  },
51
66
  "environment_check": {
52
67
  "status": "not_started",
53
- "keys_verified": [],
68
+ "keys_required": [],
69
+ "keys_found": [],
54
70
  "keys_missing": [],
55
- "confirmed": false,
71
+ "env_shown": false,
72
+ "user_question_asked": false,
73
+ "user_ready": false,
56
74
  "description": "API key and environment verification"
57
75
  },
58
76
  "tdd_red": {
59
77
  "status": "not_started",
60
78
  "test_file": null,
61
79
  "test_count": 0,
62
- "test_matrix_approved": false,
80
+ "test_scenarios": [],
81
+ "matrix_shown": false,
82
+ "user_question_asked": false,
83
+ "user_approved": false,
63
84
  "description": "Write failing tests first"
64
85
  },
65
86
  "tdd_green": {
@@ -72,8 +93,13 @@
72
93
  "status": "not_started",
73
94
  "gaps_found": 0,
74
95
  "gaps_fixed": 0,
96
+ "gaps_skipped": 0,
75
97
  "intentional_omissions": [],
76
98
  "re_research_done": false,
99
+ "gap_analysis_shown": false,
100
+ "user_question_asked": false,
101
+ "user_decided": false,
102
+ "user_decision": null,
77
103
  "description": "Re-research after Green to verify implementation matches docs"
78
104
  },
79
105
  "tdd_refactor": {
@@ -86,9 +112,23 @@
86
112
  "manifest_updated": false,
87
113
  "openapi_updated": false,
88
114
  "research_cached": false,
115
+ "checklist_shown": false,
116
+ "user_question_asked": false,
117
+ "user_confirmed": false,
89
118
  "description": "Update manifests, OpenAPI, cache research"
90
119
  }
91
120
  },
121
+ "manifest_generation": {
122
+ "last_run": null,
123
+ "manifest_generated": false,
124
+ "parameters_extracted": false,
125
+ "test_results_collected": false,
126
+ "output_files": {
127
+ "manifest": "src/app/api-test/api-tests-manifest.json",
128
+ "parameters": "src/app/api-test/parameter-matrix.json",
129
+ "results": "src/app/api-test/test-results.json"
130
+ }
131
+ },
92
132
  "verification": {
93
133
  "all_sources_fetched": false,
94
134
  "schema_matches_docs": false,
@@ -0,0 +1,315 @@
1
+ /**
2
+ * API Test UI Page
3
+ *
4
+ * Displays test structure parsed from Vitest test files.
5
+ * Tests are the SOURCE OF TRUTH - this UI only displays them.
6
+ *
7
+ * @generated by @hustle-together/api-dev-tools v3.0
8
+ */
9
+
10
+ 'use client';
11
+
12
+ import { useEffect, useState } from 'react';
13
+
14
+ // ============================================
15
+ // Types (mirror the API types)
16
+ // ============================================
17
+
18
+ interface TestCase {
19
+ name: string;
20
+ line: number;
21
+ status: 'pending' | 'passed' | 'failed' | 'skipped';
22
+ duration?: number;
23
+ error?: string;
24
+ }
25
+
26
+ interface TestGroup {
27
+ name: string;
28
+ line: number;
29
+ tests: TestCase[];
30
+ groups: TestGroup[];
31
+ }
32
+
33
+ interface TestFeature {
34
+ file: string;
35
+ relativePath: string;
36
+ groups: TestGroup[];
37
+ totalTests: number;
38
+ passedTests: number;
39
+ failedTests: number;
40
+ skippedTests: number;
41
+ }
42
+
43
+ interface TestStructure {
44
+ features: TestFeature[];
45
+ totalTests: number;
46
+ passedTests: number;
47
+ failedTests: number;
48
+ skippedTests: number;
49
+ parsedAt: string;
50
+ }
51
+
52
+ // ============================================
53
+ // Components
54
+ // ============================================
55
+
56
+ function StatusBadge({ status }: { status: TestCase['status'] }) {
57
+ const colors = {
58
+ pending: 'bg-gray-500',
59
+ passed: 'bg-green-500',
60
+ failed: 'bg-red-500',
61
+ skipped: 'bg-yellow-500'
62
+ };
63
+
64
+ const icons = {
65
+ pending: '○',
66
+ passed: '✓',
67
+ failed: '✗',
68
+ skipped: '⊘'
69
+ };
70
+
71
+ return (
72
+ <span className={`${colors[status]} text-white text-xs px-2 py-0.5 rounded font-mono`}>
73
+ {icons[status]} {status}
74
+ </span>
75
+ );
76
+ }
77
+
78
+ function TestCaseItem({ test, filePath }: { test: TestCase; filePath: string }) {
79
+ return (
80
+ <div className="flex items-center gap-3 py-2 px-3 hover:bg-gray-800/50 rounded group">
81
+ <StatusBadge status={test.status} />
82
+ <span className="flex-1 text-gray-300">{test.name}</span>
83
+ <span className="text-gray-600 text-xs font-mono opacity-0 group-hover:opacity-100 transition-opacity">
84
+ {filePath}:{test.line}
85
+ </span>
86
+ {test.duration && (
87
+ <span className="text-gray-500 text-xs">{test.duration}ms</span>
88
+ )}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ function TestGroupItem({
94
+ group,
95
+ filePath,
96
+ depth = 0
97
+ }: {
98
+ group: TestGroup;
99
+ filePath: string;
100
+ depth?: number;
101
+ }) {
102
+ const [expanded, setExpanded] = useState(true);
103
+ const totalTests = group.tests.length + group.groups.reduce((sum, g) => sum + g.tests.length, 0);
104
+
105
+ return (
106
+ <div className={`${depth > 0 ? 'ml-4 border-l border-gray-700 pl-3' : ''}`}>
107
+ <button
108
+ onClick={() => setExpanded(!expanded)}
109
+ className="flex items-center gap-2 w-full text-left py-2 px-2 hover:bg-gray-800/30 rounded"
110
+ >
111
+ <span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
112
+ <span className="font-medium text-white">{group.name}</span>
113
+ <span className="text-gray-500 text-sm">({totalTests} tests)</span>
114
+ </button>
115
+
116
+ {expanded && (
117
+ <div className="mt-1">
118
+ {group.tests.map((test, i) => (
119
+ <TestCaseItem key={i} test={test} filePath={filePath} />
120
+ ))}
121
+ {group.groups.map((subgroup, i) => (
122
+ <TestGroupItem
123
+ key={i}
124
+ group={subgroup}
125
+ filePath={filePath}
126
+ depth={depth + 1}
127
+ />
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+ );
133
+ }
134
+
135
+ function TestFeatureCard({ feature }: { feature: TestFeature }) {
136
+ const [expanded, setExpanded] = useState(true);
137
+
138
+ const passRate = feature.totalTests > 0
139
+ ? Math.round((feature.passedTests / feature.totalTests) * 100)
140
+ : 0;
141
+
142
+ return (
143
+ <div className="bg-gray-900 border border-gray-800 rounded-lg overflow-hidden">
144
+ <button
145
+ onClick={() => setExpanded(!expanded)}
146
+ className="w-full flex items-center justify-between p-4 hover:bg-gray-800/50"
147
+ >
148
+ <div className="flex items-center gap-3">
149
+ <span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
150
+ <div>
151
+ <h3 className="font-mono text-white">{feature.file}</h3>
152
+ <p className="text-gray-500 text-sm">{feature.relativePath}</p>
153
+ </div>
154
+ </div>
155
+ <div className="flex items-center gap-4">
156
+ <div className="flex items-center gap-2 text-sm">
157
+ <span className="text-green-400">{feature.passedTests} passed</span>
158
+ {feature.failedTests > 0 && (
159
+ <span className="text-red-400">{feature.failedTests} failed</span>
160
+ )}
161
+ {feature.skippedTests > 0 && (
162
+ <span className="text-yellow-400">{feature.skippedTests} skipped</span>
163
+ )}
164
+ </div>
165
+ <div className="w-20 h-2 bg-gray-700 rounded-full overflow-hidden">
166
+ <div
167
+ className="h-full bg-green-500 transition-all"
168
+ style={{ width: `${passRate}%` }}
169
+ />
170
+ </div>
171
+ </div>
172
+ </button>
173
+
174
+ {expanded && (
175
+ <div className="border-t border-gray-800 p-4">
176
+ {feature.groups.map((group, i) => (
177
+ <TestGroupItem
178
+ key={i}
179
+ group={group}
180
+ filePath={feature.relativePath}
181
+ />
182
+ ))}
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function SummaryStats({ structure }: { structure: TestStructure }) {
190
+ return (
191
+ <div className="grid grid-cols-4 gap-4 mb-6">
192
+ <div className="bg-gray-900 border border-gray-800 rounded-lg p-4">
193
+ <div className="text-3xl font-bold text-white">{structure.totalTests}</div>
194
+ <div className="text-gray-500 text-sm">Total Tests</div>
195
+ </div>
196
+ <div className="bg-gray-900 border border-green-900 rounded-lg p-4">
197
+ <div className="text-3xl font-bold text-green-400">{structure.passedTests}</div>
198
+ <div className="text-gray-500 text-sm">Passed</div>
199
+ </div>
200
+ <div className="bg-gray-900 border border-red-900 rounded-lg p-4">
201
+ <div className="text-3xl font-bold text-red-400">{structure.failedTests}</div>
202
+ <div className="text-gray-500 text-sm">Failed</div>
203
+ </div>
204
+ <div className="bg-gray-900 border border-yellow-900 rounded-lg p-4">
205
+ <div className="text-3xl font-bold text-yellow-400">{structure.skippedTests}</div>
206
+ <div className="text-gray-500 text-sm">Skipped</div>
207
+ </div>
208
+ </div>
209
+ );
210
+ }
211
+
212
+ // ============================================
213
+ // Main Page
214
+ // ============================================
215
+
216
+ export default function ApiTestPage() {
217
+ const [structure, setStructure] = useState<TestStructure | null>(null);
218
+ const [loading, setLoading] = useState(true);
219
+ const [error, setError] = useState<string | null>(null);
220
+
221
+ const fetchTestStructure = async () => {
222
+ setLoading(true);
223
+ setError(null);
224
+ try {
225
+ const response = await fetch('/api/test-structure');
226
+ if (!response.ok) {
227
+ throw new Error(`Failed to fetch: ${response.status}`);
228
+ }
229
+ const data = await response.json();
230
+ setStructure(data);
231
+ } catch (err) {
232
+ setError(String(err));
233
+ } finally {
234
+ setLoading(false);
235
+ }
236
+ };
237
+
238
+ useEffect(() => {
239
+ fetchTestStructure();
240
+ }, []);
241
+
242
+ return (
243
+ <div className="min-h-screen bg-black text-white p-8">
244
+ <div className="max-w-6xl mx-auto">
245
+ {/* Header */}
246
+ <div className="flex items-center justify-between mb-8">
247
+ <div>
248
+ <h1 className="text-3xl font-bold">API Test Suite</h1>
249
+ <p className="text-gray-500 mt-1">
250
+ Parsed from Vitest test files (source of truth)
251
+ </p>
252
+ </div>
253
+ <div className="flex items-center gap-3">
254
+ <a
255
+ href="http://localhost:51204/__vitest__/"
256
+ target="_blank"
257
+ rel="noopener noreferrer"
258
+ className="px-4 py-2 bg-purple-600 hover:bg-purple-700 rounded-lg text-sm font-medium transition-colors"
259
+ >
260
+ Open Vitest UI
261
+ </a>
262
+ <button
263
+ onClick={fetchTestStructure}
264
+ disabled={loading}
265
+ className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg text-sm font-medium transition-colors disabled:opacity-50"
266
+ >
267
+ {loading ? 'Loading...' : 'Refresh'}
268
+ </button>
269
+ </div>
270
+ </div>
271
+
272
+ {/* Error State */}
273
+ {error && (
274
+ <div className="bg-red-900/50 border border-red-700 rounded-lg p-4 mb-6">
275
+ <p className="text-red-400">{error}</p>
276
+ </div>
277
+ )}
278
+
279
+ {/* Loading State */}
280
+ {loading && !structure && (
281
+ <div className="flex items-center justify-center py-20">
282
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
283
+ </div>
284
+ )}
285
+
286
+ {/* Content */}
287
+ {structure && (
288
+ <>
289
+ <SummaryStats structure={structure} />
290
+
291
+ <div className="space-y-4">
292
+ {structure.features.length === 0 ? (
293
+ <div className="text-center py-20 text-gray-500">
294
+ <p className="text-lg">No test files found</p>
295
+ <p className="mt-2 text-sm">
296
+ Create test files matching *.test.ts or *.spec.ts patterns
297
+ </p>
298
+ </div>
299
+ ) : (
300
+ structure.features.map((feature, i) => (
301
+ <TestFeatureCard key={i} feature={feature} />
302
+ ))
303
+ )}
304
+ </div>
305
+
306
+ <div className="mt-6 text-center text-gray-600 text-sm">
307
+ Parsed at {new Date(structure.parsedAt).toLocaleString()} •{' '}
308
+ {structure.features.length} test files
309
+ </div>
310
+ </>
311
+ )}
312
+ </div>
313
+ </div>
314
+ );
315
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Test Structure Parser API
3
+ *
4
+ * Parses Vitest test files and returns the test structure for UI display.
5
+ * Vitest tests are the SOURCE OF TRUTH - this API only reads and parses them.
6
+ *
7
+ * @generated by @hustle-together/api-dev-tools v3.0
8
+ */
9
+
10
+ import { NextRequest, NextResponse } from 'next/server';
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+
14
+ // ============================================
15
+ // Types
16
+ // ============================================
17
+
18
+ interface TestCase {
19
+ name: string;
20
+ line: number;
21
+ status: 'pending' | 'passed' | 'failed' | 'skipped';
22
+ duration?: number;
23
+ error?: string;
24
+ }
25
+
26
+ interface TestGroup {
27
+ name: string;
28
+ line: number;
29
+ tests: TestCase[];
30
+ groups: TestGroup[];
31
+ }
32
+
33
+ interface TestFeature {
34
+ file: string;
35
+ relativePath: string;
36
+ groups: TestGroup[];
37
+ totalTests: number;
38
+ passedTests: number;
39
+ failedTests: number;
40
+ skippedTests: number;
41
+ }
42
+
43
+ interface TestStructure {
44
+ features: TestFeature[];
45
+ totalTests: number;
46
+ passedTests: number;
47
+ failedTests: number;
48
+ skippedTests: number;
49
+ parsedAt: string;
50
+ }
51
+
52
+ // ============================================
53
+ // Parser Logic (brace counting + regex)
54
+ // ============================================
55
+
56
+ /**
57
+ * Parse a single test file and extract describe/it blocks
58
+ * Uses brace counting to handle nested structures
59
+ */
60
+ function parseTestFile(content: string, filePath: string): TestFeature {
61
+ const lines = content.split('\n');
62
+ const rootGroups: TestGroup[] = [];
63
+ const groupStack: TestGroup[] = [];
64
+
65
+ let braceCount = 0;
66
+ let inDescribe = false;
67
+ let currentDescribeBraceStart = 0;
68
+
69
+ for (let i = 0; i < lines.length; i++) {
70
+ const line = lines[i];
71
+ const lineNum = i + 1;
72
+
73
+ // Match describe blocks: describe('name', () => { or describe("name", function() {
74
+ const describeMatch = line.match(/describe\s*\(\s*['"`]([^'"`]+)['"`]/);
75
+ if (describeMatch) {
76
+ const group: TestGroup = {
77
+ name: describeMatch[1],
78
+ line: lineNum,
79
+ tests: [],
80
+ groups: []
81
+ };
82
+
83
+ if (groupStack.length > 0) {
84
+ groupStack[groupStack.length - 1].groups.push(group);
85
+ } else {
86
+ rootGroups.push(group);
87
+ }
88
+
89
+ groupStack.push(group);
90
+ inDescribe = true;
91
+ currentDescribeBraceStart = braceCount;
92
+ }
93
+
94
+ // Match it/test blocks: it('name', ...) or test('name', ...)
95
+ const testMatch = line.match(/(?:it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
96
+ if (testMatch && groupStack.length > 0) {
97
+ const testCase: TestCase = {
98
+ name: testMatch[1],
99
+ line: lineNum,
100
+ status: 'pending' // Will be updated by test runner results
101
+ };
102
+
103
+ // Check for .skip or .todo
104
+ if (line.includes('.skip') || line.includes('.todo')) {
105
+ testCase.status = 'skipped';
106
+ }
107
+
108
+ groupStack[groupStack.length - 1].tests.push(testCase);
109
+ }
110
+
111
+ // Count braces to track scope
112
+ const openBraces = (line.match(/{/g) || []).length;
113
+ const closeBraces = (line.match(/}/g) || []).length;
114
+ braceCount += openBraces - closeBraces;
115
+
116
+ // Pop group from stack when we close its scope
117
+ if (inDescribe && braceCount <= currentDescribeBraceStart && groupStack.length > 0) {
118
+ groupStack.pop();
119
+ if (groupStack.length === 0) {
120
+ inDescribe = false;
121
+ } else {
122
+ // Update brace start for parent describe
123
+ currentDescribeBraceStart = braceCount;
124
+ }
125
+ }
126
+ }
127
+
128
+ // Count tests
129
+ const countTests = (groups: TestGroup[]): { total: number; passed: number; failed: number; skipped: number } => {
130
+ let total = 0, passed = 0, failed = 0, skipped = 0;
131
+
132
+ for (const group of groups) {
133
+ for (const test of group.tests) {
134
+ total++;
135
+ if (test.status === 'passed') passed++;
136
+ else if (test.status === 'failed') failed++;
137
+ else if (test.status === 'skipped') skipped++;
138
+ }
139
+ const nested = countTests(group.groups);
140
+ total += nested.total;
141
+ passed += nested.passed;
142
+ failed += nested.failed;
143
+ skipped += nested.skipped;
144
+ }
145
+
146
+ return { total, passed, failed, skipped };
147
+ };
148
+
149
+ const counts = countTests(rootGroups);
150
+
151
+ return {
152
+ file: path.basename(filePath),
153
+ relativePath: filePath,
154
+ groups: rootGroups,
155
+ totalTests: counts.total,
156
+ passedTests: counts.passed,
157
+ failedTests: counts.failed,
158
+ skippedTests: counts.skipped
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Find all test files in the project
164
+ */
165
+ function findTestFiles(baseDir: string): string[] {
166
+ const testFiles: string[] = [];
167
+ const testPatterns = [
168
+ '**/*.test.ts',
169
+ '**/*.test.tsx',
170
+ '**/*.spec.ts',
171
+ '**/*.spec.tsx',
172
+ '**/__tests__/**/*.ts',
173
+ '**/__tests__/**/*.tsx'
174
+ ];
175
+
176
+ function walkDir(dir: string) {
177
+ try {
178
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
179
+
180
+ for (const entry of entries) {
181
+ const fullPath = path.join(dir, entry.name);
182
+
183
+ // Skip node_modules and hidden directories
184
+ if (entry.isDirectory()) {
185
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
186
+ continue;
187
+ }
188
+ walkDir(fullPath);
189
+ } else if (entry.isFile()) {
190
+ // Check if file matches test patterns
191
+ if (
192
+ entry.name.endsWith('.test.ts') ||
193
+ entry.name.endsWith('.test.tsx') ||
194
+ entry.name.endsWith('.spec.ts') ||
195
+ entry.name.endsWith('.spec.tsx')
196
+ ) {
197
+ testFiles.push(fullPath);
198
+ }
199
+ }
200
+ }
201
+ } catch (error) {
202
+ // Ignore permission errors
203
+ }
204
+ }
205
+
206
+ walkDir(baseDir);
207
+ return testFiles;
208
+ }
209
+
210
+ // ============================================
211
+ // API Handler
212
+ // ============================================
213
+
214
+ export async function GET(request: NextRequest) {
215
+ try {
216
+ const { searchParams } = new URL(request.url);
217
+ const testPath = searchParams.get('path');
218
+ const baseDir = process.cwd();
219
+
220
+ let testFiles: string[];
221
+
222
+ if (testPath) {
223
+ // Parse specific test file
224
+ const fullPath = path.join(baseDir, testPath);
225
+ if (!fs.existsSync(fullPath)) {
226
+ return NextResponse.json(
227
+ { error: `Test file not found: ${testPath}` },
228
+ { status: 404 }
229
+ );
230
+ }
231
+ testFiles = [fullPath];
232
+ } else {
233
+ // Find all test files
234
+ testFiles = findTestFiles(baseDir);
235
+ }
236
+
237
+ const features: TestFeature[] = [];
238
+
239
+ for (const file of testFiles) {
240
+ try {
241
+ const content = fs.readFileSync(file, 'utf-8');
242
+ const relativePath = path.relative(baseDir, file);
243
+ const feature = parseTestFile(content, relativePath);
244
+ features.push(feature);
245
+ } catch (error) {
246
+ // Skip files that can't be read
247
+ console.error(`Failed to parse ${file}:`, error);
248
+ }
249
+ }
250
+
251
+ // Aggregate counts
252
+ const structure: TestStructure = {
253
+ features,
254
+ totalTests: features.reduce((sum, f) => sum + f.totalTests, 0),
255
+ passedTests: features.reduce((sum, f) => sum + f.passedTests, 0),
256
+ failedTests: features.reduce((sum, f) => sum + f.failedTests, 0),
257
+ skippedTests: features.reduce((sum, f) => sum + f.skippedTests, 0),
258
+ parsedAt: new Date().toISOString()
259
+ };
260
+
261
+ return NextResponse.json(structure);
262
+ } catch (error) {
263
+ console.error('Test structure parser error:', error);
264
+ return NextResponse.json(
265
+ { error: 'Failed to parse test structure', details: String(error) },
266
+ { status: 500 }
267
+ );
268
+ }
269
+ }