From 04e6c08fff0c1beaf2fd31fe1ddf5f459337c74f Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Thu, 15 Oct 2026 21:32:44 -0400 Subject: [PATCH] Review and strengthen LibreOffice code so far. --- LIBREOFFICE_SUPPORT.md | 97 ++++++++++----------- internal/app/libreoffice_native_test.go | 72 +++++++++++++-- internal/app/runtime_test.go | 30 ++++--- internal/atspi/adapter_foreground_native.go | 9 +- internal/atspi/libreoffice_native.go | 24 ++++- internal/tentacle/session_test.go | 4 +- testdata/parity-ledger.csv | 2 +- tests/native_libreoffice.py | 26 +++++- 8 files changed, 186 insertions(+), 78 deletions(-) diff --git a/LIBREOFFICE_SUPPORT.md b/LIBREOFFICE_SUPPORT.md index 9d8563d..a65aef4 100644 --- a/LIBREOFFICE_SUPPORT.md +++ b/LIBREOFFICE_SUPPORT.md @@ -47,6 +47,12 @@ reorganizing unrelated reader code or building a speculative plugin framework. - Calc active-cell events establish logical focus, including after editing and when the sheet changes. +- LibreOffice frame activation and X11 foreground recovery establish window + ownership without the generic focused-descendant search. Calc's following + cell/editor events establish document focus. Searching the previous view + during workbook activation caused stale-object queries and timeouts on return; + leaving activation on the frame avoids that work. Dialogs retain normal + focused-control resolution. - Cell contents and addresses are separate. Normal speech gives the value, then the address; an empty value gives “blank.” Only-displayed-text mode omits the address and the synthetic blank label. @@ -185,17 +191,21 @@ checks. These slices do not establish complete Office support. ## Verification ```sh -GOCACHE=/tmp/cthulhu-go-cache go test -race ./internal/atspi ./internal/app ./internal/presentation ./internal/input ./internal/platform/xorg -count=1 +dbus-run-session -- env GOCACHE=/tmp/cthulhu-go-cache go test -race ./... -count=1 +dbus-run-session -- env GOCACHE=/tmp/cthulhu-go-cache go test -tags brlapi ./... -count=1 +python3 tests/check_speech.py /usr/bin/python3 tests/native_libreoffice.py python3 tests/native_atspi.py --table-only GOCACHE=/tmp/cthulhu-go-cache go build -o /tmp/cthulhu-libreoffice ./cmd/cthulhu GOCACHE=/tmp/cthulhu-go-cache go test -c -o /tmp/cthulhu-libreoffice-xorg.test ./internal/platform/xorg -CTHULHU_BINARY=/tmp/cthulhu-libreoffice python3 tests/native_x11.py +CTHULHU_BINARY=/tmp/cthulhu-libreoffice CTHULHU_XRECORD_TEST=/tmp/cthulhu-libreoffice-xorg.test python3 tests/native_x11.py xvfb-run -a env CTHULHU_NATIVE_X11=1 /tmp/cthulhu-libreoffice-xorg.test -test.run '^TestNativeDispatchBeforeRelease$' -test.v ``` -The Calc harness launches installed LibreOffice with a disposable profile, -document, virtual X11 display, and private session/accessibility buses. +The Calc harness requires i3 as well as LibreOffice, Xvfb, xdotool, xprop, +the Python AT-SPI bindings and the normal native build dependencies. It launches +installed LibreOffice with a disposable profile, document, private i3 window +manager, virtual X11 display, and private session/accessibility buses. It exercises the production native adapter and event loop; recorded speech and Braille are asserted for cell movement and blanks, F2 typing, Escape cancellation, Enter commit, caret movement inside an existing numeric cell, @@ -210,8 +220,11 @@ The native listener tests check the desktop formula shortcut before key release; the Calc harness sends semantic requests through the production event loop. Dynamic-header checks exercise both axes, automatic axis-change speech, Where Am I, complete Braille context, heading edits, independent clearing, -sheet round trips, and a second workbook with matching sheet names. Returning -to the original workbook is a separate pending acceptance check below. +sheet round trips, and both return directions between workbooks with matching +sheet names and different headings, using normal navigation without Ctrl+F6 +recovery. Recorded speech, Where Am I and Braille must retain each workbook's +own headings. Find and Replace typing and +cancellation check the ordinary focused-control path and return to the sheet. Header command bindings are checked through the shared binding resolver in both layouts. No active reader is replaced and no normal Office profile is changed. @@ -260,49 +273,31 @@ physical Braille. Before submission, review the diff and test a rebuilt binary: Human review and testing remain required by `CONTRIBUTING.md`. Passing this fixture alone is not sufficient for submission or live accessibility acceptance. -Initial verification against LibreOffice 26.2.6.3 passed the Calc harness, -affected-package race tests, vet/build, GTK table/multiselection checks, and -Xorg input checks. Browser tab and table checks also passed. Browser editing -failed on an extra title announcement both here and on untouched commit -`8e76d23`. Browser modes initially failed to focus a heading after reload, -then passed on the baseline and on a rerun here; the cause of that first -failure was not established. Physical acceptance remains pending. +The tightening pass against LibreOffice 26.2.6.3 verifies the existing cell, +editor, formula, selection and dynamic-header slices and adds workbook return +coverage. Returning to the original workbook previously reproduced stale-object +timeouts through the production adapter. Keeping LibreOffice frame activation +out of the generic descendant search makes the recorded return checks pass; +header choices remain independent in both workbooks. Foreground recovery uses +the same policy, preventing raw sheet-state chatter before the first cell event. +The virtual-display driver uses window-manager activation and waits for the +heading cell before editing it. -Formula-slice verification passed the isolated real Calc harness, focused -race tests, vet/build, ordinary GTK table/multiselection checks, and the rebuilt -reader's Xorg smoke test. A separate private X11 listener test verified the -formula shortcut with both release orders. The broader app race run exposed -`TestRunReachesSoundStopAfterEarlierServiceStopError`: it can cancel during -startup and receive `context canceled` instead of its expected shutdown error. -The same failure reproduced on unchanged commit `e0c8c61` with -`GOMAXPROCS=8 go test -race ./internal/app -run '^TestRunReachesSoundStopAfterEarlierServiceStopError$' -count=100`. -The remaining app race tests passed with that one test excluded. -The broader native listener suite also has an existing C helper link failure -for missing xkbcommon symbols; the formula shortcut was tested directly using -the command above. Audible formula speech, physical input/Braille, and real -workbook acceptance remain pending. +Full default, race and BrlAPI-tagged Go suites, the speech regression gate, +vet/build, parity-ledger validation, ordinary GTK table/multiselection checks, +and the isolated X11 reader/listener harness pass. The former shutdown test +failure is fixed by waiting for completed runtime startup before cancellation. +A process-cleanup assertion also now accepts ESRCH when a killed task disappears +during a procfs read. Neither correction skips the underlying shutdown checks. +The earlier native probe linker failure no longer reproduces with the current +listener harness. -Selection-slice verification passed the isolated real Calc harness (including -disjoint and capped selections), focused race tests, vet/build, parity-ledger -validation, and ordinary GTK table/multiselection checks. The full affected-package -race run again hit the startup timing test above; all remaining affected-package -race tests passed with -`go test -race ./internal/atspi ./internal/app ./internal/presentation -skip '^TestRunReachesSoundStopAfterEarlierServiceStopError$' -count=1`. -The whole-row/column contraction event gaps described above were independently -observed without the reader. Audible output, physical input/Braille, and -independent-workbook acceptance remain pending. - -Dynamic-header verification passed focused race checks, the full affected-package -race run in a private D-Bus session, vet/build, parity-ledger validation and -ordinary GTK table/multiselection checks. Recorded Calc checks cover both axes, -heading edits, independent clearing, sheet round trips and isolation in a second -workbook with identical sheet names. A synthetic return to the original workbook -produced stale/timed-out accessibility objects after window activation, even -with Ctrl+F6 and explicit window raising. Its cause was not established, and -baseline behavior was not compared. This is not evidence of working workbook -return; verify that workflow separately. Tab rename/add/remove event delivery, -physical speech, physical input/Braille and independent real-workbook acceptance -remain pending. +These results establish recorded private-session behavior. Audible output, +physical input/Braille, normal-profile workbook return, tab rename/add/remove +event delivery, merged/structurally edited headings, large-sheet responsiveness +and independent real-workbook acceptance still need their own checks. Earlier +browser checks recorded an editing-title failure and one non-reproducible mode +failure; this pass does not establish current live browser acceptance. ## Calc startup focus diagnosis @@ -323,9 +318,11 @@ LibreOffice documents this shortcut in its Automatic focus stealing would interfere with intentional toolbar use and is not part of the reader's handling. -The native Calc test focuses its window once and uses ordinary key sequences -afterward. It also checks F6 followed by Ctrl+F6 and resumed cell speech/Braille. -Its fresh-profile virtual display does not cover this normal-profile startup +The native Calc test activates its initial window, establishes document focus +with F6 followed by Ctrl+F6, and uses ordinary key sequences within each document. +Explicit window-manager activation switches workbooks and focuses the dialog. +It also checks later F6/Ctrl+F6 recovery and resumed cell speech/Braille. +Its fresh-profile private i3 display does not cover this normal-profile startup failure. The same production test also passed under private i3 with the copied normal profile after Ctrl+F6 recovery, asserting recorded cell speech and Braille along with editing and sheet switching. Verify startup and recovery diff --git a/internal/app/libreoffice_native_test.go b/internal/app/libreoffice_native_test.go index 95e07dc..285cf46 100644 --- a/internal/app/libreoffice_native_test.go +++ b/internal/app/libreoffice_native_test.go @@ -130,12 +130,16 @@ func TestNativeLibreOfficeCalc(t *testing.T) { } } } + activate := func(window string) { + t.Helper() + if err := exec.CommandContext(ctx, "xdotool", "windowactivate", "--sync", window).Run(); err != nil { + t.Fatal(err) + } + } // Focus once. Refocusing before every key sequence can hide failures // to retain or recover Calc's document focus during normal keyboard use. - if err := exec.CommandContext(ctx, "xdotool", "windowfocus", window).Run(); err != nil { - t.Fatal(err) - } - press("Escape", "ctrl+Home", "Right") + activate(window) + press("Escape", "F6", "ctrl+F6", "ctrl+Home", "Right") waitSpeech("42, B1") waitFrame("42 B1") press("Down") @@ -277,7 +281,9 @@ func TestNativeLibreOfficeCalc(t *testing.T) { press("ctrl+Page_Up", "ctrl+Home", "Right", "Down") waitSpeech("y row header, Beta, B2") // A heading edit is visible on the next cell snapshot, without a cache. - press("Up", "8", "Return") + press("Up") + waitSpeech("Alpha row header, 42, B1") + press("8", "Return") waitSpeech("y row header, Beta, B2") waitFrame("y 8 Beta B2") commands <- input.WhereAmI @@ -320,10 +326,7 @@ func TestNativeLibreOfficeCalc(t *testing.T) { if secondWindow == "" { t.Fatal("second private workbook window missing") } - // A virtual display without a window manager does not focus new windows. - if err := exec.CommandContext(ctx, "xdotool", "windowfocus", secondWindow).Run(); err != nil { - t.Fatal(err) - } + activate(secondWindow) press("ctrl+F6", "ctrl+Page_Down", "ctrl+Home", "Down") waitSpeech("blank, A2") commands <- input.WhereAmI @@ -338,4 +341,55 @@ func TestNativeLibreOfficeCalc(t *testing.T) { waitSpeech("blank, A2") commands <- input.WhereAmI waitSpeech("d column header, blank, A2") + // Return to the first workbook, then back again: identical sheet names + // must retain independent choices and fresh cells in both directions. + // Use normal navigation here, without the Ctrl+F6 recovery shortcut. + activate(window) + press("ctrl+Home", "Down") + waitSpeech("blank, A2") + commands <- input.WhereAmI + waitSpeech("Gamma column header, blank, A2") + waitFrame("Gamma A2") + activate(secondWindow) + press("ctrl+Home", "Down") + waitSpeech("blank, A2") + commands <- input.WhereAmI + waitSpeech("d column header, blank, A2") + waitFrame("d A2") + // Dialog activation still resolves its focused control. Cancelling it + // must leave the document navigable with the same header choices. + press("ctrl+h") + dialogWindow := "" + deadline = time.Now().Add(5 * time.Second) + for dialogWindow == "" && time.Now().Before(deadline) { + output, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Find and Replace$").Output() + if candidates := strings.Fields(string(output)); len(candidates) == 1 { + dialogWindow = candidates[0] + } else { + time.Sleep(20 * time.Millisecond) + } + } + if dialogWindow == "" { + t.Fatal("private Find and Replace dialog window missing") + } + activate(dialogWindow) + press("x") + waitSpeech("x") + press("Escape") + deadline = time.Now().Add(5 * time.Second) + for { + output, _ := exec.CommandContext(ctx, "xdotool", "search", "--onlyvisible", "--name", "^Find and Replace$").Output() + if len(strings.Fields(string(output))) == 0 { + break + } + if time.Now().After(deadline) { + t.Fatal("private Find and Replace dialog did not close") + } + time.Sleep(20 * time.Millisecond) + } + activate(secondWindow) + press("ctrl+F6", "ctrl+Home", "Down") + waitSpeech("blank, A2") + commands <- input.WhereAmI + waitSpeech("d column header, blank, A2") } diff --git a/internal/app/runtime_test.go b/internal/app/runtime_test.go index 8f06148..46af168 100644 --- a/internal/app/runtime_test.go +++ b/internal/app/runtime_test.go @@ -174,13 +174,19 @@ func TestRunReachesSoundStopAfterEarlierServiceStopError(t *testing.T) { } sounds := &recordingSound{started: make(chan struct{})} ctx, cancel := context.WithCancel(context.Background()) + defer cancel() completed := make(chan error, 1) + // Sound starts first; cancellation there races later service startup. + // The startup callback runs only once the complete runtime is ready. + runtimeStarted := make(chan struct{}) go func() { completed <- Run(ctx, Options{ - Logger: slog.New(slog.DiscardHandler), - Settings: &settings, - newSound: func() soundService { return sounds }, + Logger: slog.New(slog.DiscardHandler), + Settings: &settings, + OpenPreferences: true, + onPreferences: func() { close(runtimeStarted) }, + newSound: func() soundService { return sounds }, newCommands: func() commandListener { return silentCommands{} }, @@ -190,9 +196,9 @@ func TestRunReachesSoundStopAfterEarlierServiceStopError(t *testing.T) { }() select { - case <-sounds.started: + case <-runtimeStarted: case <-time.After(time.Second): - t.Fatal("sound service did not start") + t.Fatal("runtime did not finish startup") } cancel() select { @@ -217,13 +223,17 @@ func TestRunReportsSoundStopError(t *testing.T) { soundError := errors.New("stop cue failed") sounds := &recordingSound{started: make(chan struct{}), stopError: soundError} ctx, cancel := context.WithCancel(context.Background()) + defer cancel() completed := make(chan error, 1) + runtimeStarted := make(chan struct{}) go func() { completed <- Run(ctx, Options{ - Logger: slog.New(slog.DiscardHandler), - Settings: &settings, - newSound: func() soundService { return sounds }, + Logger: slog.New(slog.DiscardHandler), + Settings: &settings, + OpenPreferences: true, + onPreferences: func() { close(runtimeStarted) }, + newSound: func() soundService { return sounds }, newCommands: func() commandListener { return silentCommands{} }, @@ -235,9 +245,9 @@ func TestRunReportsSoundStopError(t *testing.T) { }() select { - case <-sounds.started: + case <-runtimeStarted: case <-time.After(time.Second): - t.Fatal("sound service did not start") + t.Fatal("runtime did not finish startup") } cancel() select { diff --git a/internal/atspi/adapter_foreground_native.go b/internal/atspi/adapter_foreground_native.go index e806f3d..f72b169 100644 --- a/internal/atspi/adapter_foreground_native.go +++ b/internal/atspi/adapter_foreground_native.go @@ -184,7 +184,11 @@ func (adapter *Adapter) recoverForeground() { continue } if state.Epoch == adapter.Foreground().Epoch && adapter.matchesForeground(window, nativeObjectID(window)) { - target := C.cthulhu_focused_descendant(window) + target := adapter.libreOfficeFrameTarget(window) + officeFrame := target != nil + if target == nil { + target = C.cthulhu_focused_descendant(window) + } if target == nil { target = (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(window))) } @@ -202,6 +206,9 @@ func (adapter *Adapter) recoverForeground() { target = child } snapshot := snapshotFromNativeWithCaret(target, true, false, false, adapter.captureSound.Load(), adapter.captureTextAttributes.Load()) + if officeFrame { + snapshot.ApplicationProcess = adapter.applicationProcess(target) + } event := NewEvent(WindowActivatedEvent, snapshot) event.Window = nativeObjectID(window) name := C.atspi_accessible_get_name(window, nil) diff --git a/internal/atspi/libreoffice_native.go b/internal/atspi/libreoffice_native.go index ece8307..482a201 100644 --- a/internal/atspi/libreoffice_native.go +++ b/internal/atspi/libreoffice_native.go @@ -11,15 +11,19 @@ import "C" import "unsafe" -// libreOfficeFocusTarget resolves a Calc editor's direct text children and -// preserves the sheet source for selection events. F2 focuses its temporary -// panel; the paragraph can appear later, on typing. +// libreOfficeFocusTarget keeps Office frame activation separate from document +// focus, resolves a Calc editor's direct text children, and preserves the sheet +// source for selection events. F2 focuses its temporary panel; the paragraph +// can appear later, on typing. // The callback owns and releases the returned reference. No sheet is walked. func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType string) *C.AtspiAccessible { - if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent || + if eventType != FocusEvent && eventType != FocusedStateEvent && eventType != SelectionChangedEvent && eventType != WindowActivatedEvent || !IsLibreOfficeProcess(adapter.applicationProcess(event.source)) { return nil } + if eventType == WindowActivatedEvent { + return adapter.libreOfficeFrameTarget(event.source) + } role := C.atspi_accessible_get_role(event.source, nil) if eventType == SelectionChangedEvent { // Keep the sheet as the selection source. Its first selected cell @@ -79,6 +83,18 @@ func (adapter *Adapter) libreOfficeFocusTarget(event *C.AtspiEvent, eventType st return nil } +// Both real activation events and X11 foreground recovery must avoid searching +// Calc's previous view while it rebuilds controls on workbook return. Own the +// frame and let the following cell/editor events establish document focus. +// Dialogs retain ordinary focused-control resolution. The caller owns the ref. +func (adapter *Adapter) libreOfficeFrameTarget(source *C.AtspiAccessible) *C.AtspiAccessible { + if !IsLibreOfficeProcess(adapter.applicationProcess(source)) || + C.atspi_accessible_get_role(source, nil) != C.ATSPI_ROLE_FRAME { + return nil + } + return (*C.AtspiAccessible)(C.g_object_ref(C.gpointer(source))) +} + func captureLibreOfficeContext(source *C.AtspiAccessible, snapshot *Snapshot) { if !IsLibreOfficeProcess(snapshot.ApplicationProcess) || snapshot.Defunct { return diff --git a/internal/tentacle/session_test.go b/internal/tentacle/session_test.go index 87012c1..5f2e51d 100644 --- a/internal/tentacle/session_test.go +++ b/internal/tentacle/session_test.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "sync" + "syscall" "testing" "time" @@ -540,13 +541,14 @@ func TestDisableKillsGroupDescendantsAndJoinsPipes(t *testing.T) { wait_signal(t, session.Done()) // SIGKILL delivery/reaping is scheduled by the kernel; wait for /proc to // report disappearance or zombie state, not an arbitrary sleep duration. + // A task that exits during ReadFile can return ESRCH rather than ENOENT. deadline := time.NewTimer(2 * time.Second) defer deadline.Stop() ticker := time.NewTicker(time.Millisecond) defer ticker.Stop() for { data, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/stat") - if os.IsNotExist(err) { + if os.IsNotExist(err) || errors.Is(err, syscall.ESRCH) { break } if err != nil { diff --git a/testdata/parity-ledger.csv b/testdata/parity-ledger.csv index 46a5cb6..9c68c73 100644 --- a/testdata/parity-ledger.csv +++ b/testdata/parity-ledger.csv @@ -54,6 +54,6 @@ web-browse-selection,src/cthulhu/scripts/web/script.py;src/cthulhu/script_utilit web-browse-braille-line,src/cthulhu/caret_navigation.py;src/cthulhu/scripts/web/script.py;src/cthulhu/scripts/web/braille_generator.py,braille,Follow accepted browse-caret and structural navigation with the current native text line and virtual cursor,,internal/atspi internal/review internal/app,implemented,"go test -race ./internal/app ./internal/review ./internal/atspi ./internal/braille -count=1; CTHULHU_NATIVE_BROWSER_TESTS=modes,tables,keys python3 tests/native_browser.py",Private Brave/Xvfb mode/table/key checks pass on 2026-10-02; physical Xorg pending.,,Existing browse speech assertions pass in the private fixture.,Queued native line and cursor frames pass; physical HumanWare NLS eReader acceptance pending.,Reads run on the bounded navigation worker only while braille is enabled; preserve spaces and Unicode offsets. Table frames retain existing cell presentation. Complete cross-object visual-line assembly and virtual-caret routing remain open. See docs/testing/braille.md. web-explicit-link-names,src/cthulhu/scripts/web/script_utilities.py:_treatObjectAsWhole,web,Read explicitly named links as whole objects without losing descendant badges or ordinary link navigation,Storm Dragon,internal/atspi internal/review internal/app,in-progress,"go test -race ./...; CTHULHU_NATIVE_BROWSER_TESTS=namedlinks,browser,live,keys python3 tests/native_browser.py",Private Brave/Xorg named-link and key checks passed,,Fixture speech verified; real Steam listening pending,Named-link browse braille uses the full accessible label; physical check pending,docs/testing/steam-browser.md; actual Steam explicit-name exposure and game-card acceptance remain live gates steam-notifications,src/cthulhu/scripts/apps/steamwebhelper/script.py;tests/test_steam_notification_regressions.py,web,Coalesce verified Steam background toasts and present once through speech braille and notification history,Storm Dragon,internal/webscripts internal/atspi internal/app,in-progress,go test -race ./...; python3 tests/native_webscripts.py,Private GTK/AT-SPI process matching and alert-root capture passed,,Event-loop background delivery and Chromium isolation passed; real Steam toast pending,Event-loop flash verified; physical Steam toast display pending,docs/testing/steam-browser.md; independent root and bus queues deliberately prevent unrelated toast timestamps from merging -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; audible and physical-input acceptance pending.,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. Real Calc checks cover heading edits independent axis clearing and sheet round trips; 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. +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; audible and physical-input acceptance pending.,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. Live niri and physical key suppression remain unverified.,Pending live listening on Wayland,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. diff --git a/tests/native_libreoffice.py b/tests/native_libreoffice.py index bdfd106..c75e96a 100644 --- a/tests/native_libreoffice.py +++ b/tests/native_libreoffice.py @@ -2,6 +2,7 @@ """Real Calc through the production reader, in a private X11/AT-SPI session.""" import os +from contextlib import ExitStack from pathlib import Path import shutil import signal @@ -26,8 +27,29 @@ def run_inside(): gi.require_version("Atspi", "2.0") from gi.repository import Atspi - with tempfile.TemporaryDirectory(prefix="cthulhu-libreoffice-") as temporary: + with tempfile.TemporaryDirectory(prefix="cthulhu-libreoffice-") as temporary, ExitStack() as cleanup: root = Path(temporary) + # Workbook and modal-dialog activation need a window manager's focus + # protocol, rather than bare XSetInputFocus on a virtual display. + wmConfig = root / "i3.config" + wmConfig.write_text( + 'font pango:monospace 10\nfocus_follows_mouse no\n' + f'ipc-socket {root / "i3.sock"}\n', encoding="utf-8", + ) + windowManager = subprocess.Popen( + ["i3", "-a", "-c", str(wmConfig)], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + + def stop_window_manager(): + if windowManager.poll() is None: + windowManager.terminate() + windowManager.wait(timeout=5) + + cleanup.callback(stop_window_manager) + wait_for(lambda: "0x" in subprocess.check_output( + ["xprop", "-root", "_NET_SUPPORTING_WM_CHECK"], text=True), + "Private i3 window manager missing") profile = root / "profile" / "user" profile.mkdir(parents=True) (profile / "registrymodifications.xcu").write_text( @@ -142,7 +164,7 @@ def main(): environment = dict(os.environ, XDG_SESSION_TYPE="x11", XDG_CONFIG_HOME=temporary, XDG_CACHE_HOME=temporary, XDG_DATA_HOME=temporary) - for name in ("DBUS_SESSION_BUS_ADDRESS", "AT_SPI_BUS_ADDRESS", "WAYLAND_DISPLAY"): + for name in ("DBUS_SESSION_BUS_ADDRESS", "AT_SPI_BUS_ADDRESS", "WAYLAND_DISPLAY", "I3SOCK"): environment.pop(name, None) subprocess.run( ["xvfb-run", "-a", "dbus-run-session", "--", sys.executable,