@soulbatical/tetra-dev-toolkit 1.20.11 → 1.20.12

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.
@@ -3,16 +3,24 @@
3
3
  /**
4
4
  * Tetra Init Tests — Scaffold golden standard E2E test structure.
5
5
  *
6
- * Creates:
7
- * tests/e2e/helpers/api-client.ts (HTTP client with X-Test-Key bypass)
8
- * tests/e2e/helpers/test-users.ts (Login + token caching)
9
- * tests/e2e/01-auth.test.ts (Auth: login, session, refresh, logout)
10
- * tests/e2e/07-security.test.ts (Auth walls + role access)
11
- * vitest.config.e2e.ts (Vitest config for E2E)
6
+ * Creates the complete test infrastructure:
7
+ * tests/e2e/helpers/api-client.ts HTTP client with X-Test-Key bypass
8
+ * tests/e2e/helpers/test-users.ts Login + token caching
9
+ * tests/e2e/global-setup.ts Login once, share tokens
10
+ * tests/e2e/.gitignore Exclude token files
11
+ * tests/e2e/01-auth.test.ts Auth: login, session, refresh, logout
12
+ * tests/e2e/02-crud-resources.test.ts CRUD for all resources (customize)
13
+ * tests/e2e/03-permissions.test.ts Auth walls for all routes (customize)
14
+ * tests/e2e/04-business-flows.test.ts End-to-end user journeys (customize)
15
+ * tests/e2e/05-security.test.ts Input validation, injection, CORS
16
+ * vitest.config.e2e.ts Vitest config for E2E
17
+ * .github/workflows/ci.yml CI with API tests (if missing)
18
+ * package.json test:e2e script (if missing)
12
19
  *
13
20
  * Usage:
14
- * tetra-init-tests # Interactive
21
+ * tetra-init-tests # Scaffold all
15
22
  * tetra-init-tests --force # Overwrite existing files
23
+ * tetra-init-tests --ci-only # Only add CI workflow
16
24
  */
17
25
 
18
26
  import { program } from 'commander'
@@ -24,9 +32,13 @@ const __filename = fileURLToPath(import.meta.url)
24
32
  const __dirname = dirname(__filename)
25
33
  const TEMPLATES_DIR = join(__dirname, '..', 'lib', 'templates', 'tests')
26
34
 
35
+ let filesCreated = 0
36
+ let filesSkipped = 0
37
+
27
38
  function copyTemplate(templateName, destPath, force = false) {
28
39
  if (existsSync(destPath) && !force) {
29
- console.log(` ⏩ ${destPath} (exists, use --force to overwrite)`)
40
+ console.log(` ⏩ ${destPath} (exists)`)
41
+ filesSkipped++
30
42
  return false
31
43
  }
32
44
 
@@ -40,17 +52,38 @@ function copyTemplate(templateName, destPath, force = false) {
40
52
  mkdirSync(dirname(destPath), { recursive: true })
41
53
  writeFileSync(destPath, content)
42
54
  console.log(` ✅ ${destPath}`)
55
+ filesCreated++
43
56
  return true
44
57
  }
45
58
 
46
59
  function writeFile(destPath, content, force = false) {
47
60
  if (existsSync(destPath) && !force) {
48
61
  console.log(` ⏩ ${destPath} (exists)`)
62
+ filesSkipped++
49
63
  return false
50
64
  }
51
65
  mkdirSync(dirname(destPath), { recursive: true })
52
66
  writeFileSync(destPath, content)
53
67
  console.log(` ✅ ${destPath}`)
68
+ filesCreated++
69
+ return true
70
+ }
71
+
72
+ function addNpmScript(root, scriptName, scriptCmd) {
73
+ const pkgPath = join(root, 'package.json')
74
+ if (!existsSync(pkgPath)) return false
75
+
76
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
77
+ if (pkg.scripts?.[scriptName]) {
78
+ console.log(` ⏩ package.json script "${scriptName}" (exists)`)
79
+ return false
80
+ }
81
+
82
+ pkg.scripts = pkg.scripts || {}
83
+ pkg.scripts[scriptName] = scriptCmd
84
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
85
+ console.log(` ✅ package.json script "${scriptName}" added`)
86
+ filesCreated++
54
87
  return true
55
88
  }
56
89
 
@@ -59,27 +92,39 @@ program
59
92
  .description('Scaffold Tetra golden standard E2E test structure')
60
93
  .option('--force', 'Overwrite existing files')
61
94
  .option('--dir <path>', 'Test directory (default: tests/e2e)', 'tests/e2e')
95
+ .option('--ci-only', 'Only add CI workflow, skip test files')
96
+ .option('--port <port>', 'Backend port (default: 3003)', '3003')
62
97
  .action((options) => {
63
98
  const root = process.cwd()
64
99
  const testDir = join(root, options.dir)
65
100
  const helpersDir = join(testDir, 'helpers')
66
101
  const force = !!options.force
102
+ const port = options.port
67
103
 
68
- console.log('\n Tetra Init Tests — Golden Standard E2E\n')
104
+ console.log('\n Tetra Init Tests — Golden Standard E2E v2\n')
69
105
 
70
- // 1. Helpers
71
- console.log(' Helpers:')
72
- copyTemplate('api-client.ts.tmpl', join(helpersDir, 'api-client.ts'), force)
73
- copyTemplate('test-users.ts.tmpl', join(helpersDir, 'test-users.ts'), force)
106
+ if (!options.ciOnly) {
107
+ // ── 1. Helpers ──
108
+ console.log(' Helpers:')
109
+ copyTemplate('api-client.ts.tmpl', join(helpersDir, 'api-client.ts'), force)
110
+ copyTemplate('test-users.ts.tmpl', join(helpersDir, 'test-users.ts'), force)
74
111
 
75
- // 2. Test files
76
- console.log('\n Test files:')
77
- copyTemplate('01-auth.test.ts.tmpl', join(testDir, '01-auth.test.ts'), force)
78
- copyTemplate('07-security.test.ts.tmpl', join(testDir, '07-security.test.ts'), force)
112
+ // ── 2. Global setup ──
113
+ console.log('\n Setup:')
114
+ copyTemplate('global-setup.ts.tmpl', join(testDir, 'global-setup.ts'), force)
115
+ writeFile(join(testDir, '.gitignore'), '.e2e-tokens.json\n', force)
79
116
 
80
- // 3. Vitest config
81
- console.log('\n Config:')
82
- writeFile(join(root, 'vitest.config.e2e.ts'), `import { defineConfig } from 'vitest/config';
117
+ // ── 3. Test files (complete suite) ──
118
+ console.log('\n Test files:')
119
+ copyTemplate('01-auth.test.ts.tmpl', join(testDir, '01-auth.test.ts'), force)
120
+ copyTemplate('02-crud-resources.test.ts.tmpl', join(testDir, '02-crud-resources.test.ts'), force)
121
+ copyTemplate('03-permissions.test.ts.tmpl', join(testDir, '03-permissions.test.ts'), force)
122
+ copyTemplate('04-business-flows.test.ts.tmpl', join(testDir, '04-business-flows.test.ts'), force)
123
+ copyTemplate('05-security.test.ts.tmpl', join(testDir, '05-security.test.ts'), force)
124
+
125
+ // ── 4. Vitest config ──
126
+ console.log('\n Config:')
127
+ writeFile(join(root, 'vitest.config.e2e.ts'), `import { defineConfig } from 'vitest/config';
83
128
 
84
129
  export default defineConfig({
85
130
  test: {
@@ -88,62 +133,136 @@ export default defineConfig({
88
133
  testTimeout: 30000,
89
134
  hookTimeout: 60000,
90
135
  sequence: { concurrent: false },
136
+ env: {
137
+ E2E_BASE_URL: process.env.E2E_BASE_URL || 'http://localhost:${port}',
138
+ E2E_ADMIN_EMAIL: process.env.E2E_ADMIN_EMAIL || 'e2e-admin@example.com',
139
+ E2E_PASSWORD: process.env.E2E_PASSWORD || 'TestPass1234',
140
+ RATE_LIMIT_BYPASS_KEY: process.env.RATE_LIMIT_BYPASS_KEY || '',
141
+ },
91
142
  },
92
143
  });
93
144
  `, force)
94
145
 
95
- // 4. Global setup stub
96
- writeFile(join(testDir, 'global-setup.ts'), `/**
97
- * Global setup — creates test users via API before all tests.
98
- * CUSTOMIZE: implement signup + invite flow for your project.
99
- *
100
- * Required env vars (set these in your test runner or CI):
101
- * E2E_BASE_URL — Backend URL (e.g. http://localhost:3026)
102
- * RATE_LIMIT_BYPASS_KEY — X-Test-Key value (from .ralph/test-config.yml)
103
- *
104
- * This setup should:
105
- * 1. Create an admin via signup
106
- * 2. Create a shared resource (track/project/workspace)
107
- * 3. Invite an elevated user (coach/manager)
108
- * 4. Invite a basic user (client/member)
109
- * 5. Store IDs/emails in process.env for test-users.ts
110
- */
146
+ // ── 5. npm scripts ──
147
+ console.log('\n Scripts:')
148
+ addNpmScript(root, 'test:e2e', `E2E_BASE_URL=http://localhost:${port} vitest run --config vitest.config.e2e.ts`)
149
+ addNpmScript(root, 'test:e2e:watch', `E2E_BASE_URL=http://localhost:${port} vitest --config vitest.config.e2e.ts`)
150
+ }
111
151
 
112
- const BASE_URL = process.env.E2E_BASE_URL!;
113
- const PASSWORD = 'TestPass1234';
152
+ // ── 6. CI workflow ──
153
+ console.log('\n CI:')
154
+ const ciPath = join(root, '.github', 'workflows', 'ci.yml')
155
+ if (existsSync(ciPath)) {
156
+ // Check if api-tests job already exists
157
+ const ciContent = readFileSync(ciPath, 'utf-8')
158
+ if (ciContent.includes('api-tests')) {
159
+ console.log(' ⏩ .github/workflows/ci.yml (api-tests job exists)')
160
+ } else {
161
+ console.log(' ⚠️ .github/workflows/ci.yml exists but has NO api-tests job')
162
+ console.log(' Add an api-tests job that runs: npx vitest run --config vitest.config.e2e.ts')
163
+ console.log(' See: tetra-init-tests --ci-only --force')
164
+ }
165
+ } else {
166
+ writeFile(ciPath, `name: CI
114
167
 
115
- export async function setup() {
116
- // TODO: Implement for your project
117
- // See CoachHub's global-setup.ts as reference
118
- console.log('\\n ⚠️ global-setup.ts is a stub — implement for your project\\n');
119
- }
168
+ on:
169
+ push:
170
+ branches: [main, develop]
171
+ pull_request:
172
+ branches: [main, develop]
120
173
 
121
- export async function teardown() {
122
- // Best-effort cleanup
123
- }
174
+ jobs:
175
+ typecheck:
176
+ runs-on: ubuntu-latest
177
+ steps:
178
+ - uses: actions/checkout@v4
179
+ - uses: actions/setup-node@v4
180
+ with:
181
+ node-version: 20
182
+ cache: npm
183
+ - run: npm ci
184
+ - run: cd backend && npx tsc --noEmit
185
+
186
+ frontend-build:
187
+ runs-on: ubuntu-latest
188
+ steps:
189
+ - uses: actions/checkout@v4
190
+ - uses: actions/setup-node@v4
191
+ with:
192
+ node-version: 20
193
+ cache: npm
194
+ - run: npm ci
195
+ - run: cd frontend && npx next build
196
+ env:
197
+ NEXT_PUBLIC_API_URL: http://localhost:${port}
198
+ NEXT_PUBLIC_SUPABASE_URL: https://placeholder.supabase.co
199
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: placeholder
200
+
201
+ api-tests:
202
+ runs-on: ubuntu-latest
203
+ needs: typecheck
204
+ env:
205
+ SUPABASE_URL: \${{ secrets.E2E_SUPABASE_URL }}
206
+ SUPABASE_ANON_KEY: \${{ secrets.E2E_SUPABASE_ANON_KEY }}
207
+ SUPABASE_SERVICE_ROLE_KEY: \${{ secrets.E2E_SUPABASE_SERVICE_ROLE_KEY }}
208
+ SUPABASE_JWT_SECRET: \${{ secrets.E2E_SUPABASE_JWT_SECRET }}
209
+ E2E_ADMIN_EMAIL: \${{ secrets.E2E_ADMIN_EMAIL }}
210
+ E2E_PASSWORD: \${{ secrets.E2E_PASSWORD }}
211
+ steps:
212
+ - uses: actions/checkout@v4
213
+ - uses: actions/setup-node@v4
214
+ with:
215
+ node-version: 20
216
+ cache: npm
217
+ - run: npm ci
218
+ - name: Start backend
219
+ run: |
220
+ cd backend && npx tsx src/index.ts &
221
+ for i in $(seq 1 30); do
222
+ curl -sf http://localhost:${port}/api/health > /dev/null 2>&1 && break
223
+ sleep 2
224
+ done
225
+ curl -sf http://localhost:${port}/api/health || exit 1
226
+ env:
227
+ NODE_ENV: test
228
+ PORT: ${port}
229
+ - name: Run API e2e tests
230
+ run: npx vitest run --config vitest.config.e2e.ts
231
+ env:
232
+ E2E_BASE_URL: http://localhost:${port}
124
233
  `, force)
234
+ }
125
235
 
126
- // Summary
236
+ // ── 7. Post-deploy workflow ──
237
+ const postDeployPath = join(root, '.github', 'workflows', 'post-deploy-tests.yml')
238
+ if (!existsSync(postDeployPath)) {
239
+ console.log(' 💡 No post-deploy-tests.yml found')
240
+ console.log(' Create one with: tetra-smoke --url https://your-app.railway.app')
241
+ }
242
+
243
+ // ── Summary ──
127
244
  console.log(`
128
- Done! Next steps:
129
-
130
- 1. Implement global-setup.ts (create test users via your API)
131
- 2. Set E2E_BASE_URL and RATE_LIMIT_BYPASS_KEY in your env
132
- 3. Customize 07-security.test.ts with your protected routes
133
- 4. Add project-specific test files (02-*.test.ts, 03-*.test.ts, etc.)
134
- 5. Run: npx vitest run --config vitest.config.e2e.ts
135
-
136
- Golden standard pattern:
137
- 01-auth login, session, refresh, logout (universal)
138
- 02-[resource] — main resource CRUD + permissions
139
- 03-[nested] — nested resources (messages, notes, etc.)
140
- 04-admin — admin-only features
141
- 05-[feature] — feature-specific tests
142
- 06-planner — scheduling (if using Tetra Planner)
143
- 07-security — auth walls + role access (universal)
144
- 08-permissions — permission matrix (generatable)
145
- 09-isolation — cross-role data isolation
146
- 10+ — additional features
245
+ ─────────────────────────────────────────
246
+ Created: ${filesCreated} files | Skipped: ${filesSkipped} (already exist)
247
+ ─────────────────────────────────────────
248
+
249
+ Next steps:
250
+
251
+ 1. Edit global-setup.ts set your test user email/password
252
+ 2. Edit 02-crud-resources.test.ts — add your FeatureConfig resources
253
+ 3. Edit 03-permissions.test.ts — add all protected routes
254
+ 4. Edit 04-business-flows.test.ts add your business flows
255
+ 5. Run: npm run test:e2e
256
+
257
+ Then run tetra-test-audit to see what's NOT covered yet.
258
+
259
+ Golden standard layers:
260
+ 01-auth Auth: login, session, refresh (universal)
261
+ 02-crud-resources CRUD for every resource (from FeatureConfig)
262
+ 03-permissions Auth walls for every route (auto-auditable)
263
+ 04-business-flows End-to-end user journeys
264
+ 05-security Input validation, injection, error format
265
+ 06+ Project-specific features
147
266
  `)
148
267
  })
149
268
 
package/bin/tetra-init.js CHANGED
@@ -430,6 +430,66 @@ async function initQuality(config, options) {
430
430
  )
431
431
  }
432
432
 
433
+ async function initCi(config, options) {
434
+ console.log('')
435
+ console.log('🔄 Initializing CI workflow...')
436
+
437
+ const workflowDir = join(projectRoot, '.github/workflows')
438
+ if (!existsSync(workflowDir)) {
439
+ mkdirSync(workflowDir, { recursive: true })
440
+ }
441
+
442
+ // Detect project structure
443
+ const hasBackend = existsSync(join(projectRoot, 'backend/package.json'))
444
+ const hasFrontend = existsSync(join(projectRoot, 'frontend/package.json'))
445
+ const hasMigrations = existsSync(join(projectRoot, 'backend/supabase/migrations')) ||
446
+ existsSync(join(projectRoot, 'supabase/migrations'))
447
+
448
+ let workspaces
449
+ if (hasBackend && hasFrontend) {
450
+ workspaces = 'backend,frontend'
451
+ } else if (hasBackend) {
452
+ workspaces = 'backend'
453
+ } else {
454
+ workspaces = '.'
455
+ }
456
+
457
+ let withLines = [` workspaces: "${workspaces}"`]
458
+ if (!hasFrontend) withLines.push(' skip-frontend: true')
459
+ if (!hasMigrations) withLines.push(' skip-migration-lint: true')
460
+
461
+ const workflowContent = `name: PR Quality Gate
462
+
463
+ on:
464
+ push:
465
+ branches: [main]
466
+ paths-ignore:
467
+ - '*.md'
468
+ - 'docs/**'
469
+ - '.ralph/**'
470
+ - 'LICENSE'
471
+ pull_request:
472
+ branches: [main]
473
+ paths-ignore:
474
+ - '*.md'
475
+ - 'docs/**'
476
+ - '.ralph/**'
477
+ - 'LICENSE'
478
+
479
+ concurrency:
480
+ group: quality-\${{ github.ref }}
481
+ cancel-in-progress: true
482
+
483
+ jobs:
484
+ quality:
485
+ uses: mralbertzwolle/tetra/.github/workflows/pr-quality.yml@main
486
+ with:
487
+ ${withLines.join('\n')}
488
+ `
489
+
490
+ writeIfMissing(join(workflowDir, 'quality.yml'), workflowContent, options)
491
+ }
492
+
433
493
  function checkCompleteness() {
434
494
  console.log('')
435
495
  console.log('🔎 Checking project completeness...')
@@ -441,6 +501,7 @@ function checkCompleteness() {
441
501
  { path: 'doppler.yaml', category: 'root', required: true },
442
502
  { path: 'CLAUDE.md', category: 'root', required: true },
443
503
  { path: '.tetra-quality.json', category: 'root', required: false },
504
+ { path: '.github/workflows/quality.yml', category: 'root', required: true },
444
505
  { path: '.gitignore', category: 'root', required: true },
445
506
 
446
507
  // .ralph/ files
@@ -524,7 +585,7 @@ program
524
585
  .name('tetra-init')
525
586
  .description('Initialize a Tetra project with all required config files')
526
587
  .version('1.0.0')
527
- .argument('[component]', 'Component to init: ralph, quality, check, or all (default)')
588
+ .argument('[component]', 'Component to init: ralph, quality, ci, check, or all (default)')
528
589
  .option('-n, --name <name>', 'Project name (skips interactive prompt)')
529
590
  .option('-d, --description <desc>', 'Project description')
530
591
  .option('--backend-port <port>', 'Backend port number', parseInt)
@@ -573,7 +634,7 @@ program
573
634
  console.log(` Frontend: localhost:${config.frontendPort}`)
574
635
 
575
636
  const components = component === 'all' || !component
576
- ? ['ralph', 'quality']
637
+ ? ['ralph', 'quality', 'ci']
577
638
  : [component]
578
639
 
579
640
  for (const comp of components) {
@@ -584,9 +645,12 @@ program
584
645
  case 'quality':
585
646
  await initQuality(config, options)
586
647
  break
648
+ case 'ci':
649
+ await initCi(config, options)
650
+ break
587
651
  default:
588
652
  console.log(`Unknown component: ${comp}`)
589
- console.log('Available: ralph, quality, check, or all (default)')
653
+ console.log('Available: ralph, quality, ci, check, or all (default)')
590
654
  }
591
655
  }
592
656
 
@@ -262,47 +262,66 @@ async function setupCi(options) {
262
262
  mkdirSync(workflowDir, { recursive: true })
263
263
  }
264
264
 
265
+ // Detect project structure
266
+ const hasBackend = existsSync(join(projectRoot, 'backend/package.json'))
267
+ const hasFrontend = existsSync(join(projectRoot, 'frontend/package.json'))
268
+ const hasMigrations = existsSync(join(projectRoot, 'backend/supabase/migrations')) ||
269
+ existsSync(join(projectRoot, 'supabase/migrations'))
270
+
271
+ // Determine workspaces
272
+ let workspaces
273
+ if (hasBackend && hasFrontend) {
274
+ workspaces = 'backend,frontend'
275
+ } else if (hasBackend) {
276
+ workspaces = 'backend'
277
+ } else {
278
+ workspaces = '.'
279
+ }
280
+
281
+ const skipFrontend = !hasFrontend
282
+ const skipMigrationLint = !hasMigrations
283
+
284
+ // Build with block for inputs
285
+ let withBlock = ` workspaces: "${workspaces}"`
286
+ if (skipFrontend) {
287
+ withBlock += `\n skip-frontend: true`
288
+ }
289
+ if (skipMigrationLint) {
290
+ withBlock += `\n skip-migration-lint: true`
291
+ }
292
+
265
293
  const workflowPath = join(workflowDir, 'quality.yml')
266
294
  if (!existsSync(workflowPath) || options.force) {
267
- const workflowContent = `name: Quality Checks
295
+ const workflowContent = `name: PR Quality Gate
268
296
 
269
297
  on:
270
298
  push:
271
- branches: [main, master]
299
+ branches: [main]
300
+ paths-ignore:
301
+ - '*.md'
302
+ - 'docs/**'
303
+ - '.ralph/**'
304
+ - 'LICENSE'
272
305
  pull_request:
273
- branches: [main, master]
306
+ branches: [main]
307
+ paths-ignore:
308
+ - '*.md'
309
+ - 'docs/**'
310
+ - '.ralph/**'
311
+ - 'LICENSE'
312
+
313
+ concurrency:
314
+ group: quality-\${{ github.ref }}
315
+ cancel-in-progress: true
274
316
 
275
317
  jobs:
276
318
  quality:
277
- name: 🔍 Tetra Quality Audit
278
- runs-on: ubuntu-latest
279
-
280
- steps:
281
- - name: Checkout code
282
- uses: actions/checkout@v4
283
-
284
- - name: Setup Node.js
285
- uses: actions/setup-node@v4
286
- with:
287
- node-version: '20'
288
- cache: 'npm'
289
-
290
- - name: Install dependencies
291
- run: npm ci
292
-
293
- - name: Run Tetra Quality Audit
294
- run: npx tetra-audit --ci
295
-
296
- - name: Upload results
297
- if: always()
298
- uses: actions/upload-artifact@v4
299
- with:
300
- name: quality-report
301
- path: quality-report.json
302
- retention-days: 7
319
+ uses: mralbertzwolle/tetra/.github/workflows/pr-quality.yml@main
320
+ with:
321
+ ${withBlock}
303
322
  `
304
323
  writeFileSync(workflowPath, workflowContent)
305
- console.log(' ✅ Created .github/workflows/quality.yml')
324
+ console.log(` ✅ Created .github/workflows/quality.yml (${workspaces}, frontend=${!skipFrontend}, migrations=${!skipMigrationLint})`)
306
325
  } else {
307
326
  console.log(' ⏭️ Workflow already exists (use --force to overwrite)')
308
327
  }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Tetra Test Audit — Discover test coverage gaps across a Tetra project.
5
+ *
6
+ * Scans FeatureConfigs, routes, frontend pages, and test files to find:
7
+ * - Features without CRUD tests
8
+ * - API endpoints without auth wall tests
9
+ * - Frontend pages without e2e specs
10
+ * - CI pipeline gaps
11
+ *
12
+ * Usage:
13
+ * tetra-test-audit # Audit current project
14
+ * tetra-test-audit --path /path/to/project # Audit specific project
15
+ * tetra-test-audit --json # JSON output for CI
16
+ * tetra-test-audit --ci # GitHub Actions annotations
17
+ * tetra-test-audit --strict # Fail on any gap
18
+ *
19
+ * Exit codes:
20
+ * 0 = all critical checks pass (or no gaps in --strict mode)
21
+ * 1 = gaps found
22
+ */
23
+
24
+ import { program } from 'commander'
25
+ import chalk from 'chalk'
26
+ import {
27
+ runTestCoverageAudit,
28
+ formatReport,
29
+ formatReportJSON,
30
+ formatCIAnnotations,
31
+ } from '../lib/audits/test-coverage-audit.js'
32
+
33
+ program
34
+ .name('tetra-test-audit')
35
+ .description('Audit test coverage gaps across a Tetra project')
36
+ .version('1.0.0')
37
+ .option('--path <dir>', 'Project root directory (default: cwd)')
38
+ .option('--json', 'JSON output for CI')
39
+ .option('--ci', 'GitHub Actions annotations for failures')
40
+ .option('--strict', 'Fail on any gap (default: only fail on critical gaps like missing auth tests)')
41
+ .action(async (options) => {
42
+ try {
43
+ const projectRoot = options.path || process.cwd()
44
+
45
+ if (!options.json) {
46
+ console.log(chalk.gray('\n Scanning project...'))
47
+ }
48
+
49
+ const report = await runTestCoverageAudit(projectRoot)
50
+
51
+ // Output
52
+ if (options.json) {
53
+ console.log(formatReportJSON(report))
54
+ } else {
55
+ console.log(formatReport(report, chalk))
56
+
57
+ if (options.ci) {
58
+ const annotations = formatCIAnnotations(report)
59
+ if (annotations) {
60
+ console.log(annotations)
61
+ }
62
+ }
63
+ }
64
+
65
+ // Exit code
66
+ const { summary } = report
67
+ if (options.strict) {
68
+ // Strict: fail on any gap
69
+ process.exit(summary.totalGaps > 0 ? 1 : 0)
70
+ } else {
71
+ // Default: only fail on critical gaps (missing auth wall tests)
72
+ process.exit(summary.criticalGaps > 0 ? 1 : 0)
73
+ }
74
+ } catch (err) {
75
+ console.error(chalk.red(`\n ERROR: ${err.message}\n`))
76
+ if (!options.json) {
77
+ console.error(chalk.gray(` ${err.stack}`))
78
+ }
79
+ process.exit(1)
80
+ }
81
+ })
82
+
83
+ program.parse()