@hustle-together/api-dev-tools 2.0.7 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +343 -467
- package/bin/cli.js +229 -15
- package/commands/README.md +124 -251
- package/commands/api-create.md +318 -136
- package/commands/api-interview.md +252 -256
- package/commands/api-research.md +209 -234
- package/commands/api-verify.md +231 -0
- package/demo/audio/generate-all-narrations.js +581 -0
- package/demo/audio/generate-narration.js +120 -56
- package/demo/audio/generate-voice-previews.js +140 -0
- package/demo/audio/narration-adam-timing.json +4675 -0
- package/demo/audio/narration-adam.mp3 +0 -0
- package/demo/audio/narration-creature-timing.json +4675 -0
- package/demo/audio/narration-creature.mp3 +0 -0
- package/demo/audio/narration-gaming-timing.json +4675 -0
- package/demo/audio/narration-gaming.mp3 +0 -0
- package/demo/audio/narration-hope-timing.json +4675 -0
- package/demo/audio/narration-hope.mp3 +0 -0
- package/demo/audio/narration-mark-timing.json +4675 -0
- package/demo/audio/narration-mark.mp3 +0 -0
- package/demo/audio/previews/manifest.json +30 -0
- package/demo/audio/previews/preview-creature.mp3 +0 -0
- package/demo/audio/previews/preview-gaming.mp3 +0 -0
- package/demo/audio/previews/preview-hope.mp3 +0 -0
- package/demo/audio/previews/preview-mark.mp3 +0 -0
- package/demo/audio/voices-manifest.json +50 -0
- package/demo/hustle-together/blog/gemini-vs-claude-widgets.html +30 -28
- package/demo/hustle-together/blog/interview-driven-api-development.html +37 -23
- package/demo/hustle-together/index.html +142 -109
- package/demo/workflow-demo.html +2618 -1036
- package/hooks/api-workflow-check.py +2 -0
- package/hooks/enforce-deep-research.py +180 -0
- package/hooks/enforce-disambiguation.py +149 -0
- package/hooks/enforce-documentation.py +187 -0
- package/hooks/enforce-environment.py +249 -0
- package/hooks/enforce-refactor.py +187 -0
- package/hooks/enforce-research.py +93 -46
- package/hooks/enforce-schema.py +186 -0
- package/hooks/enforce-scope.py +156 -0
- package/hooks/enforce-tdd-red.py +246 -0
- package/hooks/enforce-verify.py +186 -0
- package/hooks/periodic-reground.py +154 -0
- package/hooks/session-startup.py +151 -0
- package/hooks/track-tool-use.py +109 -17
- package/hooks/verify-after-green.py +282 -0
- package/package.json +3 -2
- package/scripts/collect-test-results.ts +404 -0
- package/scripts/extract-parameters.ts +483 -0
- package/scripts/generate-test-manifest.ts +520 -0
- package/templates/CLAUDE-SECTION.md +84 -0
- package/templates/api-dev-state.json +83 -8
- package/templates/api-test/page.tsx +315 -0
- package/templates/api-test/test-structure/route.ts +269 -0
- package/templates/research-index.json +6 -0
- package/templates/settings.json +59 -0
|
@@ -1,52 +1,107 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "
|
|
2
|
+
"version": "3.0.0",
|
|
3
3
|
"created_at": null,
|
|
4
4
|
"endpoint": null,
|
|
5
5
|
"library": null,
|
|
6
|
+
"session_id": null,
|
|
7
|
+
"turn_count": 0,
|
|
8
|
+
"last_turn_timestamp": null,
|
|
6
9
|
"research_queries": [],
|
|
7
10
|
"prompt_detections": [],
|
|
8
11
|
"phases": {
|
|
12
|
+
"disambiguation": {
|
|
13
|
+
"status": "not_started",
|
|
14
|
+
"clarified": null,
|
|
15
|
+
"search_variations": [],
|
|
16
|
+
"user_question_asked": false,
|
|
17
|
+
"user_selected": null,
|
|
18
|
+
"description": "Pre-research disambiguation to clarify ambiguous requests"
|
|
19
|
+
},
|
|
9
20
|
"scope": {
|
|
10
21
|
"status": "not_started",
|
|
11
|
-
"
|
|
22
|
+
"confirmed": false,
|
|
23
|
+
"user_question_asked": false,
|
|
24
|
+
"user_confirmed": false,
|
|
25
|
+
"description": "Initial scope understanding and confirmation"
|
|
12
26
|
},
|
|
13
27
|
"research_initial": {
|
|
14
28
|
"status": "not_started",
|
|
15
29
|
"sources": [],
|
|
30
|
+
"summary_shown": false,
|
|
31
|
+
"user_question_asked": false,
|
|
32
|
+
"user_approved": false,
|
|
16
33
|
"description": "Context7/WebSearch research for live documentation"
|
|
17
34
|
},
|
|
18
35
|
"interview": {
|
|
19
36
|
"status": "not_started",
|
|
20
37
|
"questions": [],
|
|
21
|
-
"
|
|
38
|
+
"user_question_count": 0,
|
|
39
|
+
"structured_question_count": 0,
|
|
40
|
+
"decisions": {},
|
|
41
|
+
"user_question_asked": false,
|
|
42
|
+
"user_completed": false,
|
|
43
|
+
"description": "Structured interview about requirements (generated FROM research)"
|
|
22
44
|
},
|
|
23
45
|
"research_deep": {
|
|
24
46
|
"status": "not_started",
|
|
25
47
|
"sources": [],
|
|
26
|
-
"
|
|
48
|
+
"proposed_searches": [],
|
|
49
|
+
"approved_searches": [],
|
|
50
|
+
"executed_searches": [],
|
|
51
|
+
"skipped_searches": [],
|
|
52
|
+
"proposals_shown": false,
|
|
53
|
+
"user_question_asked": false,
|
|
54
|
+
"user_approved": false,
|
|
55
|
+
"description": "Deep dive based on interview answers (adaptive, not shotgun)"
|
|
27
56
|
},
|
|
28
57
|
"schema_creation": {
|
|
29
58
|
"status": "not_started",
|
|
30
59
|
"schema_file": null,
|
|
60
|
+
"fields_count": 0,
|
|
61
|
+
"schema_shown": false,
|
|
62
|
+
"user_question_asked": false,
|
|
63
|
+
"user_confirmed": false,
|
|
31
64
|
"description": "Zod schema creation from research"
|
|
32
65
|
},
|
|
33
66
|
"environment_check": {
|
|
34
67
|
"status": "not_started",
|
|
35
|
-
"
|
|
68
|
+
"keys_required": [],
|
|
69
|
+
"keys_found": [],
|
|
36
70
|
"keys_missing": [],
|
|
71
|
+
"env_shown": false,
|
|
72
|
+
"user_question_asked": false,
|
|
73
|
+
"user_ready": false,
|
|
37
74
|
"description": "API key and environment verification"
|
|
38
75
|
},
|
|
39
76
|
"tdd_red": {
|
|
40
77
|
"status": "not_started",
|
|
41
78
|
"test_file": null,
|
|
42
79
|
"test_count": 0,
|
|
80
|
+
"test_scenarios": [],
|
|
81
|
+
"matrix_shown": false,
|
|
82
|
+
"user_question_asked": false,
|
|
83
|
+
"user_approved": false,
|
|
43
84
|
"description": "Write failing tests first"
|
|
44
85
|
},
|
|
45
86
|
"tdd_green": {
|
|
46
87
|
"status": "not_started",
|
|
47
88
|
"implementation_file": null,
|
|
89
|
+
"all_tests_passing": false,
|
|
48
90
|
"description": "Minimal implementation to pass tests"
|
|
49
91
|
},
|
|
92
|
+
"verify": {
|
|
93
|
+
"status": "not_started",
|
|
94
|
+
"gaps_found": 0,
|
|
95
|
+
"gaps_fixed": 0,
|
|
96
|
+
"gaps_skipped": 0,
|
|
97
|
+
"intentional_omissions": [],
|
|
98
|
+
"re_research_done": false,
|
|
99
|
+
"gap_analysis_shown": false,
|
|
100
|
+
"user_question_asked": false,
|
|
101
|
+
"user_decided": false,
|
|
102
|
+
"user_decision": null,
|
|
103
|
+
"description": "Re-research after Green to verify implementation matches docs"
|
|
104
|
+
},
|
|
50
105
|
"tdd_refactor": {
|
|
51
106
|
"status": "not_started",
|
|
52
107
|
"description": "Code cleanup while keeping tests green"
|
|
@@ -54,7 +109,24 @@
|
|
|
54
109
|
"documentation": {
|
|
55
110
|
"status": "not_started",
|
|
56
111
|
"files_updated": [],
|
|
57
|
-
"
|
|
112
|
+
"manifest_updated": false,
|
|
113
|
+
"openapi_updated": false,
|
|
114
|
+
"research_cached": false,
|
|
115
|
+
"checklist_shown": false,
|
|
116
|
+
"user_question_asked": false,
|
|
117
|
+
"user_confirmed": false,
|
|
118
|
+
"description": "Update manifests, OpenAPI, cache research"
|
|
119
|
+
}
|
|
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"
|
|
58
130
|
}
|
|
59
131
|
},
|
|
60
132
|
"verification": {
|
|
@@ -62,6 +134,9 @@
|
|
|
62
134
|
"schema_matches_docs": false,
|
|
63
135
|
"tests_cover_params": false,
|
|
64
136
|
"all_tests_passing": false,
|
|
65
|
-
"coverage_percent": null
|
|
66
|
-
|
|
137
|
+
"coverage_percent": null,
|
|
138
|
+
"post_green_verification": false
|
|
139
|
+
},
|
|
140
|
+
"research_index": {},
|
|
141
|
+
"reground_history": []
|
|
67
142
|
}
|
|
@@ -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
|
+
}
|