@hustle-together/api-dev-tools 3.0.0 → 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 +71 -0
- package/bin/cli.js +184 -14
- package/demo/audio/generate-all-narrations.js +124 -59
- package/demo/audio/generate-narration.js +120 -56
- package/demo/audio/narration-adam-timing.json +3086 -2077
- package/demo/audio/narration-adam.mp3 +0 -0
- package/demo/audio/narration-creature-timing.json +3094 -2085
- package/demo/audio/narration-creature.mp3 +0 -0
- package/demo/audio/narration-gaming-timing.json +3091 -2082
- package/demo/audio/narration-gaming.mp3 +0 -0
- package/demo/audio/narration-hope-timing.json +3072 -2063
- package/demo/audio/narration-hope.mp3 +0 -0
- package/demo/audio/narration-mark-timing.json +3090 -2081
- package/demo/audio/narration-mark.mp3 +0 -0
- package/demo/audio/voices-manifest.json +16 -16
- package/demo/workflow-demo.html +1528 -411
- 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/verify-after-green.py +136 -6
- package/package.json +2 -1
- 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 +45 -5
- package/templates/api-test/page.tsx +315 -0
- package/templates/api-test/test-structure/route.ts +269 -0
- package/templates/settings.json +36 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Collect Test Results Script
|
|
4
|
+
*
|
|
5
|
+
* Runs Vitest and collects results programmatically.
|
|
6
|
+
* Updates the manifest with actual pass/fail status.
|
|
7
|
+
*
|
|
8
|
+
* IMPORTANT: This is 100% programmatic - NO LLM involvement.
|
|
9
|
+
* Tests are executed and results are collected automatically.
|
|
10
|
+
*
|
|
11
|
+
* @generated by @hustle-together/api-dev-tools v3.0
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync, spawn } from 'child_process';
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
|
|
18
|
+
// ============================================
|
|
19
|
+
// Types
|
|
20
|
+
// ============================================
|
|
21
|
+
|
|
22
|
+
interface TestResult {
|
|
23
|
+
name: string;
|
|
24
|
+
file: string;
|
|
25
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
26
|
+
duration: number;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TestSuiteResult {
|
|
31
|
+
file: string;
|
|
32
|
+
tests: TestResult[];
|
|
33
|
+
passed: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
skipped: number;
|
|
36
|
+
duration: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CollectedResults {
|
|
40
|
+
version: string;
|
|
41
|
+
collectedAt: string;
|
|
42
|
+
suites: TestSuiteResult[];
|
|
43
|
+
summary: {
|
|
44
|
+
totalSuites: number;
|
|
45
|
+
totalTests: number;
|
|
46
|
+
passed: number;
|
|
47
|
+
failed: number;
|
|
48
|
+
skipped: number;
|
|
49
|
+
duration: number;
|
|
50
|
+
success: boolean;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============================================
|
|
55
|
+
// Vitest Output Parser
|
|
56
|
+
// ============================================
|
|
57
|
+
|
|
58
|
+
function parseVitestJson(jsonOutput: string): CollectedResults {
|
|
59
|
+
try {
|
|
60
|
+
const data = JSON.parse(jsonOutput);
|
|
61
|
+
|
|
62
|
+
const suites: TestSuiteResult[] = [];
|
|
63
|
+
let totalPassed = 0;
|
|
64
|
+
let totalFailed = 0;
|
|
65
|
+
let totalSkipped = 0;
|
|
66
|
+
let totalDuration = 0;
|
|
67
|
+
|
|
68
|
+
// Parse Vitest JSON reporter output
|
|
69
|
+
if (data.testResults) {
|
|
70
|
+
for (const fileResult of data.testResults) {
|
|
71
|
+
const suite: TestSuiteResult = {
|
|
72
|
+
file: fileResult.name || fileResult.filepath,
|
|
73
|
+
tests: [],
|
|
74
|
+
passed: 0,
|
|
75
|
+
failed: 0,
|
|
76
|
+
skipped: 0,
|
|
77
|
+
duration: fileResult.duration || 0
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (fileResult.assertionResults) {
|
|
81
|
+
for (const test of fileResult.assertionResults) {
|
|
82
|
+
const result: TestResult = {
|
|
83
|
+
name: test.title || test.fullName,
|
|
84
|
+
file: suite.file,
|
|
85
|
+
status: test.status === 'passed' ? 'passed' :
|
|
86
|
+
test.status === 'failed' ? 'failed' : 'skipped',
|
|
87
|
+
duration: test.duration || 0
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (test.failureMessages && test.failureMessages.length > 0) {
|
|
91
|
+
result.error = test.failureMessages.join('\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
suite.tests.push(result);
|
|
95
|
+
|
|
96
|
+
if (result.status === 'passed') suite.passed++;
|
|
97
|
+
else if (result.status === 'failed') suite.failed++;
|
|
98
|
+
else suite.skipped++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
totalPassed += suite.passed;
|
|
103
|
+
totalFailed += suite.failed;
|
|
104
|
+
totalSkipped += suite.skipped;
|
|
105
|
+
totalDuration += suite.duration;
|
|
106
|
+
|
|
107
|
+
suites.push(suite);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
version: '3.0.0',
|
|
113
|
+
collectedAt: new Date().toISOString(),
|
|
114
|
+
suites,
|
|
115
|
+
summary: {
|
|
116
|
+
totalSuites: suites.length,
|
|
117
|
+
totalTests: totalPassed + totalFailed + totalSkipped,
|
|
118
|
+
passed: totalPassed,
|
|
119
|
+
failed: totalFailed,
|
|
120
|
+
skipped: totalSkipped,
|
|
121
|
+
duration: totalDuration,
|
|
122
|
+
success: totalFailed === 0
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
throw new Error(`Failed to parse Vitest JSON output: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================
|
|
131
|
+
// Console Output Parser (Fallback)
|
|
132
|
+
// ============================================
|
|
133
|
+
|
|
134
|
+
function parseVitestConsole(output: string): CollectedResults {
|
|
135
|
+
const suites: TestSuiteResult[] = [];
|
|
136
|
+
let currentSuite: TestSuiteResult | null = null;
|
|
137
|
+
|
|
138
|
+
const lines = output.split('\n');
|
|
139
|
+
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
// Match file header: ✓ src/path/file.test.ts (5 tests) 123ms
|
|
142
|
+
const fileMatch = line.match(/[✓✗◯]\s+([^\s]+\.(?:test|spec)\.tsx?)\s+\((\d+)\s+tests?\)/);
|
|
143
|
+
if (fileMatch) {
|
|
144
|
+
if (currentSuite) {
|
|
145
|
+
suites.push(currentSuite);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const durationMatch = line.match(/(\d+)ms$/);
|
|
149
|
+
|
|
150
|
+
currentSuite = {
|
|
151
|
+
file: fileMatch[1],
|
|
152
|
+
tests: [],
|
|
153
|
+
passed: 0,
|
|
154
|
+
failed: 0,
|
|
155
|
+
skipped: 0,
|
|
156
|
+
duration: durationMatch ? parseInt(durationMatch[1]) : 0
|
|
157
|
+
};
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Match test result: ✓ should do something (5ms)
|
|
162
|
+
const testMatch = line.match(/^\s*([✓✗◯⊘])\s+(.+?)(?:\s+\((\d+)ms\))?$/);
|
|
163
|
+
if (testMatch && currentSuite) {
|
|
164
|
+
const [, icon, name, duration] = testMatch;
|
|
165
|
+
|
|
166
|
+
const status: 'passed' | 'failed' | 'skipped' =
|
|
167
|
+
icon === '✓' ? 'passed' :
|
|
168
|
+
icon === '✗' ? 'failed' : 'skipped';
|
|
169
|
+
|
|
170
|
+
currentSuite.tests.push({
|
|
171
|
+
name,
|
|
172
|
+
file: currentSuite.file,
|
|
173
|
+
status,
|
|
174
|
+
duration: duration ? parseInt(duration) : 0
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (status === 'passed') currentSuite.passed++;
|
|
178
|
+
else if (status === 'failed') currentSuite.failed++;
|
|
179
|
+
else currentSuite.skipped++;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (currentSuite) {
|
|
184
|
+
suites.push(currentSuite);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Calculate summary
|
|
188
|
+
const summary = suites.reduce((acc, suite) => ({
|
|
189
|
+
totalSuites: acc.totalSuites + 1,
|
|
190
|
+
totalTests: acc.totalTests + suite.tests.length,
|
|
191
|
+
passed: acc.passed + suite.passed,
|
|
192
|
+
failed: acc.failed + suite.failed,
|
|
193
|
+
skipped: acc.skipped + suite.skipped,
|
|
194
|
+
duration: acc.duration + suite.duration,
|
|
195
|
+
success: acc.success && suite.failed === 0
|
|
196
|
+
}), {
|
|
197
|
+
totalSuites: 0,
|
|
198
|
+
totalTests: 0,
|
|
199
|
+
passed: 0,
|
|
200
|
+
failed: 0,
|
|
201
|
+
skipped: 0,
|
|
202
|
+
duration: 0,
|
|
203
|
+
success: true
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
version: '3.0.0',
|
|
208
|
+
collectedAt: new Date().toISOString(),
|
|
209
|
+
suites,
|
|
210
|
+
summary
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================
|
|
215
|
+
// Test Runner
|
|
216
|
+
// ============================================
|
|
217
|
+
|
|
218
|
+
function runVitest(baseDir: string, filter?: string): CollectedResults {
|
|
219
|
+
console.log('🧪 Running Vitest...');
|
|
220
|
+
|
|
221
|
+
const vitestArgs = ['vitest', 'run', '--reporter=json'];
|
|
222
|
+
if (filter) {
|
|
223
|
+
vitestArgs.push(filter);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Try running with JSON reporter
|
|
228
|
+
const result = execSync(`npx ${vitestArgs.join(' ')}`, {
|
|
229
|
+
cwd: baseDir,
|
|
230
|
+
encoding: 'utf-8',
|
|
231
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
232
|
+
maxBuffer: 50 * 1024 * 1024 // 50MB buffer
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return parseVitestJson(result);
|
|
236
|
+
} catch (error: unknown) {
|
|
237
|
+
// Vitest may exit with non-zero on test failures
|
|
238
|
+
// Try to parse the output anyway
|
|
239
|
+
const execError = error as { stdout?: string; stderr?: string };
|
|
240
|
+
if (execError.stdout) {
|
|
241
|
+
try {
|
|
242
|
+
return parseVitestJson(execError.stdout);
|
|
243
|
+
} catch {
|
|
244
|
+
// Fall back to console parsing
|
|
245
|
+
return parseVitestConsole(execError.stdout);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Try fallback: run without JSON reporter
|
|
250
|
+
console.log(' ⚠️ JSON reporter failed, trying console output...');
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const consoleResult = execSync(`npx vitest run ${filter || ''}`, {
|
|
254
|
+
cwd: baseDir,
|
|
255
|
+
encoding: 'utf-8',
|
|
256
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return parseVitestConsole(consoleResult);
|
|
260
|
+
} catch (fallbackError: unknown) {
|
|
261
|
+
const fbError = fallbackError as { stdout?: string };
|
|
262
|
+
if (fbError.stdout) {
|
|
263
|
+
return parseVitestConsole(fbError.stdout);
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ============================================
|
|
271
|
+
// Manifest Updater
|
|
272
|
+
// ============================================
|
|
273
|
+
|
|
274
|
+
function updateManifest(manifestPath: string, results: CollectedResults): void {
|
|
275
|
+
if (!fs.existsSync(manifestPath)) {
|
|
276
|
+
console.log(' ⚠️ Manifest not found, skipping update');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
281
|
+
|
|
282
|
+
// Create a map of test results by file
|
|
283
|
+
const resultsByFile = new Map<string, TestSuiteResult>();
|
|
284
|
+
for (const suite of results.suites) {
|
|
285
|
+
const basename = path.basename(suite.file);
|
|
286
|
+
resultsByFile.set(basename, suite);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Update each endpoint's test status
|
|
290
|
+
if (manifest.endpoints) {
|
|
291
|
+
for (const endpoint of manifest.endpoints) {
|
|
292
|
+
const testBasename = path.basename(endpoint.testFile || '');
|
|
293
|
+
const suiteResult = resultsByFile.get(testBasename);
|
|
294
|
+
|
|
295
|
+
if (suiteResult) {
|
|
296
|
+
endpoint.testResults = {
|
|
297
|
+
passed: suiteResult.passed,
|
|
298
|
+
failed: suiteResult.failed,
|
|
299
|
+
skipped: suiteResult.skipped,
|
|
300
|
+
duration: suiteResult.duration,
|
|
301
|
+
lastRun: results.collectedAt
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Update summary
|
|
308
|
+
manifest.lastTestRun = {
|
|
309
|
+
...results.summary,
|
|
310
|
+
timestamp: results.collectedAt
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
314
|
+
console.log(` ✅ Updated manifest with test results`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ============================================
|
|
318
|
+
// CLI Entry Point
|
|
319
|
+
// ============================================
|
|
320
|
+
|
|
321
|
+
function main() {
|
|
322
|
+
const args = process.argv.slice(2);
|
|
323
|
+
const baseDir = args[0] || process.cwd();
|
|
324
|
+
const filter = args[1] || undefined;
|
|
325
|
+
const outputPath = args[2] || path.join(baseDir, 'src', 'app', 'api-test', 'test-results.json');
|
|
326
|
+
const manifestPath = path.join(baseDir, 'src', 'app', 'api-test', 'api-tests-manifest.json');
|
|
327
|
+
|
|
328
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
329
|
+
console.log(' 🧪 Test Results Collector');
|
|
330
|
+
console.log(' @hustle-together/api-dev-tools v3.0');
|
|
331
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
332
|
+
console.log(`\n📁 Base directory: ${baseDir}`);
|
|
333
|
+
if (filter) {
|
|
334
|
+
console.log(`🔍 Filter: ${filter}`);
|
|
335
|
+
}
|
|
336
|
+
console.log(`📄 Output file: ${outputPath}\n`);
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const results = runVitest(baseDir, filter);
|
|
340
|
+
|
|
341
|
+
// Ensure output directory exists
|
|
342
|
+
const outputDir = path.dirname(outputPath);
|
|
343
|
+
if (!fs.existsSync(outputDir)) {
|
|
344
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Write results
|
|
348
|
+
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
|
|
349
|
+
|
|
350
|
+
// Update manifest with results
|
|
351
|
+
updateManifest(manifestPath, results);
|
|
352
|
+
|
|
353
|
+
console.log('\n═══════════════════════════════════════════════════════════════');
|
|
354
|
+
if (results.summary.success) {
|
|
355
|
+
console.log(' ✅ All tests passed!');
|
|
356
|
+
} else {
|
|
357
|
+
console.log(' ❌ Some tests failed');
|
|
358
|
+
}
|
|
359
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
360
|
+
|
|
361
|
+
console.log(`\n📊 Summary:`);
|
|
362
|
+
console.log(` • Suites: ${results.summary.totalSuites}`);
|
|
363
|
+
console.log(` • Tests: ${results.summary.totalTests}`);
|
|
364
|
+
console.log(` • Passed: ${results.summary.passed} ✓`);
|
|
365
|
+
if (results.summary.failed > 0) {
|
|
366
|
+
console.log(` • Failed: ${results.summary.failed} ✗`);
|
|
367
|
+
}
|
|
368
|
+
if (results.summary.skipped > 0) {
|
|
369
|
+
console.log(` • Skipped: ${results.summary.skipped} ⊘`);
|
|
370
|
+
}
|
|
371
|
+
console.log(` • Duration: ${results.summary.duration}ms`);
|
|
372
|
+
|
|
373
|
+
// List failed tests
|
|
374
|
+
const failedTests = results.suites.flatMap(suite =>
|
|
375
|
+
suite.tests.filter(t => t.status === 'failed').map(t => ({
|
|
376
|
+
file: suite.file,
|
|
377
|
+
name: t.name,
|
|
378
|
+
error: t.error
|
|
379
|
+
}))
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
if (failedTests.length > 0) {
|
|
383
|
+
console.log(`\n❌ Failed tests:`);
|
|
384
|
+
for (const test of failedTests) {
|
|
385
|
+
console.log(` • ${test.file}: ${test.name}`);
|
|
386
|
+
if (test.error) {
|
|
387
|
+
console.log(` ${test.error.split('\n')[0]}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(`\n📄 Results: ${outputPath}`);
|
|
393
|
+
console.log(`📄 Manifest: ${manifestPath}\n`);
|
|
394
|
+
|
|
395
|
+
// Exit with appropriate code
|
|
396
|
+
process.exit(results.summary.success ? 0 : 1);
|
|
397
|
+
|
|
398
|
+
} catch (error) {
|
|
399
|
+
console.error('\n❌ Failed to collect test results:', error);
|
|
400
|
+
process.exit(1);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
main();
|