Share verified native Where Am I speech with Remote
This commit is contained in:
1 parent
ee3d91af23
commit
8631c5a601
15 files changed
+473
-12
No files matched your search
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/atspi"
|
||||
"git.stormux.org/storm/cthulhu/internal/presentation"
|
||||
"git.stormux.org/storm/cthulhu/internal/review"
|
||||
)
|
||||
|
||||
func native_where_am_i_handler(t *testing.T) (*eventReviewHandler, *fakeSpeechQueue, review.Result) {
|
||||
t.Helper()
|
||||
result := native_current_line_result()
|
||||
result.WhereAmI, result.DocumentFocus = true, result.DocumentPosition.Snapshot.ID()
|
||||
result.DocumentPosition.Snapshot.Name = "Native colour."
|
||||
worker, err := review.NewWorker(&fakeAdapter{}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
worker.SyncFocus(result.DocumentFocus)
|
||||
queue := &fakeSpeechQueue{requests: make(chan speechRequest, 4), spoken: make(chan string, 4)}
|
||||
handler := &eventReviewHandler{state: &eventLoopState{}, objectReview: worker, speech: queue,
|
||||
logger: slog.New(slog.DiscardHandler), focusGate: &focusGate{}, syncForeground: func() {}}
|
||||
handler.state.setCurrent(result.DocumentPosition.Snapshot)
|
||||
handler.state.presentationOptions = presentation.DefaultOptions()
|
||||
result.WhereAmIRevision = handler.state.currentRevision
|
||||
return handler, queue, result
|
||||
}
|
||||
|
||||
func native_where_am_i_request(t *testing.T, privacy string) speechRequest {
|
||||
t.Helper()
|
||||
handler, queue, result := native_where_am_i_handler(t)
|
||||
switch privacy {
|
||||
case "password":
|
||||
result.DocumentPosition.Snapshot.Role = "password text"
|
||||
case "ancestor", "unknown", "invalid":
|
||||
role := privacy
|
||||
if privacy == "ancestor" {
|
||||
role = "password text"
|
||||
}
|
||||
result.DocumentCaretContext[1].Role = role
|
||||
case "defunct":
|
||||
result.DocumentCaretContext[1].Defunct = true
|
||||
case "missing":
|
||||
result.DocumentCaretContext = nil
|
||||
case "truncated":
|
||||
result.DocumentCaretContext = result.DocumentCaretContext[:2]
|
||||
case "mismatched":
|
||||
result.DocumentCaretContext[0].Path = "/other"
|
||||
case "foreign":
|
||||
result.DocumentPosition.Snapshot.Path = "/other"
|
||||
case "changed-text":
|
||||
result.DocumentPosition.Snapshot.Name = "Fresh public text"
|
||||
case "cached-ancestor":
|
||||
handler.state.current.SpeechContext = []atspi.Snapshot{{Role: "password text"}}
|
||||
case "fresh-ancestor":
|
||||
result.DocumentPosition.Snapshot.SpeechContext = []atspi.Snapshot{{Role: "password text"}}
|
||||
case "failure":
|
||||
result.Err = errors.New("provider unavailable")
|
||||
}
|
||||
handler.handle(result)
|
||||
return <-queue.requests
|
||||
}
|
||||
|
||||
func TestRemoteNativeWhereAmISourcePrivacy(t *testing.T) {
|
||||
for _, privacy := range []string{"ordinary", "password", "ancestor", "unknown", "invalid", "defunct", "missing", "truncated", "mismatched", "foreign", "changed-text", "cached-ancestor", "fresh-ancestor", "failure"} {
|
||||
t.Run(privacy, func(t *testing.T) {
|
||||
request := native_where_am_i_request(t, privacy)
|
||||
if exportable_speech(request) != (privacy == "ordinary") {
|
||||
t.Fatalf("privacy %s produced exportable=%v", privacy, exportable_speech(request))
|
||||
}
|
||||
if !strings.Contains(request.Text, "Native colour.") || request.Cause != "where-am-i" {
|
||||
t.Fatal("privacy verification changed the local report", request.Text)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteNativeWhereAmIRetiresCapture(t *testing.T) {
|
||||
for _, change := range []string{"snapshot", "focus", "epoch", "lock", "handoff", "foreground"} {
|
||||
t.Run(change, func(t *testing.T) {
|
||||
handler, queue, result := native_where_am_i_handler(t)
|
||||
switch change {
|
||||
case "snapshot":
|
||||
handler.state.setCurrent(handler.state.current)
|
||||
case "focus":
|
||||
handler.objectReview.SyncFocus(atspi.NewObjectID(result.DocumentFocus.BusName, "/other"))
|
||||
case "epoch":
|
||||
handler.objectReview.ResetDocument(result.DocumentFocus)
|
||||
case "lock":
|
||||
handler.focusGate.SetLocked(true, nil, nil, nil, nil, &brailleWorker{})
|
||||
case "handoff":
|
||||
handler.focusGate.SetSuspended(true)
|
||||
case "foreground":
|
||||
handler.syncForeground = func() { handler.focusGate.SetSuspended(true) }
|
||||
}
|
||||
handler.handle(result)
|
||||
if len(queue.requests) != 0 {
|
||||
t.Fatal("retired native Where Am I capture spoke")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type whereAmICapturer struct {
|
||||
*fakeAdapter
|
||||
content atspi.DocumentContent
|
||||
}
|
||||
|
||||
type whereAmIObservationQueue struct {
|
||||
*fakeSpeechQueue
|
||||
active bool
|
||||
}
|
||||
|
||||
func (queue *whereAmIObservationQueue) remote_observation_active() bool { return queue.active }
|
||||
|
||||
func (capturer *whereAmICapturer) ReadFocusedSnapshot(context.Context, atspi.ObjectID) (atspi.DocumentContent, error) {
|
||||
return capturer.content, nil
|
||||
}
|
||||
|
||||
func TestRemoteNativeWhereAmIQueuesCapture(t *testing.T) {
|
||||
handler, queue, result := native_where_am_i_handler(t)
|
||||
capturer := &whereAmICapturer{fakeAdapter: &fakeAdapter{}, content: atspi.DocumentContent{
|
||||
Snapshot: result.DocumentPosition.Snapshot, SourceContext: result.DocumentCaretContext}}
|
||||
worker, err := review.NewWorker(capturer, time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
worker.SyncFocus(result.DocumentFocus)
|
||||
handler.objectReview = worker
|
||||
observedQueue := &whereAmIObservationQueue{fakeSpeechQueue: queue}
|
||||
handler.speech = observedQueue
|
||||
handler.whereAmI()
|
||||
if request := <-queue.requests; exportable_speech(request) {
|
||||
t.Fatal("inactive observation authorized native export")
|
||||
}
|
||||
<-queue.spoken
|
||||
observedQueue.active = true
|
||||
handler.whereAmI()
|
||||
if len(queue.requests) != 0 {
|
||||
t.Fatal("native capture bypassed the review worker")
|
||||
}
|
||||
if err := worker.Start(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { worker.Stop(context.Background()) })
|
||||
select {
|
||||
case captured := <-worker.Results():
|
||||
handler.handle(captured)
|
||||
if request := <-queue.requests; !exportable_speech(request) {
|
||||
t.Fatal("ordinary native report stayed private after fresh verification")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("native report did not complete")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteNativeWhereAmIResolvedOutput(t *testing.T) {
|
||||
worker, session, _, _ := observation_fixture(t)
|
||||
if !worker.remote_observation_active() {
|
||||
t.Fatal("live software observer did not enable source verification")
|
||||
}
|
||||
worker.textTransform = func(text string) string { return strings.ReplaceAll(text, "colour", "color") }
|
||||
for _, privacy := range []string{"ordinary", "ancestor", "changed-text", "failure"} {
|
||||
if err := worker.EnqueueAndWait(t.Context(), native_where_am_i_request(t, privacy)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for len(worker.speaker.(*observationSpeaker).spoken) > 0 {
|
||||
want := <-worker.speaker.(*observationSpeaker).spoken
|
||||
if privacy == "ordinary" {
|
||||
value := observed_speech(t, session)
|
||||
if value.Kind != "speak" || len(value.Parts) != 2 || value.Parts[0].Text != want || !value.Parts[1].End {
|
||||
t.Fatal("native Where Am I export lost resolved text or chunks", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
worker.InterruptForUser()
|
||||
if value := observed_speech(t, session); value.Kind != "cancel" {
|
||||
t.Fatal("private native report reached the child", value)
|
||||
}
|
||||
}
|
||||
session.RevokeRemote()
|
||||
if worker.remote_observation_active() {
|
||||
t.Fatal("revoked observer still enabled native verification")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user