diff options
| author | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-05-13 01:05:46 -0400 |
|---|---|---|
| committer | Tianren Dong <tianrendong.dtr@gmail.com> | 2026-05-13 01:05:46 -0400 |
| commit | 33110e803fcacc31dc2859dbc0d470b008556798 (patch) | |
| tree | bb30dad365139c558f9d9651cb8bca375320cb8e /test-suite/README.md | |
| parent | d78081aa58ecaa1c72e847c2d095b5f07c391506 (diff) | |
pi-chrome: humanize synthetic input + anti-automation test suite
Bump 0.8.0 → 0.9.1.
Companion extension (service_worker.js):
- humanise pointer paths: easing Bezier-ish interpolation w/ wobble, 12–42
steps, 4–18 ms gaps; pointer position persisted across calls
- click point picked inside element rect, not dead center
- pointer events fill movementX/Y, screenX/Y, pressure (0.5 on down),
pointerId, width/height, tilt, composed
- click sequence: humanMoveTo → pointerdown → focus → 45–140 ms hold →
pointerup → mouseup → click
- per-character typing: keydown → keypress → beforeinput → value mutate
(or execCommand for contenteditable) → input → keyup, with 25–95 ms
hold + 35–140 ms inter-key; auto Shift chord for uppercase/symbols
- pressKeyInPage emits same cadence + handles printable insert / Backspace
- hoverPage: humanised path + 80–220 ms dwell
- dragPage: full HTML5 sequence (dragstart/drag/dragenter/dragover/
dragleave/drop/dragend) with shared DataTransfer + pointer continuity
- new scrollPage: wheel events with front-loaded momentum-shaped deltas
- self-reload on version drift: SW reads x-pi-chrome-version header,
reloads when older than pi-chrome
Pi side (index.ts):
- new chrome_scroll tool
- PI_CHROME_VERSION read from package.json (and re-read on every /next
so future bumps take effect without pi restart)
- /chrome-doctor escalates version mismatch to top-line ✗ error and
skips downstream capability probes when stale
- /next response carries x-pi-chrome-version header for SW self-heal
Test suite (test-suite/):
- 28 challenge pages each catching a different synthetic-event tell:
isTrusted, webdriver/fingerprint, mouse entropy/timing/coords,
pointer pressure/continuity, per-char cadence + framework invariants,
user activation, honeypots, focus order, wheel scroll, drag+
DataTransfer, contenteditable+Selection, paste/clipboard, native
select, hover dwell, React valueTracker, keyboard modifiers, touch
events, stack-trace fingerprint, viewport-edge clicks, mousemove
rate, scroll momentum, IntersectionObserver visibility
- shared _lib.js exposes window.__verdict + writes localStorage so
index.html dashboard aggregates results
- notes/bypass-ideas.md documents fixes still needed per challenge
Diffstat (limited to 'test-suite/README.md')
| -rw-r--r-- | test-suite/README.md | 74 |
1 files changed, 74 insertions, 0 deletions
diff --git a/test-suite/README.md b/test-suite/README.md new file mode 100644 index 0000000..241976c --- /dev/null +++ b/test-suite/README.md @@ -0,0 +1,74 @@ +# pi-chrome anti-automation test suite + +Static pages that each block a different automated-interaction signal. Drive them +with the `chrome_*` tools to discover where the companion-extension's synthetic +events still look like a bot, and to verify any humanization fixes. + +## Run + +```bash +cd test-suite +python3 -m http.server 8765 +# open http://127.0.0.1:8765/ in the Chrome window pi-chrome controls +``` + +Each challenge page exposes: + +- `window.__challenge` — short id +- `window.__verdict` — `"PENDING" | "PASS" | "FAIL"` +- `window.__reason` — array of strings, why it failed (or how it passed) +- `window.__events` — raw event log for forensics + +`PASS` means the page believes a real human did the thing. +`FAIL` means the page caught the automation. + +## Aggregate runner + +`index.html` lists every challenge, loads each one in an iframe, lets you (or +the agent) drive the interaction, and tallies verdicts. + +Recommended agent flow: + +1. `chrome_navigate` to `http://127.0.0.1:8765/`. +2. For each challenge link, open it, run the listed interaction with the + `chrome_*` tools, then read `window.__verdict` / `window.__reason` via + `chrome_evaluate`. +3. Compare against the manual baseline (do the same interaction by hand and + confirm `PASS`). + +## Challenges + +| id | file | what it blocks | +|----|------|----------------| +| `is-trusted-click` | `challenges/01-is-trusted-click.html` | click handler ignores `event.isTrusted === false` | +| `is-trusted-keyboard` | `challenges/02-is-trusted-keyboard.html` | input rejects if `keydown.isTrusted` false | +| `webdriver-flag` | `challenges/03-webdriver-flag.html` | `navigator.webdriver` truthy / Chrome runtime quirks | +| `mouse-entropy` | `challenges/04-mouse-entropy.html` | requires organic mousemove path before click | +| `event-timing` | `challenges/05-event-timing.html` | rejects when pointerdown→pointerup gap < N ms or always-equal | +| `click-coordinates` | `challenges/06-click-coordinates.html` | rejects clicks that land on exact element center | +| `pointer-properties` | `challenges/07-pointer-properties.html` | demands pressure / non-zero movementX/Y / pointerId variety | +| `keyboard-cadence` | `challenges/08-keyboard-cadence.html` | rejects same-tick `keydown`/`keyup` and missing per-char events | +| `composition-input` | `challenges/09-composition-input.html` | listens for native `keydown` + `keypress` + `input` sequence | +| `user-activation` | `challenges/10-user-activation.html` | feature gates: clipboard.writeText, fullscreen | +| `honeypot` | `challenges/11-honeypot.html` | hidden field must stay empty | +| `fingerprint` | `challenges/12-fingerprint.html` | UA-CH, languages, plugins, permissions consistency | +| `focus-order` | `challenges/13-focus-order.html` | requires `pointerdown`-driven focus, not direct `.focus()` | +| `wheel-scroll` | `challenges/14-wheel-scroll.html` | rejects scrollTop jumps without `wheel`/`scroll` events | +| `drag-drop-datatransfer` | `challenges/15-drag-drop-datatransfer.html` | requires full HTML5 drag cycle with populated `DataTransfer` | +| `contenteditable-selection` | `challenges/16-contenteditable-selection.html` | requires `selectionchange` + monotonic caret advance per keystroke | +| `paste-clipboard` | `challenges/17-paste-clipboard.html` | demands real OS paste with `clipboardData` + `inputType=insertFromPaste` | +| `native-select` | `challenges/18-native-select.html` | rejects `<option>` clicks / `.value=` assignment; needs trusted picker change | +| `hover-dwell` | `challenges/19-hover-dwell.html` | reveal gated on ≥600ms hover with pointermove activity, latency after reveal | +| `react-value-tracker` | `challenges/20-react-value-tracker.html` | input must mutate via native `value` setter so React's `_valueTracker` is stale | +| `keyboard-modifiers` | `challenges/21-keyboard-modifiers.html` | Shift+a chord: modifier ordering, `shiftKey`, `code=KeyA`, `getModifierState` | +| `touch-events` | `challenges/22-touch-events.html` | real `TouchEvent`+`Touch` (radius, force) — synthetic pointerType=touch fails | +| `stack-trace-fingerprint` | `challenges/23-stack-trace-fingerprint.html` | inspects handler call stack for extension/eval/Runtime.evaluate frames | +| `viewport-edge-clicks` | `challenges/24-viewport-edge-clicks.html` | rejects pointer events with clientX/Y outside viewport or (0,0) default | +| `pointer-continuity` | `challenges/25-pointer-continuity.html` | two clicks far apart need bridging pointermoves with mid-span samples | +| `mousemove-rate` | `challenges/26-mousemove-rate.html` | mousemove Δt distribution must match ~60–250Hz with jitter | +| `scroll-momentum` | `challenges/27-scroll-momentum.html` | wheel ΔY trace must show peak + decaying momentum tail ≥100ms | +| `intersection-visibility` | `challenges/28-intersection-visibility.html` | IntersectionObserver gradient + rAF scroll frames — no teleport | + +## What each `PASS` would require in `service_worker.js` + +See `notes/bypass-ideas.md`. |
