@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.
- package/.dockerignore +75 -0
- package/.env.example +67 -0
- package/.github/workflows/ci-cd.yml +30 -0
- package/.prettierignore +62 -0
- package/.prettierrc +11 -0
- package/.vscode/settings.json +29 -0
- package/CLAUDE.md +170 -0
- package/Dockerfile +76 -0
- package/README.md +22 -0
- package/bun.lock +707 -0
- package/docs/superpowers/specs/2026-04-20-smarter-scanner-navigation-design.md +121 -0
- package/eslint.config.js +80 -0
- package/package.json +55 -0
- package/plans/DATA.md +703 -0
- package/plans/POLLING.md +569 -0
- package/plans/RUNNER.md +288 -0
- package/src/adapters/PuppeteerAdapter.ts +394 -0
- package/src/auth/credential-manager.ts +17 -0
- package/src/auth/form-identifier.test.ts +136 -0
- package/src/auth/form-identifier.ts +54 -0
- package/src/auth/login-executor.ts +112 -0
- package/src/auth/password-detector.test.ts +61 -0
- package/src/auth/password-detector.ts +119 -0
- package/src/auth/signic-registrar.ts +186 -0
- package/src/browser/chromium.ts +35 -0
- package/src/config/index.test.ts +23 -0
- package/src/config/index.ts +35 -0
- package/src/email/deep-link.test.ts +17 -0
- package/src/email/deep-link.ts +23 -0
- package/src/email/sender.ts +35 -0
- package/src/email/templates.ts +34 -0
- package/src/index.test.ts +17 -0
- package/src/index.ts +110 -0
- package/src/orchestrator.ts +220 -0
- package/src/plugins/content/ai-checks.ts +115 -0
- package/src/plugins/content/checks.test.ts +49 -0
- package/src/plugins/content/checks.ts +141 -0
- package/src/plugins/content/index.ts +73 -0
- package/src/plugins/registry.test.ts +49 -0
- package/src/plugins/registry.ts +21 -0
- package/src/plugins/security/header-checks.ts +56 -0
- package/src/plugins/security/html-checks.ts +93 -0
- package/src/plugins/security/index.ts +58 -0
- package/src/plugins/security/network-checks.test.ts +74 -0
- package/src/plugins/security/network-checks.ts +136 -0
- package/src/plugins/seo/checks.test.ts +70 -0
- package/src/plugins/seo/checks.ts +173 -0
- package/src/plugins/seo/index.ts +85 -0
- package/src/plugins/types.ts +43 -0
- package/src/plugins/ui-consistency/comparator.test.ts +108 -0
- package/src/plugins/ui-consistency/comparator.ts +58 -0
- package/src/plugins/ui-consistency/index.ts +36 -0
- package/src/plugins/ui-consistency/style-extractor.ts +79 -0
- package/src/runner/executor.test.ts +37 -0
- package/src/runner/executor.ts +167 -0
- package/src/runner/reporter.ts +19 -0
- package/src/runner/worker-pool.ts +106 -0
- package/src/runner-manager.ts +163 -0
- package/src/scanner/email-checker.ts +106 -0
- package/tsconfig.json +21 -0
package/plans/POLLING.md
ADDED
|
@@ -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)
|