Add native coverage for ticking browser pages
Incremental-game style fixture whose AT-SPI leaves recreate every 250 ms, native navigation test, private-session AT-SPI daemon launch in the harness (no systemd, no host session required), and testing notes.
This commit is contained in:
1 parent
7eef6e8be6
commit
6c10ed0517
5 files changed
+472
-8
No files matched your search
@@ -0,0 +1,186 @@
|
||||
# Ticking-page (incremental game) navigation
|
||||
|
||||
Perpetually updating pages recreate their accessible text leaves on a timer.
|
||||
The maintainer's 2026-11-03 debug log showed the same failure on three
|
||||
incremental games, which made arrow-key reading useless while focus
|
||||
(link/button) navigation still worked:
|
||||
|
||||
- Exotic Matter Dimensions: every Down paid a full recapture and re-spoke
|
||||
the first line (14 repeats of one counter line, `moved=true`), ending in
|
||||
"Could not find current location."
|
||||
- Ordinal Pringles: 16 Downs re-spoke a single-letter fragment, then the
|
||||
focused anchor itself was destroyed and six consecutive moves reported
|
||||
"Could not find current location." until Tab supplied a new anchor.
|
||||
Moving among buttons was 1 ms; crossing the ticking static region cost a
|
||||
20–25 ms recapture per key on a 129-node document.
|
||||
- Prestige Tree: ~20 accessible-name events per second while idle. Speech
|
||||
coalescing already handled these; no change was needed there.
|
||||
|
||||
Root causes were positional, not per-game: the saved browse position was an
|
||||
AT-SPI object identity that Chromium retires on every tick, the
|
||||
generation-checked sibling boundaries retired with it, recovery restarted
|
||||
at the surviving parent's first line, and capture was keyed on the destroyed
|
||||
focus anchor so recapture itself failed with `ErrObjectNotFound` forever.
|
||||
|
||||
## Changes
|
||||
|
||||
- Replacement recovery (`internal/review/document_replacement.go`) now also
|
||||
retains the child's index and the text parent's child count. When every
|
||||
saved sibling reference has retired but the same generation-checked parent
|
||||
still has the same child count, the child at the same index is the same
|
||||
navigation line (`replaced-text-child-index` in diagnostics). A changed
|
||||
count, or any saved sibling that still lives (reorder, single replaced
|
||||
neighbor), stays ambiguous and falls back to the surviving parent, as
|
||||
before. Activation never uses either recovery.
|
||||
- Document capture for moves (`captureDocumentForMove`) falls back to the
|
||||
cursor's saved document when capture keyed on the focus anchor returns
|
||||
`ErrObjectNotFound`, so the existing anchor-recovery path can rebind focus
|
||||
within the same document generation.
|
||||
- Down after structural navigation to a heading no longer descends into the
|
||||
heading's embedded link and repeats its title as a new line
|
||||
(`internal/review/document_caret_line.go`). The structural target's own
|
||||
first text range keeps the previous within-object line logic.
|
||||
- Identical structural moves queued while the worker is busy coalesce into
|
||||
one batch (one sync, capture cycle, placement and result, full step
|
||||
count). Caret steps are excluded: queued character/word/line steps keep
|
||||
their own ordered results.
|
||||
|
||||
## Caret validation on ticking text
|
||||
|
||||
A follow-up log showed residual traps at dense counter lines (Exotic
|
||||
Matter's "Progress to Luck Shards" repeated nine times, three location
|
||||
losses). Attempt errors were all identity/text/children invalidation —
|
||||
no stale-geometry errors — so this was not Xorg/Wayland degradation; the
|
||||
AT-SPI tree mutates identically under either backend. The trap: caret
|
||||
moves validated captured text/geometry freshness for every traversed node
|
||||
before committing, while the move's whole retry budget (one attempt plus
|
||||
two captures) races a game tick that re-invalidates text several times
|
||||
per second. Recovery succeeded on every attempt and still lost.
|
||||
|
||||
Caret validation (`ValidateDocumentCaretRange`) now tolerates text,
|
||||
geometry, and child-order staleness, because caret moves read text and
|
||||
line bounds live and generation-check every endpoint; identity, semantic
|
||||
snapshots (names are spoken from the snapshot), and hypertext
|
||||
embedded-object ranges stay strict. This deliberately changes the older
|
||||
contracts in `TestDocumentCaretDefersUnrelatedUpdatesUntilTraversed` and
|
||||
the children case of `TestDocumentCaretKeepsOffsetAfterDocumentRefresh`:
|
||||
dirty target text and changed child order no longer force a recapture
|
||||
before a caret move commits, and the tests now prove the live provider
|
||||
text is what the move reports. Structural navigation keeps strict
|
||||
validation everywhere. Capture root selection also learned to resolve a
|
||||
focus identity from retained document sources (a document root after a
|
||||
load may have no focus-event history) and to keep scanning focus history
|
||||
past chrome entries when recovering a dead browse focus. Provider-side
|
||||
stalls remain possible: one log ends with two 15 s capture deadlines from
|
||||
an unresponsive game tab, which no validation change can fix.
|
||||
|
||||
## Graph-guided follow-ups (Orca comparison)
|
||||
|
||||
A later comparison against knowledge graphs of Orca and at-spi2-core
|
||||
(verified in source) produced two more changes. First, index-based
|
||||
recovery now also covers recreated non-text children such as button rows
|
||||
(`replaced-child` / `replaced-child-index`), under the same bounded
|
||||
evidence as text leaves and without widening their stricter rules — Orca
|
||||
recovers removed children from parent plus removed index in the same
|
||||
spirit. Second, a bus whose document capture just missed its deadline is
|
||||
marked hung for a two-second cooldown (`ErrProviderHung`, announced as
|
||||
"Application is not responding."), so a frozen tab fails fast instead of
|
||||
stalling every following keypress; Orca likewise remembers hung objects
|
||||
briefly and propagates the mark to the application. The gate covers all
|
||||
document operations (reads, placement, activation, context), not just
|
||||
captures — a live freeze on switching to the Exotic Matter tab showed
|
||||
reads re-entering the stuck provider after the capture deadline — and
|
||||
speculative warm-up captures are capped at three seconds. One in-flight
|
||||
capture can still occupy the provider thread for its full deadline;
|
||||
the gates keep everything after it responsive.
|
||||
|
||||
## Tab/workspace-switch retention
|
||||
|
||||
Returning to a browser tab or workspace used to pay a cold whole-document
|
||||
capture (2.9 s for a 4226-node YouTube page in the 2026-11-03 log) before
|
||||
the first keypress worked. Three causes stacked: load/tab resets bumped
|
||||
the document epoch and discarded the browse cursor, the adapter kept a
|
||||
single document tree slot so any other tab's capture evicted it, and with
|
||||
the root changed `nativeRefresh` had nothing to reuse.
|
||||
|
||||
Now the adapter retains a bounded LRU of per-root document trees, text
|
||||
trees, and native sources (`rotateDocumentSlots`, active plus three
|
||||
stashed), events keep invalidating stashed trees while inactive, and the
|
||||
worker retains a browse cursor per retained tree. A move first trials
|
||||
retained cursors (cheap in-memory syncs) before capturing, so an unchanged
|
||||
document resumes with no capture at all and a changed one recaptures warm
|
||||
(only dirty nodes re-read). Tab switches suspend instead of hard-resetting
|
||||
documents; page loads still hard-reset, and requests carry a load epoch so
|
||||
a cursor captured before a same-path load is never reused afterwards
|
||||
(`TestWorkerDocumentLoadRetiresOutstandingResult` still guards this).
|
||||
Load-complete and tab-select also enqueue a best-effort warm-up capture so
|
||||
even a first visit starts capturing before the first keypress.
|
||||
|
||||
Still deferred: subtree-scoped capture. The first-ever capture of a huge
|
||||
document remains whole-document (now usually hidden by warm-up), and a
|
||||
document that changed a lot while away still re-reads its dirty nodes over
|
||||
a full walk. The validation invariant (dirty traversed objects and
|
||||
ancestors must refresh before a move may skip them) is deliberate and
|
||||
covered by the replacement/generated-link fixtures. Revisit subtree
|
||||
capture only if large ticking documents still feel unresponsive after
|
||||
retention.
|
||||
|
||||
## Automated checks (2026-11-03)
|
||||
|
||||
- New Go regressions, each observed failing before its fix:
|
||||
`TestDocumentCaretRecoversRecreatedTextSiblings`,
|
||||
`TestDocumentCaretRecapturesThroughDocumentWhenFocusAnchorDies`,
|
||||
`TestDocumentCaretContinuesFromStructuralLinkedHeading`,
|
||||
`TestWorkerCoalescesQueuedIdenticalDocumentMoves`,
|
||||
`TestDocumentSlotsRetainTreesAcrossRoots`,
|
||||
`TestWorkerRetainsDocumentCursorsAcrossSwitches`,
|
||||
`TestWorkerWarmDocumentPreparesFirstMove`,
|
||||
`TestValidateDocumentCaretRangeToleratesTextAndGeometryStaleness`, and
|
||||
`TestDocumentCaretMoveCrossesTextInvalidatedLines`.
|
||||
- `go test ./... -count=1` and `go vet ./...` pass. One sandbox-only
|
||||
exception: `internal/speech` unix-socket tests fail inside the restricted
|
||||
sandbox ("socket: operation not permitted") and pass outside it.
|
||||
- `go test -race ./internal/review/ ./internal/atspi/ ./internal/app/
|
||||
-count=1` passes in the private native session.
|
||||
- `BenchmarkDocument*` (200 ms, `-benchmem`) matches the medians in
|
||||
`browser-navigation-performance.md` (heading pair ≈ 0.036 ms, snapshot
|
||||
≈ 5.9 ms, line assembly ≈ 3.5 ms at 10,000 nodes). Informational only.
|
||||
- New native proof `ticking` (`TestNativeBrowserTickingNavigation`, plus
|
||||
`tests/browser-ticking.html` for manual runs) is registered in
|
||||
`tests/native_browser.py` and passes: Down moves visit rebuilt lines
|
||||
1, 2, 3 in order across ticks, then reach the following button. The
|
||||
guarding proofs `linkedheadings`, `dynamic`, `replacement`,
|
||||
`replaced-feed`, `generatedlinks`, and `document` also pass. Getting
|
||||
there required a harness fix first: inside the private D-Bus session,
|
||||
AT-SPI activation delegated to a systemd user unit
|
||||
(`org.freedesktop.systemd1 ... unit failed`), which cannot work with a
|
||||
disposable runtime dir and must not be required on non-systemd systems.
|
||||
`tests/native_browser.py` now launches `at-spi-bus-launcher
|
||||
--launch-immediately` and `at-spi2-registryd` directly inside its private
|
||||
Xvfb/D-Bus session. The host session (compositor, buses, AT-SPI) is never
|
||||
touched. Observed but not addressed: empty fragment stops between the
|
||||
fixture's `<br>`-separated spans, a pre-existing line-assembly quirk.
|
||||
|
||||
## Live acceptance (pending, maintainer)
|
||||
|
||||
Do not replace the running reader without approval; build to a side path
|
||||
(e.g. `/tmp/cthulhu-ticking`) or rebuild through the usual switcher after
|
||||
review. With a fresh debug log:
|
||||
|
||||
1. Exotic Matter Dimensions and Ordinal Pringles: traverse the previously
|
||||
stuck regions by Down/Up only, reach a button, and activate it. Expect
|
||||
progressing distinct lines, no repeated counter line, and no "Could not
|
||||
find current location." Diagnostics should show
|
||||
`replaced-text-child-index` recoveries.
|
||||
2. Prestige Tree: H/Down through the main view; idle name churn stays
|
||||
silent except for the focused control.
|
||||
3. YouTube: H onto a video heading, then one Down. Expect the channel/meta
|
||||
line, not the title again as a link. Mash H three times during a slow
|
||||
load; expect one landing three headings on. Note your heading position,
|
||||
switch to another tab/workspace and back, then press H: expect to
|
||||
continue from the noted heading promptly, with no multi-second stall,
|
||||
instead of restarting from the top after a cold capture.
|
||||
4. Compare log counters against the 2026-11-03 log: duplicate consecutive
|
||||
`document-caret` lines, location losses, recaptures per 100 commands.
|
||||
5. Report audible speech and physical Braille separately from the automated
|
||||
results above.
|
||||
Reference in new issue
Block a user