From 6c10ed05172072180cae6a5506d9425e99c4005f Mon Sep 17 00:00:00 2001 From: Hunter Jozwiak Date: Tue, 3 Nov 2026 10:35:00 -0500 Subject: [PATCH] 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. --- docs/testing/browser-ticking-pages.md | 186 ++++++++++++++++++++ internal/app/browser_ticking_native_test.go | 145 +++++++++++++++ testdata/parity-ledger.csv | 4 +- tests/browser-ticking.html | 64 +++++++ tests/native_browser.py | 81 ++++++++- 5 files changed, 472 insertions(+), 8 deletions(-) create mode 100644 docs/testing/browser-ticking-pages.md create mode 100644 internal/app/browser_ticking_native_test.go create mode 100644 tests/browser-ticking.html diff --git a/docs/testing/browser-ticking-pages.md b/docs/testing/browser-ticking-pages.md new file mode 100644 index 0000000..83ad07e --- /dev/null +++ b/docs/testing/browser-ticking-pages.md @@ -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 `
`-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. diff --git a/internal/app/browser_ticking_native_test.go b/internal/app/browser_ticking_native_test.go new file mode 100644 index 0000000..e6abbbe --- /dev/null +++ b/internal/app/browser_ticking_native_test.go @@ -0,0 +1,145 @@ +// Copyright (c) 2026 Storm Dragon +// Authors: Storm Dragon, Angus Kola +// SPDX-License-Identifier: GPL-3.0-or-later + +package app + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "strings" + "testing" + "time" + + "git.stormux.org/storm/cthulhu/internal/atspi" + "git.stormux.org/storm/cthulhu/internal/review" +) + +// Incremental games rebuild their text leaves on a fast timer. Arrow-key +// reading must keep progressing through the rebuilt lines instead of +// restarting at the first line after every tick, which is how the reported +// Exotic Matter Dimensions and Ordinal Pringles failures presented. +func TestNativeBrowserTickingNavigation(t *testing.T) { + if os.Getenv("CTHULHU_NATIVE_BROWSER") != "1" { + t.Skip("requires private browser session") + } + ctx, cancel := context.WithTimeout(context.Background(), 35*time.Second) + defer cancel() + page := `Ticking navigation regression +

Rebuilt region

+

+

After rebuilt region

+ +

Final paragraph

+` + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, page) + })) + defer server.Close() + adapter := atspi.New() + if err := adapter.Start(ctx); err != nil { + t.Fatal(err) + } + defer adapter.Stop(context.Background()) + command := func(args ...string) { + t.Helper() + if output, err := exec.CommandContext(ctx, "xdotool", args...).CombinedOutput(); err != nil { + t.Fatalf("xdotool: %v: %s", err, output) + } + } + command("key", "--clearmodifiers", "ctrl+l") + command("type", "--clearmodifiers", "--delay", "0", server.URL) + command("key", "Return") + var document atspi.ObjectID + for !document.Valid() { + select { + case event := <-adapter.Events(): + if event.Type == atspi.DocumentLoadCompleteEvent && event.Source.Name == "Ticking navigation regression" { + document = event.Source.ID() + } + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + } + worker, err := review.NewWorker(adapter, 2*time.Second) + if err != nil { + t.Fatal(err) + } + if err := worker.Start(ctx); err != nil { + t.Fatal(err) + } + defer worker.Stop(context.Background()) + worker.SyncFocus(document) + next := func() (string, bool) { + t.Helper() + if err := worker.MoveDocumentCaret(review.CaretNextLine); err != nil { + t.Fatal(err) + } + select { + case result := <-worker.Results(): + got := result.DocumentCaretText + if got == "" { + got = result.DocumentPosition.Snapshot.Name + } + if result.Err != nil { + t.Fatalf("next line on ticking page: %+v", result) + } + t.Logf("line=%q moved=%v elapsed=%s capture=%s reason=%v", got, result.Moved, result.Elapsed, result.CaptureDuration, result.CaptureReason) + return got, result.Moved + case <-ctx.Done(): + t.Fatal(ctx.Err()) + return "", false + } + } + seen := []int{} + previous := "" + for range 14 { + got, moved := next() + if !moved { + break + } + if got == previous && strings.HasPrefix(got, "Rebuilt line") { + t.Fatalf("ticking line repeated instead of progressing: %q", got) + } + previous = got + if line, ok := strings.CutPrefix(got, "Rebuilt line "); ok { + var number int + if _, err := fmt.Sscanf(line, "%d", &number); err == nil { + seen = append(seen, number) + // Let several ticks recreate every leaf before moving on. + time.Sleep(700 * time.Millisecond) + } + continue + } + if strings.Contains(got, "After rebuilt region") { + if fmt.Sprint(seen) != "[1 2 3]" { + t.Fatalf("rebuilt lines visited = %v, want [1 2 3] in order", seen) + } + if final, _ := next(); !strings.Contains(final, "Final action") { + t.Fatalf("line after rebuilt region = %q, want Final action", final) + } + return + } + } + t.Fatalf("never reached text after the rebuilt region; rebuilt lines visited = %v", seen) +} diff --git a/testdata/parity-ledger.csv b/testdata/parity-ledger.csv index 9137460..da1f003 100644 --- a/testdata/parity-ledger.csv +++ b/testdata/parity-ledger.csv @@ -23,7 +23,7 @@ managed-descendant-presentation,src/cthulhu/scripts/default.py,focus,Resolve foc ssip-speak-cancel,src/cthulhu/speechdispatcherfactory.py,speech,Send speech and cancellation through SSIP,Storm Dragon,internal/speech,in-progress,go test ./internal/speech -count=1,Maintainer accepted audible non-consuming Control/Shift interruption in the Stage 1 physical Xorg checklist on 2026-09-11.,,Maintainer heard lifecycle and terminal speech plus Control/Shift interruption during the accepted Stage 1 physical Xorg checklist on 2026-09-11.,,Protocol replies 230 and 225 were confirmed against the local service; broader backend and X11 coverage remains pending. web-structural-navigation,src/cthulhu/scripts/web/script.py,web,Move by headings and other document structure,Storm Dragon,internal/input internal/review internal/app,implemented,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb structural and mode/chrome sequences pass on 2026-09-20.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md.,,Structural navigation follows native caret movement. Echo suppression ends on subsequent input. Failed same-object context capture retries. Delayed document carets cannot override editable focus and stale results cannot overwrite unresolved unrelated focus. All 27 browse symbols pass through after routing is disabled. Automatic structural focus is configurable globally and per application on 2026-10-28; defaults off and preserves sticky modes. Private GTK Apply/reload and Chromium native typing checks pass. Other Python structural settings remain pending. New option live listening and physical Braille acceptance remain pending; see docs/testing/structural-focus.md. Parity review corrections and remaining physical gates: docs/testing/parity-review-follow-up.md. web-document-recapture,src/cthulhu/ax_object.py,web,Recover heading navigation after browser document replacement,Storm Dragon,internal/atspi internal/review internal/app,implemented,go test -race ./... -count=1; python3 tests/native_browser.py,Private Brave/Xvfb passes immediate H after loading a new page and after activating its return link on 2026-09-20.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md.,,Native caches refresh on recapture. Load completion retains the new root and resets browser context and pending reading. Worker page epochs retire queued and in-flight results across reused paths. Native reload and mode/chrome tests remain covered. Installed Go preview and Brave Origin are not verified. On 2026-09-21 semantic invalidation was scoped to captured objects so browser chrome and other applications cannot discard the document. Focused race tests and the full private Brave suite pass; heading/link jumps assert capture reuse. Large-page latency and logged stale-object failures still require physical retesting. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 23 ownership correction rejects loads outside the current application or known window and after owner loss; accepted loads update the focus owner for correct defunct recovery. Five event-loop regressions and app race tests pass; physical acceptance remains pending. September 24 same-geometry window recovery now requires fresh active/showing/non-iconified state before foreground correlation; private recovery regression fails on the earlier preview and passes on the rebuilt one. Address changes outside the owned field are suppressed; popup-associated typing remains supported. September 25 review: recovery now checks the fresh tree when text/geometry recapture discovers a removed focus; deterministic regression and focused race checks pass. -web-caret-line-reading,src/cthulhu/scripts/web/script.py,web,Read displayed document content at the browser caret,Storm Dragon,internal/atspi internal/review internal/app internal/presentation,implemented,"go test -race ./... -count=1; python3 tests/native_browser.py; go test -race ./internal/atspi ./internal/review ./internal/app -count=1; CTHULHU_NATIVE_BROWSER_TESTS=generatedlinks,replacement,tooltips,fragments,namedlinks,linkedheadings python3 tests/native_browser.py",Private Brave/Xvfb H/Down/H and expanded page transition sequences pass on 2026-09-20. On 2026-10-22 the maintainer confirmed reading the entire original Techrights article in Brave without looping after the generated-link recovery fix. See docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md. Original Techrights full-article reading accepted after the generated-link fix on 2026-10-22; see docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,,Captured and assembled document lines use role/state sound categories and speech fallback. Unit tests cover visited links and per-object cue deduplication and sound-only versus speech-only policy. Character and word intents remain separate. Ordinary caret movement updates structural position. Initial read-all is implemented; braille and complete line-presentation parity remain pending. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 24 prerequisite review: pending caret/terminal timers extracted under the same event-loop owner; adjacent-character word navigation and whitespace and embedded-marker traversal corrected; whole caret movement deadline added. Full race and isolated Brave/X11/ownership proofs pass; physical and installed-preview gates remain pending. See docs/testing/chromium-browser.md for dependency order. September 24 log correction: browse word and line speech includes link and heading ancestors with role sound policy; character output retains character granularity. Native Brave assertions cover link words and document-start heading level. See docs/testing/chromium-browser.md. September 25 review: trailing non-text objects no longer cause false stale-location errors for line/document-edge movement; focused regression and private Brave suite pass. Physical listening remains open. Explicitly named links retain whole-label line speech while character and word review use accessible-label offsets independently of native text. Generic unit and private Brave named-link regressions cover empty native text and badges plus both directions across following punctuation. Live speech and physical Braille acceptance remain pending. October 22 generated-link regression verifies exact prefix/body text with forward/reverse lines and Read All in private Brave. The maintainer accepted full-article reading on the original Techrights page after the fix; other live gates remain scoped to their named workflows. See docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22. October 24 audit fixes validate dirty hypertext ancestors before crossing objects; refreshed retries include inserted parent text. Word whitespace uses bounded native character blocks and caret ranges have indexed lookup. Existing native navigation regressions pass; see docs/testing/browser-audit-fixes.md for limitations and remaining work. +web-caret-line-reading,src/cthulhu/scripts/web/script.py,web,Read displayed document content at the browser caret,Storm Dragon,internal/atspi internal/review internal/app internal/presentation,implemented,"go test -race ./... -count=1; python3 tests/native_browser.py; go test -race ./internal/atspi ./internal/review ./internal/app -count=1; CTHULHU_NATIVE_BROWSER_TESTS=generatedlinks,replacement,tooltips,fragments,namedlinks,linkedheadings python3 tests/native_browser.py",Private Brave/Xvfb H/Down/H and expanded page transition sequences pass on 2026-09-20. On 2026-10-22 the maintainer confirmed reading the entire original Techrights article in Brave without looping after the generated-link recovery fix. See docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md. Original Techrights full-article reading accepted after the generated-link fix on 2026-10-22; see docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,,Captured and assembled document lines use role/state sound categories and speech fallback. Unit tests cover visited links and per-object cue deduplication and sound-only versus speech-only policy. Character and word intents remain separate. Ordinary caret movement updates structural position. Initial read-all is implemented; braille and complete line-presentation parity remain pending. September 22 log regressions: native invalid text-end recovery and bounded embedded text expansion; web name-event suppression; background load preserves verified chrome focus. Focused race tests and private Brave proofs pass. See docs/testing/chromium-browser.md for reference behavior and remaining physical gates. September 24 prerequisite review: pending caret/terminal timers extracted under the same event-loop owner; adjacent-character word navigation and whitespace and embedded-marker traversal corrected; whole caret movement deadline added. Full race and isolated Brave/X11/ownership proofs pass; physical and installed-preview gates remain pending. See docs/testing/chromium-browser.md for dependency order. September 24 log correction: browse word and line speech includes link and heading ancestors with role sound policy; character output retains character granularity. Native Brave assertions cover link words and document-start heading level. See docs/testing/chromium-browser.md. September 25 review: trailing non-text objects no longer cause false stale-location errors for line/document-edge movement; focused regression and private Brave suite pass. Physical listening remains open. Explicitly named links retain whole-label line speech while character and word review use accessible-label offsets independently of native text. Generic unit and private Brave named-link regressions cover empty native text and badges plus both directions across following punctuation. Live speech and physical Braille acceptance remain pending. October 22 generated-link regression verifies exact prefix/body text with forward/reverse lines and Read All in private Brave. The maintainer accepted full-article reading on the original Techrights page after the fix; other live gates remain scoped to their named workflows. See docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22. October 24 audit fixes validate dirty hypertext ancestors before crossing objects; refreshed retries include inserted parent text. Word whitespace uses bounded native character blocks and caret ranges have indexed lookup. Existing native navigation regressions pass; see docs/testing/browser-audit-fixes.md for limitations and remaining work. November 2026: Down after a structural heading no longer descends into the heading embedded link and repeats its title as a new line. See docs/testing/browser-ticking-pages.md. config-read-preview,src/cthulhu/backends/toml_backend.py,settings,Read existing Cthulhu TOML and merge the startup profile,Storm Dragon,internal/config,implemented,go test ./internal/config ./cmd/cthulhu -count=1,,,,,Read-only preview bridge available only through explicit --config selection. The Go-native schema and default path are intentionally separate; the one-shot importer remains Phase 8 work. config-speech-preview,src/cthulhu/speechdispatcherfactory.py,speech,Apply supported active-profile speech settings,Storm Dragon,internal/config internal/speech internal/app,implemented,go test ./internal/config ./internal/speech ./internal/presentation ./internal/app -count=1,,,Maintainer confirms speech preferences; previews; Apply and Cancel; saved settings; and interruption-key settings work in live testing on 2026-10-22. See docs/testing/release-acceptance.md.,,Applies enablement description punctuation capitalization and default voice settings. typing-printable-key-echo,src/cthulhu/typing_echo_presenter.py,input,Apply printable typing-echo category settings to terminal and editable text insertions,Storm Dragon,internal/config internal/atspi internal/presentation internal/app,implemented,go test ./internal/config ./internal/atspi ./internal/presentation ./internal/app -count=1,Maintainer reported the configured printable echo settings worked in minimal physical Xorg tests on 2026-09-12; broader application coverage remains pending.,,Maintainer confirms thorough live testing of native application and terminal workflows; typing echo; selection; clipboard feedback; and object navigation on 2026-10-22. These workflows pass. See docs/testing/release-acceptance.md.,,Single-character terminal and editable AT-SPI insertions honor enableKeyEcho enableEchoByCharacter and alphabetic numeric punctuation space and diacritical categories. Multi-character insertions remain presentation events. Preferences offer disabled enabled and edit-fields-only radio choices; edit-fields-only uses native editable metadata and Wine editor focus while preserving configured echo voices. Automated focus and persistence checks pass; live speech and physical Braille acceptance for the new mode remain pending. @@ -62,7 +62,7 @@ steam-notifications,src/cthulhu/scripts/apps/steamwebhelper/script.py;tests/test libreoffice-calc-basic,Orca src/orca/scripts/apps/soffice/script.py; Orca src/orca/ax_utilities_table.py; Orca src/orca/speech_generator.py,libreoffice,Present Calc cells and recover logical focus around editing and sheet changes,Storm Dragon,internal/app internal/atspi internal/presentation,in-progress,GOCACHE=/tmp/cthulhu-go-cache go test -race ./internal/atspi ./internal/app ./internal/presentation -count=1; /usr/bin/python3 tests/native_libreoffice.py,Private real LibreOffice Calc 26.2.6.3 on Xvfb and a private AT-SPI bus validates cell movement blanks F2 typing Escape Enter and sheet changes.,,Recorded speech assertions pass. Maintainer confirms basic cell navigation and editing work in live use on 2026-10-22. Selection and sheet-switching acceptance await real-world testing. See docs/testing/release-acceptance.md.,Recorded value and coordinate frames pass; physical Braille and editor routing acceptance pending.,Current Orca is the primary Office reference. Dedicated application policy follows verified process identity after foreground/security gates; native capture remains bounded. Formula presence and explicit formula/basic Where Am I speech are implemented with bounded uncached cell attributes and recorded real-Calc checks; physical acceptance remains pending. Bounded selection deltas for small ranges and row/column selections plus count summaries for large selections and Select All are implemented. Real Calc checks cover disjoint cells and huge selections; physical speech/input/Braille acceptance remains pending. Dynamic row and column headers now use session choices scoped by provider bus document view window and exact sheet name; native reads are bounded to two cells. Four assignable commands preserve existing shortcuts. Private i3 Calc checks cover heading edits independent axis clearing sheet round trips both workbook-return directions with distinct headings and Find and Replace typing/cancellation. Frame activation and X11 foreground recovery avoid stale descendant searches; physical speech/input/Braille acceptance remains pending. Full-row reading Writer workflows and full topology reporting for large selections remain open. See LIBREOFFICE_SUPPORT.md. steam-shader-progress,Steam shader dialog AT-SPI events,web,Report Steam Vulkan shader percentages through shared progress output,,internal/webscripts internal/app,in-progress,go test ./internal/app ./internal/webscripts ./internal/presentation ./internal/sound -count=1; python3 tests/native_progress.py; CTHULHU_NATIVE_BROWSER_TESTS=progress python3 tests/native_browser.py,Private GTK and Brave HTML/ARIA progress checks passed.,,Tone requests verified; live Steam listening pending.,Unrelated Braille ownership covered by unit tests; physical check pending.,English static shader-status name changes only; verified steamwebhelper identity; see PROGRESS_BARS.md. wayland-shared-keyboard-access,src/cthulhu/input_event_manager.py,input,Monitor keys and grab reader shortcuts through the shared accessibility keyboard interface,Storm Dragon,internal/platform/wayland internal/app internal/sessionbus,in-progress,Private dbus-run-session race tests cover wire protocol runtime dispatch configuration rollback secure transitions and cleanup; go test ./... -count=1; python3 tests/check_speech.py,Existing native_x11.py keyboard harness passed; browser tabs and editing passed; browser modes passed on isolated rerun after one failure whose cause was not established.,Shared D-Bus fixture passed without an X display. On 2026-10-22 the maintainer reports minimal live testing and user reports of poorer behavior than native Xorg. Specific compositor and physical key-suppression acceptance remain unverified. See docs/testing/release-acceptance.md.,Minimal live Wayland testing reported by the maintainer on 2026-10-22; broad listening acceptance is not established.,Pending physical Braille on Wayland,No compositor-specific code. See WAYLAND_SUPPORT.md for supported commands limitations and live acceptance. X11 production listeners are unchanged; the suspension probe now links its existing xkbcommon dependency. -web-browse-recovery-activation,Orca src/orca/scripts/web/script_utilities.py; master:src/cthulhu/scripts/web/script.py,web,Recover dynamic browse positions and activate custom clickables silently,,internal/atspi internal/review internal/app,implemented,"go test -race ./... -count=1; python3 tests/native_browser.py; python3 tests/native_browser_navigation.py; python3 tests/native_firefox.py; go test -race ./internal/atspi ./internal/review ./internal/app -count=1; CTHULHU_NATIVE_BROWSER_TESTS=generatedlinks,replacement,tooltips,fragments,namedlinks,linkedheadings python3 tests/native_browser.py",Private Chromium and Firefox fixtures; real-reader changing-page and activation checks use private Xvfb and D-Bus. On 2026-10-22 the maintainer confirmed reading the entire original Techrights article in Brave without looping after the generated-link recovery fix. See docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md. Original Techrights full-article reading accepted after the generated-link fix on 2026-10-22; see docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,Selection-collapse regression and native mode frames covered; physical routing remains pending.,See docs/testing/browser-release-review.md for findings and live acceptance. No full Orca parity claim. October 22 generated-link recovery retains the same parent and adjacent sibling identities; refreshes navigation-defunct child order and disappearing native text; and gives Read All navigation recovery with document-generation rejection. The page-CSS fixture reproduces the first fix failure and passes after correction. Full-article reading on the original Brave page is accepted; separate manual reverse-navigation Read All activation and physical Braille checks were not reported. The separate dynamic-page flat-review failure remains open. +web-browse-recovery-activation,Orca src/orca/scripts/web/script_utilities.py; master:src/cthulhu/scripts/web/script.py,web,Recover dynamic browse positions and activate custom clickables silently,,internal/atspi internal/review internal/app,implemented,"go test -race ./... -count=1; python3 tests/native_browser.py; python3 tests/native_browser_navigation.py; python3 tests/native_firefox.py; go test -race ./internal/atspi ./internal/review ./internal/app -count=1; CTHULHU_NATIVE_BROWSER_TESTS=generatedlinks,replacement,tooltips,fragments,namedlinks,linkedheadings python3 tests/native_browser.py",Private Chromium and Firefox fixtures; real-reader changing-page and activation checks use private Xvfb and D-Bus. On 2026-10-22 the maintainer confirmed reading the entire original Techrights article in Brave without looping after the generated-link recovery fix. See docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md. Original Techrights full-article reading accepted after the generated-link fix on 2026-10-22; see docs/testing/browser-release-review.md#generated-link-recovery-acceptance-2026-10-22.,Selection-collapse regression and native mode frames covered; physical routing remains pending.,See docs/testing/browser-release-review.md for findings and live acceptance. No full Orca parity claim. October 22 generated-link recovery retains the same parent and adjacent sibling identities; refreshes navigation-defunct child order and disappearing native text; and gives Read All navigation recovery with document-generation rejection. The page-CSS fixture reproduces the first fix failure and passes after correction. Full-article reading on the original Brave page is accepted; separate manual reverse-navigation Read All activation and physical Braille checks were not reported. The separate dynamic-page flat-review failure remains open. November 2026 ticking-page recovery: when every text sibling is recreated at once on incremental-game pages; navigation re-resolves the same line by child index and count under the surviving generation-checked parent; capture falls back to the saved document when the focus anchor itself was destroyed. See docs/testing/browser-ticking-pages.md; live game acceptance pending. web-structural-lists,Orca src/orca/structural_navigator.py; src/orca/structural_navigator_command_definitions.py,web,List browser elements and move to a selected item,,internal/input internal/review internal/interaction internal/app,implemented,"go test -race ./... -count=1; CTHULHU_NATIVE_BROWSER_TESTS=lists,keys,document,modes,editing python3 tests/native_browser.py; python3 tests/native_internal_ui.py",Private Brave/Xvfb list workflow covers 302 links hidden content current row selection Escape clickables heading level and empty list.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md.,Shared internal UI frames covered; physical Braille acceptance pending.,25 browse shortcuts use Alt+Shift. All Orca list types are registered including unbound annotations internal frames and math. Exact targets and page ownership are validated before selection. See docs/testing/browser-lists.md for manual review and regression checks. mozilla-leaf-text-reading,Orca src/orca/scripts/web/script_utilities.py,web,Read native heading and link text leaves in Firefox and Thunderbird,,internal/atspi tests/native_mozilla.py,implemented,python3 tests/native_firefox.py; CTHULHU_BINARY=/tmp/cthulhu-mozilla-review /usr/bin/python3 tests/native_mozilla.py; go test -race ./internal/atspi ./internal/review ./internal/presentation ./internal/app -count=1 -timeout=120s,Private Firefox 158.0 and Thunderbird 158.0 sessions each cover 17 navigation commands and Read All with zero native caret synchronization errors.,,Maintainer confirms extensive live browser testing passes in Firefox and Chromium-based browsers on 2026-10-22; element lists and jumping to selected items are explicitly confirmed. See docs/testing/release-acceptance.md.,Shared Chromium selection and routing checks passed; physical Mozilla Braille acceptance pending.,Shared document text metrics now include heading and link roles. Native Firefox metadata assertion failed before the fix. See docs/testing/mozilla-reading.md. Thunderbird folders message lists and composition remain outside this reading baseline. thunderbird-local-mail-composition,Orca src/orca/scripts/apps/Thunderbird/script.py;src/orca/script_utilities.py,application,Navigate local folders and message tabs; select messages; compose replies and forwards; save and add attachments,,internal/atspi internal/app tests/native_thunderbird.py,implemented,go test -race ./internal/atspi ./internal/app ./internal/review ./internal/presentation -count=1 -timeout=120s; CTHULHU_BINARY=/tmp/cthulhu-mozilla-review /usr/bin/python3 tests/native_thunderbird.py,Private Thunderbird 158.0 HTML and plain-text local mailbox checks cover message-tab return; Subject and body editing; reply and reply-all recipients with Cc; inline forwarding; exact selected rows after Shift and Ctrl navigation; received attachment saving with content verification; and adding a local file to an unsent draft.,,SSIP verifies focus handoff and row roles; selection toggling; Subject and body echo; reply and forward editing; attachment menus and picker return. Maintainer relays third-party confirmation on 2026-10-22 that message reading; replies; forwards; and attachments work well in live testing. See docs/testing/release-acceptance.md.,Event-loop regression checks paragraph cursor and routing metadata; physical display acceptance pending.,Foreground replacement requires a registered application and fresh complete window membership. Editable paragraph events belong only to their focused editable document. Title echoes require owned editable focus and recent editing plus unchanged title prefix/suffix; unrelated status titles still announce. See docs/testing/thunderbird-workflows.md; no sending remote mailbox or page-load-option acceptance claimed. diff --git a/tests/browser-ticking.html b/tests/browser-ticking.html new file mode 100644 index 0000000..785133c --- /dev/null +++ b/tests/browser-ticking.html @@ -0,0 +1,64 @@ + +Cthulhu Browser Probe + + +

Rebuilt text region

+

+

Rebuilt container region

+
+

Renamed controls region

+
+ + +
+

Final region

+

Text after ticking regions

+ +

Final paragraph

+ + diff --git a/tests/native_browser.py b/tests/native_browser.py index 6e752ed..f006040 100644 --- a/tests/native_browser.py +++ b/tests/native_browser.py @@ -37,6 +37,7 @@ BROWSER_PROOFS = { "lines": ("./internal/atspi", "^TestNativeBrowserLogicalLines$", 25, 30, "browser-lines.html"), "tables": ("./internal/app", "^TestNativeBrowserTables$", 35, 40), "dynamic": ("./internal/app", "^TestNativeBrowserDynamicNavigation$", 40, 45), + "ticking": ("./internal/app", "^TestNativeBrowserTickingNavigation$", 40, 45), "browser": ("./internal/atspi", "^TestNativeBrowser$", 45, 50), "live": ("./internal/atspi", "^TestNativeBrowserLiveRegion$", 30, 35), "document": ("./internal/app", "^TestNativeBrowserDocumentCommands$", 45, 50), @@ -58,17 +59,85 @@ def selected_proofs(): return selected +def start_atspi_bus(): + # D-Bus activation of org.a11y.Bus and of the AT-SPI registry may + # delegate to systemd user units, which cannot work in this private + # session (disposable runtime dir, no user manager tied to the private + # bus) and must not be required by the test suite on non-systemd systems + # either. Launch both daemons directly; later activation requests then + # find the names already owned. + def locate(name, candidates): + found = shutil.which(name) + if found: + return found + for candidate in candidates: + if os.access(candidate, os.X_OK): + return candidate + raise RuntimeError(name + " not found") + + launcher = locate("at-spi-bus-launcher", + ("/usr/lib/at-spi-bus-launcher", "/usr/libexec/at-spi-bus-launcher")) + process = subprocess.Popen([launcher, "--launch-immediately"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + address = None + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError("at-spi-bus-launcher exited before the probe") + result = subprocess.run( + ["gdbus", "call", "--session", "--dest", "org.a11y.Bus", + "--object-path", "/org/a11y/bus", "--method", "org.a11y.Bus.GetAddress"], + capture_output=True, text=True) + if result.returncode == 0 and "unix:" in result.stdout: + address = result.stdout.split("'", 2)[1] + break + time.sleep(0.1) + if address is None: + process.terminate() + raise RuntimeError("AT-SPI bus did not become ready") + registry = locate("at-spi2-registryd", + ("/usr/lib/at-spi2-registryd", "/usr/libexec/at-spi2-registryd")) + registryd = subprocess.Popen([registry], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + if registryd.poll() is not None: + process.terminate() + raise RuntimeError("at-spi2-registryd exited before the probe") + result = subprocess.run( + ["gdbus", "call", "--address", address, "--dest", "org.a11y.atspi.Registry", + "--object-path", "/org/a11y/atspi/registry", + "--method", "org.freedesktop.DBus.Peer.Ping"], + capture_output=True, text=True) + if result.returncode == 0: + return [process, registryd] + time.sleep(0.1) + registryd.terminate() + process.terminate() + raise RuntimeError("AT-SPI registry did not become ready") + + def run_inside(): browser = os.environ.get("CTHULHU_BROWSER") or shutil.which("brave") if not browser: raise RuntimeError("Set CTHULHU_BROWSER to a Chromium-family executable") selected = selected_proofs() - for name, proof in BROWSER_PROOFS.items(): - if name in selected: - print(f"Running browser proof: {name}", flush=True) - # F7 toggles a profile-wide setting. Each proof must start from - # the same browser state whether run alone or after editing. - run_browser_proof(browser, proof) + atspi = start_atspi_bus() + try: + for name, proof in BROWSER_PROOFS.items(): + if name in selected: + print(f"Running browser proof: {name}", flush=True) + # F7 toggles a profile-wide setting. Each proof must start from + # the same browser state whether run alone or after editing. + run_browser_proof(browser, proof) + finally: + for process in reversed(atspi): + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + process.wait(timeout=5) def run_browser_proof(browser, proof):