@sudobility/testomniac_runner 0.0.128

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 (60) hide show
  1. package/.dockerignore +75 -0
  2. package/.env.example +67 -0
  3. package/.github/workflows/ci-cd.yml +30 -0
  4. package/.prettierignore +62 -0
  5. package/.prettierrc +11 -0
  6. package/.vscode/settings.json +29 -0
  7. package/CLAUDE.md +170 -0
  8. package/Dockerfile +76 -0
  9. package/README.md +22 -0
  10. package/bun.lock +707 -0
  11. package/docs/superpowers/specs/2026-04-20-smarter-scanner-navigation-design.md +121 -0
  12. package/eslint.config.js +80 -0
  13. package/package.json +55 -0
  14. package/plans/DATA.md +703 -0
  15. package/plans/POLLING.md +569 -0
  16. package/plans/RUNNER.md +288 -0
  17. package/src/adapters/PuppeteerAdapter.ts +394 -0
  18. package/src/auth/credential-manager.ts +17 -0
  19. package/src/auth/form-identifier.test.ts +136 -0
  20. package/src/auth/form-identifier.ts +54 -0
  21. package/src/auth/login-executor.ts +112 -0
  22. package/src/auth/password-detector.test.ts +61 -0
  23. package/src/auth/password-detector.ts +119 -0
  24. package/src/auth/signic-registrar.ts +186 -0
  25. package/src/browser/chromium.ts +35 -0
  26. package/src/config/index.test.ts +23 -0
  27. package/src/config/index.ts +35 -0
  28. package/src/email/deep-link.test.ts +17 -0
  29. package/src/email/deep-link.ts +23 -0
  30. package/src/email/sender.ts +35 -0
  31. package/src/email/templates.ts +34 -0
  32. package/src/index.test.ts +17 -0
  33. package/src/index.ts +110 -0
  34. package/src/orchestrator.ts +220 -0
  35. package/src/plugins/content/ai-checks.ts +115 -0
  36. package/src/plugins/content/checks.test.ts +49 -0
  37. package/src/plugins/content/checks.ts +141 -0
  38. package/src/plugins/content/index.ts +73 -0
  39. package/src/plugins/registry.test.ts +49 -0
  40. package/src/plugins/registry.ts +21 -0
  41. package/src/plugins/security/header-checks.ts +56 -0
  42. package/src/plugins/security/html-checks.ts +93 -0
  43. package/src/plugins/security/index.ts +58 -0
  44. package/src/plugins/security/network-checks.test.ts +74 -0
  45. package/src/plugins/security/network-checks.ts +136 -0
  46. package/src/plugins/seo/checks.test.ts +70 -0
  47. package/src/plugins/seo/checks.ts +173 -0
  48. package/src/plugins/seo/index.ts +85 -0
  49. package/src/plugins/types.ts +43 -0
  50. package/src/plugins/ui-consistency/comparator.test.ts +108 -0
  51. package/src/plugins/ui-consistency/comparator.ts +58 -0
  52. package/src/plugins/ui-consistency/index.ts +36 -0
  53. package/src/plugins/ui-consistency/style-extractor.ts +79 -0
  54. package/src/runner/executor.test.ts +37 -0
  55. package/src/runner/executor.ts +167 -0
  56. package/src/runner/reporter.ts +19 -0
  57. package/src/runner/worker-pool.ts +106 -0
  58. package/src/runner-manager.ts +163 -0
  59. package/src/scanner/email-checker.ts +106 -0
  60. package/tsconfig.json +21 -0
@@ -0,0 +1,569 @@
1
+ # Plan: Scanner-to-API Migration (Polling Architecture)
2
+
3
+ ## Context
4
+
5
+ The scanner currently shares a Postgres database directly with the API, causing tight schema coupling. Both projects maintain separate Drizzle schema definitions — any column change requires coordinated updates in both repos. This plan migrates to a clean architecture where the scanner calls the API via HTTP, and the API is the sole DB owner.
6
+
7
+ **Outcome**: Scanner becomes a stateless HTTP client. API exposes fine-grained REST endpoints. All request/response types are defined in `@sudobility/testomniac_types` so both projects share a single contract. DB code is removed from the scanner entirely.
8
+
9
+ **Post-modification workflow**: After modifying `testomniac_types`, run `/testomniac_app/scripts/push_all.sh` to publish and propagate changes.
10
+
11
+ ---
12
+
13
+ ## Phase 1: Define Shared Types in `testomniac_types`
14
+
15
+ Add request/response types for every scanner API endpoint. Both `testomniac_api` and `testomniac_runner` import these types — no inline type definitions in either project.
16
+
17
+ **File to modify**: `~/projects/testomniac_types/src/index.ts`
18
+
19
+ ### New types to add:
20
+
21
+ ```typescript
22
+ // --- Runs ---
23
+ interface PendingRunResponse {
24
+ id: number;
25
+ appId: number;
26
+ sizeClass: string;
27
+ status: string;
28
+ }
29
+
30
+ interface UpdateRunPhaseRequest {
31
+ phase: string;
32
+ }
33
+
34
+ interface UpdateRunStatsRequest {
35
+ pagesFound?: number;
36
+ pageStatesFound?: number;
37
+ actionsCompleted?: number;
38
+ }
39
+
40
+ interface UpdatePhaseDurationRequest {
41
+ field: string;
42
+ durationMs: number;
43
+ }
44
+
45
+ interface CompleteRunRequest {
46
+ aiSummary?: string;
47
+ totalDurationMs?: number;
48
+ }
49
+
50
+ // --- Pages ---
51
+ interface FindOrCreatePageRequest {
52
+ appId: number;
53
+ url: string;
54
+ }
55
+
56
+ interface PageResponse {
57
+ id: number;
58
+ appId: number;
59
+ url: string;
60
+ routeKey: string | null;
61
+ requiresLogin: boolean | null;
62
+ createdAt: string | null;
63
+ }
64
+
65
+ interface MarkRequiresLoginRequest {
66
+ pageId: number;
67
+ }
68
+
69
+ // --- Page States ---
70
+ interface FindPageStateRequest {
71
+ pageId: number;
72
+ sizeClass: string;
73
+ hashes: PageHashes;
74
+ }
75
+
76
+ interface CreatePageStateRequest {
77
+ pageId: number;
78
+ sizeClass: string;
79
+ hashes: PageHashes;
80
+ screenshotPath?: string;
81
+ rawHtmlPath?: string;
82
+ contentText?: string;
83
+ }
84
+
85
+ interface PageStateResponse {
86
+ id: number;
87
+ pageId: number;
88
+ sizeClass: string;
89
+ htmlHash: string | null;
90
+ normalizedHtmlHash: string | null;
91
+ textHash: string | null;
92
+ actionableHash: string | null;
93
+ createdByActionId: number;
94
+ screenshotPath: string | null;
95
+ rawHtmlPath: string | null;
96
+ contentText: string | null;
97
+ capturedAt: string | null;
98
+ }
99
+
100
+ // --- Actionable Items ---
101
+ interface InsertActionableItemsRequest {
102
+ pageStateId: number;
103
+ items: ActionableItem[];
104
+ }
105
+
106
+ interface ActionableItemResponse {
107
+ id: number;
108
+ pageStateId: number;
109
+ stableKey: string | null;
110
+ selector: string | null;
111
+ tagName: string | null;
112
+ role: string | null;
113
+ actionKind: string | null;
114
+ accessibleName: string | null;
115
+ disabled: boolean | null;
116
+ visible: boolean | null;
117
+ x: number | null;
118
+ y: number | null;
119
+ width: number | null;
120
+ height: number | null;
121
+ attributesJson: unknown;
122
+ }
123
+
124
+ // --- Actions ---
125
+ interface CreateActionRequest {
126
+ runId: number;
127
+ type: string;
128
+ actionableItemId?: number;
129
+ startingPageStateId?: number;
130
+ targetPageId?: number;
131
+ sizeClass: string;
132
+ personaId?: number;
133
+ useCaseId?: number;
134
+ inputValue?: string;
135
+ }
136
+
137
+ interface CompleteActionRequest {
138
+ targetPageId?: number;
139
+ targetPageStateId?: number;
140
+ durationMs?: number;
141
+ consoleLog?: string;
142
+ networkLog?: string;
143
+ screenshotBefore?: string;
144
+ screenshotAfter?: string;
145
+ }
146
+
147
+ interface ActionResponse {
148
+ id: number;
149
+ runId: number;
150
+ type: string;
151
+ actionableItemId: number;
152
+ startingPageStateId: number;
153
+ targetPageId: number;
154
+ targetPageStateId: number;
155
+ personaId: number;
156
+ useCaseId: number;
157
+ inputValue: string | null;
158
+ status: string;
159
+ sizeClass: string;
160
+ durationMs: number | null;
161
+ screenshotBefore: string | null;
162
+ screenshotAfter: string | null;
163
+ consoleLog: string | null;
164
+ networkLog: string | null;
165
+ startedAt: string | null;
166
+ executedAt: string | null;
167
+ }
168
+
169
+ // --- Personas / Use Cases / Input Values ---
170
+ interface CreatePersonaRequest {
171
+ appId: number;
172
+ name: string;
173
+ description: string;
174
+ }
175
+
176
+ interface PersonaResponse {
177
+ id: number;
178
+ appId: number;
179
+ name: string;
180
+ description: string | null;
181
+ createdAt: string | null;
182
+ }
183
+
184
+ interface CreateUseCaseRequest {
185
+ personaId: number;
186
+ name: string;
187
+ description: string;
188
+ }
189
+
190
+ interface UseCaseResponse {
191
+ id: number;
192
+ personaId: number;
193
+ name: string;
194
+ description: string | null;
195
+ createdAt: string | null;
196
+ }
197
+
198
+ interface CreateInputValueRequest {
199
+ useCaseId: number;
200
+ fieldSelector: string;
201
+ fieldName: string;
202
+ value: string;
203
+ }
204
+
205
+ interface InputValueResponse {
206
+ id: number;
207
+ useCaseId: number;
208
+ fieldSelector: string;
209
+ fieldName: string | null;
210
+ value: string;
211
+ createdAt: string | null;
212
+ }
213
+
214
+ // --- Forms ---
215
+ interface InsertFormRequest {
216
+ pageStateId: number;
217
+ form: FormInfo;
218
+ formType?: string;
219
+ }
220
+
221
+ interface FormResponse {
222
+ id: number;
223
+ pageStateId: number;
224
+ selector: string;
225
+ action: string | null;
226
+ method: string | null;
227
+ submitSelector: string | null;
228
+ fieldCount: number | null;
229
+ formType: string | null;
230
+ fieldsJson: unknown;
231
+ createdAt: string | null;
232
+ }
233
+
234
+ // --- Test Cases ---
235
+ interface InsertTestCaseRequest {
236
+ runId: number;
237
+ testCase: TestCase;
238
+ }
239
+
240
+ interface TestCaseResponse {
241
+ id: number;
242
+ runId: number;
243
+ name: string;
244
+ testType: string;
245
+ sizeClass: string;
246
+ suiteTags: string[];
247
+ pageId: number;
248
+ personaId: number;
249
+ useCaseId: number;
250
+ priority: string;
251
+ actionsJson: unknown;
252
+ generatedAt: string | null;
253
+ }
254
+
255
+ // --- Test Runs ---
256
+ interface CreateTestRunRequest {
257
+ testCaseId: number;
258
+ runId: number;
259
+ screen: string;
260
+ }
261
+
262
+ interface CompleteTestRunRequest {
263
+ status: string;
264
+ durationMs: number;
265
+ errorMessage?: string;
266
+ screenshotPath?: string;
267
+ consoleLog?: string;
268
+ networkLog?: string;
269
+ }
270
+
271
+ interface TestRunResponse {
272
+ id: number;
273
+ testCaseId: number;
274
+ runId: number;
275
+ screen: string;
276
+ status: string;
277
+ durationMs: number | null;
278
+ errorMessage: string | null;
279
+ screenshotPath: string | null;
280
+ consoleLog: string | null;
281
+ networkLog: string | null;
282
+ startedAt: string | null;
283
+ completedAt: string | null;
284
+ }
285
+
286
+ // --- Issues ---
287
+ interface CreateIssueRequest {
288
+ runId: number;
289
+ actionId?: number;
290
+ testCaseId?: number;
291
+ testRunId?: number;
292
+ type: string;
293
+ description: string;
294
+ reproductionSteps: unknown[];
295
+ consoleLog?: string;
296
+ networkLog?: string;
297
+ screenshotPath?: string;
298
+ pageId?: number;
299
+ pageStateId?: number;
300
+ }
301
+
302
+ interface IssueResponse {
303
+ id: number;
304
+ runId: number;
305
+ actionId: number;
306
+ testCaseId: number;
307
+ testRunId: number;
308
+ type: string;
309
+ description: string;
310
+ reproductionSteps: unknown;
311
+ consoleLog: string | null;
312
+ networkLog: string | null;
313
+ screenshotPath: string | null;
314
+ pageId: number;
315
+ pageStateId: number;
316
+ createdAt: string | null;
317
+ }
318
+
319
+ // --- AI Usage ---
320
+ interface RecordAiUsageRequest {
321
+ runId: number;
322
+ phase: string;
323
+ model: string;
324
+ promptTokens: number;
325
+ completionTokens: number;
326
+ totalTokens: number;
327
+ purpose?: string;
328
+ }
329
+
330
+ // --- Report Emails ---
331
+ interface CreateReportEmailRequest {
332
+ runId: number;
333
+ userEmail: string;
334
+ deepLinkToken: string;
335
+ }
336
+
337
+ // --- Components ---
338
+ interface SaveComponentRequest {
339
+ appId: number;
340
+ sizeClass: string;
341
+ component: {
342
+ name: string;
343
+ selector: string;
344
+ hash: string;
345
+ canonicalPageStateId: number;
346
+ instances: Array<{
347
+ pageStateId: number;
348
+ isIdentical: boolean;
349
+ hash: string;
350
+ }>;
351
+ };
352
+ }
353
+
354
+ // --- Apps ---
355
+ interface AppResponse {
356
+ id: number;
357
+ projectId: number;
358
+ name: string;
359
+ baseUrl: string | null;
360
+ normalizedBaseUrl: string;
361
+ createdAt: string | null;
362
+ }
363
+ ```
364
+
365
+ **After modifying**: Run `~/projects/testomniac_app/scripts/push_all.sh` to publish the updated types package.
366
+
367
+ ---
368
+
369
+ ## Phase 2: Add Scanner Tables to API
370
+
371
+ The API currently only has entity-related tables. All scanner tables need to be added.
372
+
373
+ **Files to modify:**
374
+ - `~/projects/testomniac_api/src/db/schema.ts` — add Drizzle schema definitions for all scanner tables
375
+ - `~/projects/testomniac_api/src/db/index.ts` — add `CREATE TABLE IF NOT EXISTS` statements in `initDatabase()`
376
+
377
+ **Tables to add** (matching the scanner's existing schema exactly):
378
+ projects, apps, runs, pages, page_states, actionable_items, actions, personas, use_cases, input_values, forms, components, component_instances, test_cases, test_runs, issues, ai_usage, report_emails
379
+
380
+ All tables go under the `testomniac` schema (same as existing tables). Use the scanner's `src/db/schema.ts` as the source of truth for column definitions.
381
+
382
+ ---
383
+
384
+ ## Phase 3: API Key Middleware for Scanner Auth
385
+
386
+ **File to create**: `~/projects/testomniac_api/src/middleware/scannerAuth.ts`
387
+
388
+ ```
389
+ Check X-Scanner-Key header against SCANNER_API_KEY env var.
390
+ Missing or invalid key → 401.
391
+ ```
392
+
393
+ **File to modify**: `~/projects/testomniac_api/.env.example` — add `SCANNER_API_KEY=`
394
+
395
+ ---
396
+
397
+ ## Phase 4: Firebase Auth + Entity Access Control on GET Endpoints
398
+
399
+ **File to create**: `~/projects/testomniac_api/src/middleware/projectAccess.ts`
400
+
401
+ Logic:
402
+ 1. Load project by ID from URL param
403
+ 2. If project has no `entityId` → allow (anyone can view unclaimed projects)
404
+ 3. If project has `entityId`:
405
+ a. Require Firebase auth (user must be logged in)
406
+ b. Check if user is associated with that entity using `entityHelpers.members.isMember(entityId, userId)`
407
+ c. For personal entities: `isMember` returns true for the owner
408
+ d. For organization entities: `isMember` returns true for any active member (which covers member entities)
409
+ e. If not a member → 403
410
+
411
+ This reuses the `@sudobility/entity_service` `isMember()` method which already handles both personal and organization entity types.
412
+
413
+ ---
414
+
415
+ ## Phase 5: REST Endpoints in API
416
+
417
+ ### 5a. Public endpoint (no auth)
418
+
419
+ **File to create**: `~/projects/testomniac_api/src/routes/scan.ts`
420
+
421
+ | Method | Path | Request Type | Response Type |
422
+ |--------|------|-------------|---------------|
423
+ | POST | `/api/v1/scan` | `CreateScanRequest` | `BaseResponse<CreateScanResponse>` |
424
+
425
+ Creates project + app + run + initial navigate action. Uses existing types already in `testomniac_types`.
426
+
427
+ ### 5b. Scanner-facing endpoints (API key auth)
428
+
429
+ **File to create**: `~/projects/testomniac_api/src/routes/scanner.ts`
430
+
431
+ All wrapped with `scannerAuthMiddleware`. All request/response bodies use types from `@sudobility/testomniac_types`.
432
+
433
+ | Method | Path | Request Type | Response Type |
434
+ |--------|------|-------------|---------------|
435
+ | GET | `/scanner/runs/pending` | — | `BaseResponse<PendingRunResponse \| null>` |
436
+ | PATCH | `/scanner/runs/:id/phase` | `UpdateRunPhaseRequest` | `BaseResponse<void>` |
437
+ | PATCH | `/scanner/runs/:id/stats` | `UpdateRunStatsRequest` | `BaseResponse<void>` |
438
+ | PATCH | `/scanner/runs/:id/phase-duration` | `UpdatePhaseDurationRequest` | `BaseResponse<void>` |
439
+ | PATCH | `/scanner/runs/:id/complete` | `CompleteRunRequest` | `BaseResponse<void>` |
440
+ | GET | `/scanner/apps/:id` | — | `BaseResponse<AppResponse>` |
441
+ | POST | `/scanner/pages` | `FindOrCreatePageRequest` | `BaseResponse<PageResponse>` |
442
+ | PATCH | `/scanner/pages/:id/requires-login` | — | `BaseResponse<void>` |
443
+ | GET | `/scanner/pages?appId=X` | — | `BaseResponse<PageResponse[]>` |
444
+ | POST | `/scanner/page-states` | `CreatePageStateRequest` | `BaseResponse<PageStateResponse>` |
445
+ | GET | `/scanner/page-states/match` | query: pageId, sizeClass, hashes | `BaseResponse<PageStateResponse \| null>` |
446
+ | POST | `/scanner/actionable-items` | `InsertActionableItemsRequest` | `BaseResponse<ActionableItemResponse[]>` |
447
+ | GET | `/scanner/actionable-items?pageStateId=X` | — | `BaseResponse<ActionableItemResponse[]>` |
448
+ | POST | `/scanner/actions` | `CreateActionRequest` | `BaseResponse<ActionResponse>` |
449
+ | GET | `/scanner/actions/next?runId=X&sizeClass=Y` | — | `BaseResponse<ActionResponse \| null>` |
450
+ | PATCH | `/scanner/actions/:id/start` | — | `BaseResponse<void>` |
451
+ | PATCH | `/scanner/actions/:id/complete` | `CompleteActionRequest` | `BaseResponse<void>` |
452
+ | GET | `/scanner/actions/open-count?runId=X&sizeClass=Y` | — | `BaseResponse<{count: number}>` |
453
+ | GET | `/scanner/actions/chain/:id` | — | `BaseResponse<ActionResponse[]>` |
454
+ | POST | `/scanner/personas` | `CreatePersonaRequest` | `BaseResponse<PersonaResponse>` |
455
+ | GET | `/scanner/personas?appId=X` | — | `BaseResponse<PersonaResponse[]>` |
456
+ | POST | `/scanner/use-cases` | `CreateUseCaseRequest` | `BaseResponse<UseCaseResponse>` |
457
+ | GET | `/scanner/use-cases?personaId=X` | — | `BaseResponse<UseCaseResponse[]>` |
458
+ | POST | `/scanner/input-values` | `CreateInputValueRequest` | `BaseResponse<InputValueResponse>` |
459
+ | GET | `/scanner/input-values?useCaseId=X` | — | `BaseResponse<InputValueResponse[]>` |
460
+ | POST | `/scanner/forms` | `InsertFormRequest` | `BaseResponse<FormResponse>` |
461
+ | GET | `/scanner/forms?pageStateId=X` | — | `BaseResponse<FormResponse[]>` |
462
+ | POST | `/scanner/test-cases` | `InsertTestCaseRequest` | `BaseResponse<TestCaseResponse>` |
463
+ | GET | `/scanner/test-cases?runId=X` | — | `BaseResponse<TestCaseResponse[]>` |
464
+ | POST | `/scanner/test-runs` | `CreateTestRunRequest` | `BaseResponse<TestRunResponse>` |
465
+ | PATCH | `/scanner/test-runs/:id/complete` | `CompleteTestRunRequest` | `BaseResponse<void>` |
466
+ | POST | `/scanner/issues` | `CreateIssueRequest` | `BaseResponse<IssueResponse>` |
467
+ | GET | `/scanner/issues?runId=X` | — | `BaseResponse<IssueResponse[]>` |
468
+ | POST | `/scanner/ai-usage` | `RecordAiUsageRequest` | `BaseResponse<void>` |
469
+ | POST | `/scanner/report-emails` | `CreateReportEmailRequest` | `BaseResponse<void>` |
470
+ | POST | `/scanner/components` | `SaveComponentRequest` | `BaseResponse<void>` |
471
+
472
+ ### 5c. User-facing GET endpoints (Firebase auth + project access)
473
+
474
+ **File to create**: `~/projects/testomniac_api/src/routes/projects.ts`
475
+
476
+ | Method | Path | Response Type |
477
+ |--------|------|---------------|
478
+ | GET | `/projects/:id` | `BaseResponse<ProjectSummaryResponse>` |
479
+ | GET | `/projects/:id/runs` | `BaseResponse<RunDetailResponse[]>` |
480
+ | GET | `/runs/:id` | `BaseResponse<RunDetailResponse>` |
481
+ | GET | `/runs/:id/issues` | `BaseResponse<IssueResponse[]>` |
482
+ | GET | `/runs/:id/test-cases` | `BaseResponse<TestCaseResponse[]>` |
483
+ | GET | `/runs/:id/test-runs` | `BaseResponse<TestRunResponse[]>` |
484
+
485
+ **File to modify**: `~/projects/testomniac_api/src/routes/index.ts` — mount all new routers
486
+
487
+ ---
488
+
489
+ ## Phase 6: Refactor Scanner to Use HTTP Client
490
+
491
+ ### 6a. Create API client
492
+
493
+ **File to create**: `~/projects/testomniac_runner/src/api/client.ts`
494
+
495
+ A typed HTTP client that:
496
+ - Uses `TESTOMNIAC_API_URL` as base URL
497
+ - Sends `X-Scanner-Key` header with `SCANNER_API_KEY`
498
+ - Exposes methods matching every scanner endpoint
499
+ - All methods use types from `@sudobility/testomniac_types` for request params and return types
500
+ - Unwraps `BaseResponse<T>` and throws on error responses
501
+
502
+ ### 6b. Replace DB calls with API calls
503
+
504
+ **Files to modify** (replace `import * as xxxRepo from "../db/repositories/xxx"` with `import { apiClient } from "../api/client"`):
505
+
506
+ | Scanner File | DB Repos Used | Replacement |
507
+ |-------------|---------------|-------------|
508
+ | `src/index.ts` | runs, apps | `apiClient.getPendingRun()`, `apiClient.getApp()`, `apiClient.updateRunPhase()`, `apiClient.completeRun()` |
509
+ | `src/orchestrator.ts` | projects, apps, runs, testCases, reportEmails | All corresponding `apiClient.*` methods |
510
+ | `src/scanner/mouse-scanner.ts` | pages, pageStates, actionableItems, actions, runs | All corresponding `apiClient.*` methods |
511
+ | `src/ai/analyzer.ts` | personas (createPersona, createUseCase, createInputValue) | `apiClient.createPersona()`, etc. |
512
+ | `src/scanner/input-scanner.ts` | personas (getPersonasByApp, getUseCasesByPersona, getInputValuesByUseCase) | `apiClient.getPersonas()`, etc. |
513
+ | `src/generation/generator.ts` | pages (getPagesByApp) | `apiClient.getPages()` |
514
+ | `src/runner/worker-pool.ts` | testRuns, issues | `apiClient.createTestRun()`, `apiClient.completeTestRun()`, `apiClient.createIssue()` |
515
+ | `src/ai/token-tracker.ts` | aiUsage | `apiClient.recordAiUsage()` |
516
+
517
+ ### 6c. Update scanner config
518
+
519
+ **File to modify**: `src/config/index.ts`
520
+ - Remove `databaseUrl`
521
+ - Add `apiUrl` (from `TESTOMNIAC_API_URL`, default `http://localhost:8027`)
522
+ - Add `scannerApiKey` (from `SCANNER_API_KEY`)
523
+
524
+ **File to modify**: `.env.example`
525
+ - Remove `DATABASE_URL`
526
+ - Add `TESTOMNIAC_API_URL=http://localhost:8027`
527
+ - Add `SCANNER_API_KEY=`
528
+
529
+ ### 6d. Delete DB code
530
+
531
+ **Delete entirely**:
532
+ - `src/db/` directory (connection.ts, schema.ts, repositories/*)
533
+ - `drizzle.config.ts`
534
+
535
+ **Remove from `package.json` dependencies**:
536
+ - `drizzle-orm`
537
+ - `drizzle-kit`
538
+ - `postgres`
539
+
540
+ ---
541
+
542
+ ## Phase 7: Verification
543
+
544
+ 1. **Type check**: `bun run build` in `testomniac_types` — all new types compile
545
+ 2. **API tests**: Start API, verify:
546
+ - `POST /api/v1/scan` creates project + app + run + action
547
+ - Scanner endpoints return 401 without `X-Scanner-Key`
548
+ - Scanner endpoints work with correct key
549
+ - User GET endpoints enforce entity access control
550
+ 3. **Scanner tests**: Start scanner with `TESTOMNIAC_API_URL` + `SCANNER_API_KEY`:
551
+ - Polls `GET /scanner/runs/pending` on interval
552
+ - Picks up and completes a scan via API calls
553
+ 4. **End-to-end**: Submit URL → scanner polls → scan completes → GET results with Firebase token
554
+ 5. **Negative tests**:
555
+ - Invalid scanner API key → 401
556
+ - User not in entity → 403 on project GET
557
+ - No entity on project → anyone can GET
558
+
559
+ ---
560
+
561
+ ## Execution Order
562
+
563
+ 1. **Phase 1** (testomniac_types) → run `push_all.sh`
564
+ 2. **Phase 2** (API schema + tables)
565
+ 3. **Phase 3** (API scanner auth middleware)
566
+ 4. **Phase 4** (API project access middleware)
567
+ 5. **Phase 5** (API endpoints)
568
+ 6. **Phase 6** (scanner refactor)
569
+ 7. **Phase 7** (verification)