@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Smarter Scanner Navigation Design
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
|
|
5
|
+
The current mouse scanner has several navigation shortcomings:
|
|
6
|
+
|
|
7
|
+
1. **No state restoration for non-navigate actions.** If the scanner needs to execute a mouseover/click on a page it's not currently on, it proceeds anyway and fails silently.
|
|
8
|
+
2. **Navigation actions are only seeded once** (to baseUrl at startup). New pages discovered via clicks don't get their own navigation actions, so they can't be revisited.
|
|
9
|
+
3. **Navigate always goes to baseUrl.** There's no concept of navigating to a specific page — all navigate actions go to the same URL.
|
|
10
|
+
4. **Generic selectors** cause duplicate work — but that's a separate extractor issue, not addressed here.
|
|
11
|
+
|
|
12
|
+
## Design
|
|
13
|
+
|
|
14
|
+
### Navigator Helper (`src/scanner/navigator.ts`)
|
|
15
|
+
|
|
16
|
+
A new `Navigator` class that encapsulates all page-routing logic:
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
class Navigator {
|
|
20
|
+
private pagesWithNavAction: Set<number>; // local dedup tracker
|
|
21
|
+
private stateManager: StateManager;
|
|
22
|
+
private api: ApiClient;
|
|
23
|
+
private appId: number;
|
|
24
|
+
private runId: number;
|
|
25
|
+
private sizeClass: string;
|
|
26
|
+
|
|
27
|
+
async ensureOnPage(page: Page, startingPageStateId: number | null): Promise<void>
|
|
28
|
+
async ensureNavigationActionExists(pageId: number, url: string): Promise<void>
|
|
29
|
+
private async resolvePageUrl(pageStateId: number): Promise<{ pageId: number; url: string }>
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### `ensureOnPage(page, startingPageStateId)`
|
|
34
|
+
|
|
35
|
+
Called before executing any action:
|
|
36
|
+
|
|
37
|
+
1. If `startingPageStateId` is null/undefined — no-op (navigate actions don't require a starting page).
|
|
38
|
+
2. If `stateManager.currentPageStateId === startingPageStateId` — no-op (already there).
|
|
39
|
+
3. Otherwise:
|
|
40
|
+
- Resolve URL via API: get page state → get page → get URL.
|
|
41
|
+
- Call `page.goto(url, { waitUntil: "networkidle0", timeout: 30_000 })`.
|
|
42
|
+
- Update `stateManager` with the new state.
|
|
43
|
+
- Log the navigation at `info` level.
|
|
44
|
+
|
|
45
|
+
#### `ensureNavigationActionExists(pageId, url)`
|
|
46
|
+
|
|
47
|
+
Called whenever an action lands on a new page (different URL, same origin):
|
|
48
|
+
|
|
49
|
+
1. Check `pagesWithNavAction.has(pageId)`.
|
|
50
|
+
2. If already tracked, return.
|
|
51
|
+
3. Create a navigate action: `api.createAction({ runId, type: "navigate", targetPageId: pageId, sizeClass })`.
|
|
52
|
+
4. Add pageId to the set.
|
|
53
|
+
|
|
54
|
+
Deduplication is local (in-memory `Set<number>`). Since only one scanner instance processes a given run, this is safe.
|
|
55
|
+
|
|
56
|
+
### Action Loop Changes (`src/scanner/mouse-scanner.ts`)
|
|
57
|
+
|
|
58
|
+
**Before each action:**
|
|
59
|
+
Replace the current state-mismatch block (lines 218-229) with:
|
|
60
|
+
```typescript
|
|
61
|
+
await navigator.ensureOnPage(page, nextAction.startingPageStateId);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Navigate action execution:**
|
|
65
|
+
Currently always goes to `baseUrl`. Change to:
|
|
66
|
+
- If `nextAction.targetPageId` is set, look up the page URL and navigate there.
|
|
67
|
+
- If not set (the initial seed action), navigate to `baseUrl`.
|
|
68
|
+
|
|
69
|
+
**After any action that changes the page URL:**
|
|
70
|
+
```typescript
|
|
71
|
+
if (result.isNew) {
|
|
72
|
+
await navigator.ensureNavigationActionExists(result.pageId, afterUrl);
|
|
73
|
+
await createMouseoverActions(result.pageStateId); // continue scanning inline
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This applies to both click actions (when a click navigates to a new page) and navigate actions (when we land on a page for the first time).
|
|
78
|
+
|
|
79
|
+
### API Client Addition
|
|
80
|
+
|
|
81
|
+
Add one method to resolve a page state's URL:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// Already available via existing endpoints:
|
|
85
|
+
// 1. getPageState(id) → { pageId } — needs new endpoint or use findMatchingPageState
|
|
86
|
+
// 2. getPage(id) → { url }
|
|
87
|
+
|
|
88
|
+
// Simplest: add a GET /scanner/page-states/:id endpoint to the API
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The navigator needs to go from `startingPageStateId → pageId → url`. The page lookup (`getPage(id)`) doesn't exist yet in the scanner API. Need to add:
|
|
92
|
+
- `GET /scanner/pages/:id` — get a single page by ID (returns `PageResponse`)
|
|
93
|
+
|
|
94
|
+
### Schema/Type Changes
|
|
95
|
+
|
|
96
|
+
None. The existing `CreateActionRequest.targetPageId` field is already available but unused for navigate actions. We'll start setting it.
|
|
97
|
+
|
|
98
|
+
### Session Continuity
|
|
99
|
+
|
|
100
|
+
No changes needed. The scanner already uses a single `page` object throughout the scan. Navigating via `page.goto(url)` preserves cookies and session state.
|
|
101
|
+
|
|
102
|
+
## Files to Modify
|
|
103
|
+
|
|
104
|
+
| File | Change |
|
|
105
|
+
|------|--------|
|
|
106
|
+
| `scanner/src/scanner/navigator.ts` | New file — Navigator class |
|
|
107
|
+
| `scanner/src/scanner/mouse-scanner.ts` | Replace state-mismatch block with Navigator, update navigate action to use targetPageId URL, call ensureNavigationActionExists after discovering new pages |
|
|
108
|
+
| `scanner/src/scanner/state-manager.ts` | No changes (Navigator wraps it) |
|
|
109
|
+
| `scanner/src/api/client.ts` | Add `getPage(id)` method |
|
|
110
|
+
| `api/src/routes/scanner.ts` | Add `GET /scanner/pages/:id` endpoint |
|
|
111
|
+
| `types/src/index.ts` | No changes (PageResponse already exists) |
|
|
112
|
+
|
|
113
|
+
## Verification
|
|
114
|
+
|
|
115
|
+
1. Start a scan against a multi-page site.
|
|
116
|
+
2. Verify logs show:
|
|
117
|
+
- Navigation actions automatically created when clicks discover new pages.
|
|
118
|
+
- `ensureOnPage` navigating to correct URLs before executing actions on other pages.
|
|
119
|
+
- No duplicate navigation actions for the same page.
|
|
120
|
+
3. Verify the scanner explores all same-origin pages it discovers (not just the landing page).
|
|
121
|
+
4. Verify session/cookies persist across navigations (no re-login prompts on protected pages).
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
import typescript from '@typescript-eslint/eslint-plugin';
|
|
3
|
+
import typescriptParser from '@typescript-eslint/parser';
|
|
4
|
+
import prettier from 'eslint-plugin-prettier';
|
|
5
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
js.configs.recommended,
|
|
9
|
+
prettierConfig,
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
languageOptions: {
|
|
13
|
+
parser: typescriptParser,
|
|
14
|
+
parserOptions: {
|
|
15
|
+
ecmaVersion: 'latest',
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
},
|
|
18
|
+
globals: {
|
|
19
|
+
console: 'readonly',
|
|
20
|
+
process: 'readonly',
|
|
21
|
+
Buffer: 'readonly',
|
|
22
|
+
__dirname: 'readonly',
|
|
23
|
+
__filename: 'readonly',
|
|
24
|
+
exports: 'writable',
|
|
25
|
+
global: 'readonly',
|
|
26
|
+
module: 'readonly',
|
|
27
|
+
require: 'readonly',
|
|
28
|
+
setTimeout: 'readonly',
|
|
29
|
+
setInterval: 'readonly',
|
|
30
|
+
clearTimeout: 'readonly',
|
|
31
|
+
clearInterval: 'readonly',
|
|
32
|
+
NodeJS: 'readonly',
|
|
33
|
+
fetch: 'readonly',
|
|
34
|
+
Response: 'readonly',
|
|
35
|
+
Request: 'readonly',
|
|
36
|
+
AbortController: 'readonly',
|
|
37
|
+
URL: 'readonly',
|
|
38
|
+
URLSearchParams: 'readonly',
|
|
39
|
+
Headers: 'readonly',
|
|
40
|
+
Bun: 'readonly',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
plugins: {
|
|
44
|
+
'@typescript-eslint': typescript,
|
|
45
|
+
prettier: prettier,
|
|
46
|
+
},
|
|
47
|
+
rules: {
|
|
48
|
+
'no-unused-vars': 'off',
|
|
49
|
+
'@typescript-eslint/no-unused-vars': [
|
|
50
|
+
'error',
|
|
51
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
|
52
|
+
],
|
|
53
|
+
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
|
54
|
+
'prefer-const': 'error',
|
|
55
|
+
'no-constant-binary-expression': 'off',
|
|
56
|
+
'no-undef': 'off',
|
|
57
|
+
'no-empty': ['error', { allowEmptyCatch: true }],
|
|
58
|
+
'prettier/prettier': 'error',
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
files: ['**/__tests__/**/*', '**/*.test.*', '**/*.spec.*'],
|
|
63
|
+
languageOptions: {
|
|
64
|
+
globals: {
|
|
65
|
+
describe: 'readonly',
|
|
66
|
+
it: 'readonly',
|
|
67
|
+
expect: 'readonly',
|
|
68
|
+
beforeEach: 'readonly',
|
|
69
|
+
afterEach: 'readonly',
|
|
70
|
+
vi: 'readonly',
|
|
71
|
+
test: 'readonly',
|
|
72
|
+
beforeAll: 'readonly',
|
|
73
|
+
afterAll: 'readonly',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
ignores: ['dist/**', '**/*.example.*', 'node_modules/**', 'vendor/**'],
|
|
79
|
+
},
|
|
80
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sudobility/testomniac_runner",
|
|
3
|
+
"version": "0.0.128",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"description": "Testomniac scanner worker service",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun --watch src/index.ts",
|
|
11
|
+
"build": "bun build src/index.ts --outdir dist --target bun",
|
|
12
|
+
"start": "bun dist/index.js",
|
|
13
|
+
"test": "bun test",
|
|
14
|
+
"test:unit": "bun test --test-name-pattern '^(?!scanner service)' ",
|
|
15
|
+
"test:watch": "bun test --watch",
|
|
16
|
+
"typecheck": "bunx tsc --noEmit",
|
|
17
|
+
"lint": "bunx eslint src/",
|
|
18
|
+
"lint:fix": "bunx eslint src/ --fix",
|
|
19
|
+
"format": "bunx prettier --write \"src/**/*.ts\"",
|
|
20
|
+
"format:check": "bunx prettier --check \"src/**/*.ts\"",
|
|
21
|
+
"verify": "bun run typecheck && bun run lint && bun run test && bun run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@noble/curves": "^1.0.0",
|
|
25
|
+
"@noble/hashes": "^1.0.0",
|
|
26
|
+
"@sudobility/signic_sdk": "^0.1.7",
|
|
27
|
+
"@sudobility/testomniac_runner_service": "^0.1.127",
|
|
28
|
+
"@sudobility/testomniac_types": "^0.0.66",
|
|
29
|
+
"hono": "^4.7.0",
|
|
30
|
+
"jose": "^6.1.2",
|
|
31
|
+
"openai": "^6.7.0",
|
|
32
|
+
"pino": "^10.1.0",
|
|
33
|
+
"postmark": "^4.0.7",
|
|
34
|
+
"puppeteer-core": "^24.31.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "^9.38.0",
|
|
38
|
+
"@types/node": "^24.10.1",
|
|
39
|
+
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
|
40
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
41
|
+
"eslint": "^9.39.2",
|
|
42
|
+
"eslint-config-prettier": "^10.1.8",
|
|
43
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
44
|
+
"globals": "^16.4.0",
|
|
45
|
+
"prettier": "^3.7.4",
|
|
46
|
+
"tsx": "^4.21.0",
|
|
47
|
+
"typescript": "~5.9.3",
|
|
48
|
+
"vitest": "^4.0.15"
|
|
49
|
+
},
|
|
50
|
+
"license": "BUSL-1.1",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git@github.com:johnqh/testomniac_runner.git"
|
|
54
|
+
}
|
|
55
|
+
}
|