From 02fb70e81acbd0615ce1130e6a4fd0c9411b48c7 Mon Sep 17 00:00:00 2001 From: Angus Kola Date: Thu, 5 Nov 2026 16:56:50 -0500 Subject: [PATCH] Keep uncertain accessibility sources out of Remote speech --- .../app/speech_document_observation_test.go | 18 +++++++++-- internal/app/speech_observation.go | 31 ++++++++++--------- internal/app/speech_observation_test.go | 8 +++++ 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/internal/app/speech_document_observation_test.go b/internal/app/speech_document_observation_test.go index 506870e..72306cd 100644 --- a/internal/app/speech_document_observation_test.go +++ b/internal/app/speech_document_observation_test.go @@ -46,11 +46,21 @@ func document_observation_request(t *testing.T, kind string, result review.Resul func TestRemoteDocumentSpeechSourcePrivacy(t *testing.T) { for _, kind := range []string{"caret", "navigation", "read-all"} { - for _, source := range []string{"ordinary", "password", "ancestor", "snapshot-ancestor"} { + for _, source := range []string{"ordinary", "password", "ancestor", "snapshot-ancestor", "unknown", "invalid", "defunct", "unknown-ancestor", "defunct-ancestor", "unknown-snapshot-ancestor"} { t.Run(kind+"/"+source, func(t *testing.T) { snapshot := atspi.Snapshot{Path: "/text", Role: "text", Name: "Reading. Next sentence."} ancestors := []atspi.Snapshot{} switch source { + case "unknown", "invalid": + snapshot.Role = source + case "defunct": + snapshot.Defunct = true + case "unknown-ancestor": + ancestors = []atspi.Snapshot{{Role: "unknown"}} + case "defunct-ancestor": + ancestors = []atspi.Snapshot{{Role: "section", Defunct: true}} + case "unknown-snapshot-ancestor": + snapshot.SpeechContext = []atspi.Snapshot{{Role: "unknown"}} case "password": snapshot.Role = "password text" case "ancestor": @@ -90,7 +100,7 @@ func TestRemoteDocumentSpeechSourcePrivacy(t *testing.T) { func TestRemoteDocumentSpeechFragmentPrivacy(t *testing.T) { worker, session, _, _ := observation_fixture(t) for _, kind := range []string{"caret", "read-all"} { - for _, source := range []string{"ordinary", "password", "ancestor", "snapshot-ancestor", "unknown", "missing"} { + for _, source := range []string{"ordinary", "password", "ancestor", "snapshot-ancestor", "unknown", "unknown-role", "invalid", "defunct", "missing"} { t.Run(kind+"/"+source, func(t *testing.T) { caret := atspi.Snapshot{Path: "/caret", Role: "static"} neighbor := atspi.Snapshot{Path: "/neighbor", Role: "static"} @@ -104,6 +114,10 @@ func TestRemoteDocumentSpeechFragmentPrivacy(t *testing.T) { context[0].SpeechContext = []atspi.Snapshot{{Role: "password text"}} case "unknown": context[0].Role = "" + case "unknown-role", "invalid": + context[0].Role = strings.TrimSuffix(source, "-role") + case "defunct": + context[0].Defunct = true case "missing": context = nil } diff --git a/internal/app/speech_observation.go b/internal/app/speech_observation.go index 20affd5..9523733 100644 --- a/internal/app/speech_observation.go +++ b/internal/app/speech_observation.go @@ -33,24 +33,33 @@ func accessibility_speech_origin(source atspi.Snapshot) speechOrigin { return speechOriginSecret } } + if !known_speech_source(source) { + return speechOriginUnknown + } + for _, ancestor := range source.SpeechContext { + if !known_speech_source(ancestor) { + return speechOriginUnknown + } + } return speechOriginLocal } +func known_speech_source(source atspi.Snapshot) bool { + return !source.Defunct && source.Role != "" && source.Role != "unknown" && source.Role != "invalid" +} + // Document producers may remove the current object from their presentation // context. Inspect the original sources before that filtering, including every // content object in an assembled line. A mixed sensitive line stays private. func document_speech_origin(source atspi.Snapshot, ancestors []atspi.Snapshot) speechOrigin { - if accessibility_speech_origin(source) == speechOriginSecret { - return speechOriginSecret + if origin := accessibility_speech_origin(source); origin != speechOriginLocal { + return origin } for _, ancestor := range ancestors { - if accessibility_speech_origin(ancestor) == speechOriginSecret { - return speechOriginSecret + if origin := accessibility_speech_origin(ancestor); origin != speechOriginLocal { + return origin } } - if source.Role == "" { - return speechOriginUnknown - } return speechOriginLocal } @@ -90,14 +99,6 @@ func review_speech_origin(contexts ...[]atspi.Snapshot) speechOrigin { if origin := document_speech_origin(source, source.SpeechContext); origin != speechOriginLocal { return origin } - if source.Defunct || source.Role == "unknown" || source.Role == "invalid" { - return speechOriginUnknown - } - for _, ancestor := range source.SpeechContext { - if ancestor.Role == "" || ancestor.Role == "unknown" || ancestor.Role == "invalid" || ancestor.Defunct { - return speechOriginUnknown - } - } } } return speechOriginLocal diff --git a/internal/app/speech_observation_test.go b/internal/app/speech_observation_test.go index f8c3ad3..d347d50 100644 --- a/internal/app/speech_observation_test.go +++ b/internal/app/speech_observation_test.go @@ -203,6 +203,14 @@ func TestRemoteSpeechObservationAccessibilityPresenter(t *testing.T) { Path: "/nested-entry", Role: "entry", Name: "private descendant", SpeechContext: []atspi.Snapshot{{Role: "password text"}}, }}) + for _, source := range []atspi.Snapshot{ + {Role: "unknown"}, {Role: "invalid"}, {Role: "entry", Defunct: true}, + {Role: "entry", SpeechContext: []atspi.Snapshot{{Role: "unknown"}}}, + {Role: "entry", SpeechContext: []atspi.Snapshot{{Role: "panel", Defunct: true}}}, + } { + source.Path, source.Name = "/uncertain-entry", "unclassified source" + presenter.speak(atspi.Event{Type: "focus:", Source: source}) + } if err := worker.EnqueueAndWait(t.Context(), speechRequest{Text: "private completion barrier"}); err != nil { t.Fatal(err) }