@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
@@ -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();