From 89686fa0297c69ec077e398d9f9682b7130998b1 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Tue, 3 Nov 2026 02:37:07 -0500 Subject: [PATCH] Refresh browser tests and recover Firefox tab announcements Bring the fragmented-line browser fixture into agreement with current browse navigation. Rich editors are read through their text descendants, so Down from the preceding heading must read "Review the code." directly. Remove the stale empty editor-control stop while retaining forward and reverse line navigation, character and word movement, Read All, and inline Braille-routing assertions. Fix Firefox's missing announcement when creating a second tab. Firefox can emit selection events before the new tab enters its accessible child list, causing the native selected-child lookup to return nothing. Use the subsequent child-added event only when its target is a live, freshly selected browser-chrome tab with the emitting tab list as its direct parent. Keep document-authored tabs and unselected background additions excluded, retain foreground filtering and tab-identity deduplication, and release native references on the GLib owner. Normalize child-added origin suffixes such as :system before event dispatch, preserving the original native type in diagnostics. Rename the shared mutation normalizer to reflect its expanded scope and update its callers, including the Pidgin insertion check. Extend the normalization regression to cover child additions and reject unrelated event names. Firefox keeps its normal animations and all existing tab-announcement and address-editing assertions. Make private-session teardown tolerate the document portal's asynchronous FUSE unmount after D-Bus exits. Retry only ENOTCONN and EBUSY cleanup failures for at most five seconds. Keep unrelated errors and persistent mount failures visible. Add regressions for transient teardown, preservation of a failing child's exit status, unrelated cleanup errors, and the cleanup deadline. Runtime directories and accessibility buses remain private, with no init-system dependency. Update browser verification notes and the parity ledger with the fixes, evidence, regression risks, and concrete live acceptance procedures. Validation: - All 33 Chromium browser proofs passed in the final combined run. - Firefox baseline and tab checks passed repeatedly with normal animations. - All eight private-session isolation and cleanup regressions passed. - AT-SPI, app, and review package race suites passed during development; focused race regressions passed again after the final event-normalization change. - Go vet passed for internal/atspi, internal/app, and internal/review. - A separate reader binary built successfully. - Parity-ledger validation passed for 75 rows; git diff --check passed. --- docs/testing/browser-audit-fixes.md | 54 +++++++++++++++++- .../atspi/adapter_event_callback_native.go | 15 +++-- internal/atspi/adapter_event_target_native.go | 53 +++++++++++++++++- internal/atspi/model.go | 8 +-- internal/atspi/model_test.go | 10 +++- internal/atspi/pidgin_native.go | 2 +- .../document_visual_line_native_test.go | 2 +- internal/version/version.go | 2 +- testdata/parity-ledger.csv | 2 +- tests/native_test_session.py | 20 ++++++- tests/test_native_test_session.py | 55 +++++++++++++++++++ 11 files changed, 199 insertions(+), 24 deletions(-) diff --git a/docs/testing/browser-audit-fixes.md b/docs/testing/browser-audit-fixes.md index a697a74..fea2920 100644 --- a/docs/testing/browser-audit-fixes.md +++ b/docs/testing/browser-audit-fixes.md @@ -58,11 +58,12 @@ The final `live-atomic` run also verifies that busy completion requires no new t insertion. It covers atomic text updates, atomic child additions, non-atomic updates, suppression while busy, and release after busy becomes false. -Firefox baseline reading passed. Its tab regression timed out on the second +At the original review, Firefox baseline reading passed. Its tab regression timed +out on the second “New Tab” announcement in both the modified checkout and an untouched `bf4b9d7` snapshot. The recorded events include a selected-state event for the previous -unselected tab. This is an unresolved existing/provider case, not a verified -regression introduced by these changes. +unselected tab. That failure predated the changes reviewed here. The browser-test +maintenance below addresses it without relaxing the announcement assertions. The affected `go vet` checks, normal reader build, BrlAPI helper build, Python syntax check, parity-ledger validation and whitespace check passed. No installed @@ -90,6 +91,53 @@ go vet ./internal/atspi ./internal/review ./internal/app go run ./cmd/parityledger validate testdata/parity-ledger.csv ``` +## Browser-test maintenance (2026-11-03) + +The fragmented-line fixture expected an empty editor-control stop before its +first text line. Current browse navigation and its unit regression deliberately +descend directly into rich-editor text. The native fixture now requires that same +sequence, retaining forward/backward reading, character/word movement, Read All +and inline Braille-routing checks. + +Firefox can emit tab selection before the new tab enters its accessible child +list. Its later child-added event exposes the selected tab. The native adapter +now recognizes that event only for a live, freshly selected browser-chrome tab +whose parent matches the emitting tab list. Document-authored tabs and unselected +background additions do not qualify. Existing foreground checks and tab-identity +deduplication still apply. Child-added origin suffixes such as `:system` are +normalized before dispatch, with the original native type retained in diagnostics. +The browser profile keeps its normal animations; the tab test still requires an +announcement for each creation, switch and close, without duplicate announcements +or automatic address-rewrite speech. + +Private-session cleanup now waits up to five seconds for a document portal's +FUSE mount to disappear after D-Bus exits. Only disconnected/busy cleanup errors +are retried; unrelated errors and a persistent mount still fail the run. The +isolation regressions also verify that retries preserve a failing child test's +exit status. + +All 33 Chromium proofs passed in the final combined run, and Firefox baseline +and tab checks passed in repeated runs with normal animations. Eight private- +session regressions, focused package race tests, affected-package vet checks and +a separate reader build passed. An earlier Chromium run failed generated-link +Read All with “accessible children require recapture”; its isolated and final +combined reruns passed. That intermittent failure's cause is not established. + +```sh +python3 -B tests/test_native_test_session.py +CTHULHU_NATIVE_BROWSER_TESTS=fragments,tabs,linkedheadings,selection,modes,live,live-atomic python3 tests/native_browser.py +python3 tests/native_firefox.py +``` + +For live acceptance, create two Firefox tabs in quick succession, switch between +them, then close them. Each action should announce the newly selected tab once; +editing and deleting text in the address bar must still speak. Open a link in a +background tab and check that its creation does not announce a tab switch. Check +page-authored ARIA tabs and an ordinary GTK notebook retain their own behavior. +In `tests/browser-fragments.html`, navigate to “Embedded suggestion”: Down should +read “Review the code.” immediately, followed by “Report actionable issues.” and +“After the suggestion.” Check reverse navigation, Read All and Braille routing. + ## Human acceptance before submission Review the generated diff under the project's AI-assisted contribution policy. diff --git a/internal/atspi/adapter_event_callback_native.go b/internal/atspi/adapter_event_callback_native.go index 79eb7de..a8c3263 100644 --- a/internal/atspi/adapter_event_callback_native.go +++ b/internal/atspi/adapter_event_callback_native.go @@ -420,7 +420,7 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) { } return } - eventType := canonicalTextChangeEventType(nativeEventType) + eventType := canonicalObjectChangeEventType(nativeEventType) originID := nativeObjectID(event.source) if eventType == AccessibleNameEvent || eventType == DefunctStateEvent && C.cthulhu_event_detail1(event) != 0 { C.cthulhu_forget_menu_checkability(event.source) @@ -449,9 +449,9 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) { adapter.forgetWindowDocuments(originID) } invalidated := adapter.applyTreeInvalidation(eventType, event) - returnedMenuItem := adapter.returned_menu_item(event, eventType, originID) - if returnedMenuItem != nil { - defer C.g_object_unref(C.gpointer(returnedMenuItem)) + returnedTarget := adapter.returned_menu_item(event, eventType, originID) + if returnedTarget != nil { + defer C.g_object_unref(C.gpointer(returnedTarget)) // Keep the original showing event and popup origin in diagnostics. // Only fresh focus on its parent supplies this normalized focus event. eventType, invalidated = FocusEvent, false @@ -505,6 +505,11 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) { adapter.backgroundEventsFiltered.Add(1) return } + if tab := addedSelectedBrowserTab(event, eventType); tab != nil { + defer C.g_object_unref(C.gpointer(tab)) + returnedTarget = tab + eventType, invalidated = SelectionChangedEvent, false + } var live nativeLiveProperties if eventType == TextInsertedEvent || eventType == ChildrenAddedEvent || eventType == BusyStateEvent { live = capture_live_properties(event.source) @@ -523,7 +528,7 @@ func cthulhuAtspiEventCallback(event *C.AtspiEvent, userData unsafe.Pointer) { return } snapshotStarted := time.Now() - target, ownedTarget := adapter.resolveNativeEventTarget(event, eventType, returnedMenuItem) + target, ownedTarget := adapter.resolveNativeEventTarget(event, eventType, returnedTarget) if target == nil { return } diff --git a/internal/atspi/adapter_event_target_native.go b/internal/atspi/adapter_event_target_native.go index e1aefcb..a5a53f0 100644 --- a/internal/atspi/adapter_event_target_native.go +++ b/internal/atspi/adapter_event_target_native.go @@ -9,11 +9,58 @@ package atspi */ import "C" -// resolveNativeEventTarget runs on the GLib owner. The returned menu item is +// addedSelectedBrowserTab handles providers which emit selection before the new +// tab enters their accessible child list. The later child-added event supplies +// the actual target; only a freshly selected browser chrome tab qualifies. +// The caller releases the retained target on the GLib owner. +func addedSelectedBrowserTab(event *C.AtspiEvent, eventType string) *C.AtspiAccessible { + if eventType != ChildrenAddedEvent || C.atspi_accessible_get_role(event.source, nil) != C.ATSPI_ROLE_PAGE_TAB_LIST { + return nil + } + child := C.cthulhu_event_active_descendant(event) + if child == nil || C.atspi_accessible_get_role(child, nil) != C.ATSPI_ROLE_PAGE_TAB { + return nil + } + parent := C.atspi_accessible_get_parent(child, nil) + if parent == nil { + return nil + } + matchesParent := nativeObjectID(parent) == nativeObjectID(event.source) + C.g_object_unref(C.gpointer(parent)) + if !matchesParent { + return nil + } + C.atspi_accessible_clear_cache_single(child) + states := C.atspi_accessible_get_state_set(child) + if states == nil { + return nil + } + selected := C.atspi_state_set_contains(states, C.ATSPI_STATE_SELECTED) != 0 && + C.atspi_state_set_contains(states, C.ATSPI_STATE_DEFUNCT) == 0 + C.g_object_unref(C.gpointer(states)) + if !selected { + return nil + } + toolkit := C.atspi_accessible_get_toolkit_name(child, nil) + snapshot := Snapshot{Role: "page tab", ToolkitName: C.GoString(toolkit)} + C.g_free(C.gpointer(toolkit)) + document := C.cthulhu_document_ancestor(child) + if document != nil { + C.g_object_unref(C.gpointer(document)) + return nil + } + if !isBrowserChromeTab(snapshot, false) { + return nil + } + C.g_object_ref(C.gpointer(child)) + return child +} + +// resolveNativeEventTarget runs on the GLib owner. The override target is // borrowed from the callback; LibreOffice and selected descendants can return // owned references. The caller releases an owned target after event capture. -func (adapter *Adapter) resolveNativeEventTarget(event *C.AtspiEvent, eventType string, returnedMenuItem *C.AtspiAccessible) (*C.AtspiAccessible, bool) { - target := returnedMenuItem +func (adapter *Adapter) resolveNativeEventTarget(event *C.AtspiEvent, eventType string, overrideTarget *C.AtspiAccessible) (*C.AtspiAccessible, bool) { + target := overrideTarget ownedTarget := false if target == nil { target = adapter.libreOfficeFocusTarget(event, eventType) diff --git a/internal/atspi/model.go b/internal/atspi/model.go index d031214..eafafea 100644 --- a/internal/atspi/model.go +++ b/internal/atspi/model.go @@ -349,18 +349,18 @@ func NewEvent(eventType string, source Snapshot) Event { if eventType == "object:state-changed:checked" && source.Role == "menu item" { source.Checkable = true } - event := Event{Type: canonicalTextChangeEventType(eventType), Source: source, OriginPath: source.Path} + event := Event{Type: canonicalObjectChangeEventType(eventType), Source: source, OriginPath: source.Path} if event.Type != eventType { event.NativeType = eventType } return event } -// AT-SPI appends an origin to some text mutations (for example, :system). +// AT-SPI appends an origin to some object mutations (for example, :system). // The reader handles those mutations by kind; leaving the suffix on Type // makes them fall through as ordinary object presentation events. -func canonicalTextChangeEventType(eventType string) string { - for _, kind := range []string{TextInsertedEvent, TextDeletedEvent} { +func canonicalObjectChangeEventType(eventType string) string { + for _, kind := range []string{TextInsertedEvent, TextDeletedEvent, ChildrenAddedEvent} { if eventType == kind || strings.HasPrefix(eventType, kind+":") { return kind } diff --git a/internal/atspi/model_test.go b/internal/atspi/model_test.go index e1cbdd3..4928270 100644 --- a/internal/atspi/model_test.go +++ b/internal/atspi/model_test.go @@ -45,17 +45,21 @@ func TestShouldSpeakEvent(t *testing.T) { } } -func TestTextChangeOriginIsCanonicalized(t *testing.T) { +func TestObjectChangeOriginIsCanonicalized(t *testing.T) { for _, test := range []struct{ incoming, want string }{ {TextInsertedEvent + ":system", TextInsertedEvent}, {TextDeletedEvent + ":system", TextDeletedEvent}, {TextInsertedEvent, TextInsertedEvent}, {TextDeletedEvent, TextDeletedEvent}, + {ChildrenAddedEvent + ":system", ChildrenAddedEvent}, + {ChildrenAddedEvent, ChildrenAddedEvent}, + {ChildrenRemovedEvent + ":system", ChildrenRemovedEvent + ":system"}, + {"object:children-changed:additional", "object:children-changed:additional"}, {"object:text-caret-moved", "object:text-caret-moved"}, {"object:text-changed:insertion", "object:text-changed:insertion"}, } { - if got := canonicalTextChangeEventType(test.incoming); got != test.want { - t.Errorf("canonicalTextChangeEventType(%q) = %q, want %q", test.incoming, got, test.want) + if got := canonicalObjectChangeEventType(test.incoming); got != test.want { + t.Errorf("canonicalObjectChangeEventType(%q) = %q, want %q", test.incoming, got, test.want) } if got := NewEvent(test.incoming, Snapshot{}).Type; got != test.want { t.Errorf("NewEvent(%q).Type = %q, want %q", test.incoming, got, test.want) diff --git a/internal/atspi/pidgin_native.go b/internal/atspi/pidgin_native.go index 89d47c0..4aab4b3 100644 --- a/internal/atspi/pidgin_native.go +++ b/internal/atspi/pidgin_native.go @@ -35,7 +35,7 @@ import ( // title or application-supplied name. Only verified Pidgin transcript insertions // bypass background filtering. Entry typing and buddy-list changes do not. func (adapter *Adapter) pidginMessage(event *C.AtspiEvent) *messaging.Message { - if canonicalTextChangeEventType(C.GoString(event._type)) != TextInsertedEvent || adapter.applicationProcess(event.source) != "pidgin" || + if canonicalObjectChangeEventType(C.GoString(event._type)) != TextInsertedEvent || adapter.applicationProcess(event.source) != "pidgin" || C.atspi_accessible_get_role(event.source, nil) != C.ATSPI_ROLE_TEXT { return nil } diff --git a/internal/review/document_visual_line_native_test.go b/internal/review/document_visual_line_native_test.go index f1b1502..d9df652 100644 --- a/internal/review/document_visual_line_native_test.go +++ b/internal/review/document_visual_line_native_test.go @@ -196,7 +196,7 @@ func TestNativeBrowserFragmentLines(t *testing.T) { command DocumentCaretCommand want string }{ - {CaretNextLine, ""}, // Stop on the editor control before entering its text. + // Read rich-editor descendants directly, without an empty control stop. {CaretNextLine, "Review the code."}, {CaretNextLine, "Report actionable issues."}, {CaretNextLine, "After the suggestion."}, diff --git a/internal/version/version.go b/internal/version/version.go index 9a21679..24ae986 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -22,7 +22,7 @@ import "strings" const Name = "Cthulhu" const Version = "2026.11.03" -const Channel = "hunter" +const Channel = "testing" var Build = Version diff --git a/testdata/parity-ledger.csv b/testdata/parity-ledger.csv index 9137460..bd3885f 100644 --- a/testdata/parity-ledger.csv +++ b/testdata/parity-ledger.csv @@ -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 3 browser-test maintenance corrects the stale empty editor-stop expectation and private portal teardown. Firefox tab creation now handles a selected browser-chrome child added after early selection events; native origin suffixes are preserved in diagnostics. All 33 Chromium proofs and repeated Firefox baseline/tab runs pass; live rapid-tab and background-tab acceptance remains pending. See docs/testing/browser-audit-fixes.md#browser-test-maintenance-2026-11-03. An earlier generated-link Read All recapture failure did not reproduce in isolated or final combined reruns; its cause remains unverified. 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/native_test_session.py b/tests/native_test_session.py index 9999044..a79a4e1 100644 --- a/tests/native_test_session.py +++ b/tests/native_test_session.py @@ -3,10 +3,12 @@ # Authors: Storm Dragon, Angus Kola # SPDX-License-Identifier: GPL-3.0-or-later """Launch native tests with disposable runtime sockets and private D-Bus.""" +import errno import os import subprocess import sys import tempfile +import time def run_private_session(command, *, env, **kwargs): @@ -20,12 +22,26 @@ def run_private_session(command, *, env, **kwargs): # Keep this path short: AT-SPI falls back from named sockets if its # generated socket path is 100 bytes or longer. - with tempfile.TemporaryDirectory(prefix="cth-run-", dir="/tmp") as runtimeDir: - sessionEnv = dict(env, XDG_RUNTIME_DIR=runtimeDir) + runtime = tempfile.TemporaryDirectory(prefix="cth-run-", dir="/tmp") + try: + sessionEnv = dict(env, XDG_RUNTIME_DIR=runtime.name) for name in ("DBUS_SESSION_BUS_ADDRESS", "DBUS_STARTER_ADDRESS", "DBUS_STARTER_BUS_TYPE", "AT_SPI_BUS_ADDRESS", "WAYLAND_DISPLAY"): sessionEnv.pop(name, None) return subprocess.run(command, env=sessionEnv, **kwargs) + finally: + # Activated document portals tear down their FUSE mount after D-Bus + # exits. Wait for that teardown instead of hiding a test's result with + # a transient disconnected-mount error, or ignoring cleanup failures. + deadline = time.monotonic() + 5 + while True: + try: + runtime.cleanup() + break + except OSError as error: + if error.errno not in (errno.ENOTCONN, errno.EBUSY) or time.monotonic() >= deadline: + raise + time.sleep(0.05) def main(): diff --git a/tests/test_native_test_session.py b/tests/test_native_test_session.py index 931a786..b2cbdc1 100644 --- a/tests/test_native_test_session.py +++ b/tests/test_native_test_session.py @@ -4,6 +4,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later """Runtime isolation and real AT-SPI socket collision regressions.""" import ast +import errno import json import os from pathlib import Path @@ -55,6 +56,60 @@ print(json.dumps(dict(runtime=str(runtime), mode=stat.S_IMODE(runtime.stat().st_ self.assertEqual(caught.exception.returncode, 7) self.assertFalse(Path(caught.exception.stdout.strip()).exists()) + def test_portal_teardown_retry_preserves_child_failure(self): + originalCleanup = tempfile.TemporaryDirectory.cleanup + attempts = [] + + def cleanup(runtime): + attempts.append(runtime.name) + if len(attempts) <= 2: + errorCode = (errno.ENOTCONN, errno.EBUSY)[len(attempts) - 1] + raise OSError(errorCode, "Document portal is shutting down") + originalCleanup(runtime) + + with patch("native_test_session.tempfile.TemporaryDirectory.cleanup", cleanup), \ + patch("native_test_session.time.sleep") as pause: + with self.assertRaises(subprocess.CalledProcessError) as caught: + run_private_session( + ["dbus-run-session", "--", sys.executable, "-c", "raise SystemExit(7)"], + env=os.environ, capture_output=True, check=True, timeout=15) + self.assertEqual(caught.exception.returncode, 7) + self.assertEqual(len(attempts), 3) + self.assertEqual(pause.call_count, 2) + self.assertFalse(Path(attempts[0]).exists()) + + def test_cleanup_does_not_hide_unrelated_errors(self): + originalCleanup = tempfile.TemporaryDirectory.cleanup + + def cleanup(runtime): + originalCleanup(runtime) + raise PermissionError(errno.EACCES, "Cleanup denied") + + with patch("native_test_session.tempfile.TemporaryDirectory.cleanup", cleanup), \ + patch("native_test_session.time.sleep") as pause: + with self.assertRaises(PermissionError): + run_private_session( + ["dbus-run-session", "--", sys.executable, "-c", "pass"], + env=os.environ, capture_output=True, check=True, timeout=15) + pause.assert_not_called() + + def test_portal_teardown_wait_is_bounded(self): + originalCleanup = tempfile.TemporaryDirectory.cleanup + + def cleanup(runtime): + originalCleanup(runtime) + raise OSError(errno.ENOTCONN, "Document portal did not unmount") + + with patch("native_test_session.tempfile.TemporaryDirectory.cleanup", cleanup), \ + patch("native_test_session.time.monotonic", side_effect=[0, 6]), \ + patch("native_test_session.time.sleep") as pause: + with self.assertRaises(OSError) as caught: + run_private_session( + ["dbus-run-session", "--", sys.executable, "-c", "pass"], + env=os.environ, capture_output=True, check=True, timeout=15) + self.assertEqual(caught.exception.errno, errno.ENOTCONN) + pause.assert_not_called() + def test_rejects_dbus_before_xvfb_without_launching(self): with patch("native_test_session.subprocess.run") as launch: with self.assertRaisesRegex(ValueError, "Start Xvfb before D-Bus"):