Enable isolated Remote speech and protect reading exports
Attach incoming software speech to reader sessions with lease checks, local output priority and isolated cancellation. Preserve every document fragment source for privacy checks and export reviewed browse current-line reading, spelling and phonetic output. Verified focused reading and Remote race tests, complete review and external Remote race suites, private relay integration, speech regressions, affected-package vet, reader build and Tentacles make check. Audible speech, physical input/Braille and live interoperability remain unverified.
This commit is contained in:
1 parent
ac67236eca
commit
49be8309fe
27 files changed
+986
-54
No files matched your search
@@ -32,7 +32,7 @@ import (
|
||||
|
||||
// This uses the source-built external executable and real host/SDK IPC. Only
|
||||
// OpenLocal submissions are synthetic; it cannot prove physical-source input.
|
||||
func remote_command_fixture(t *testing.T) (*tentacleRuntime, *consentTestForms, input.Command) {
|
||||
func remote_command_fixture(t *testing.T, setup ...func(*tentacleRuntime)) (*tentacleRuntime, *consentTestForms, input.Command) {
|
||||
t.Helper()
|
||||
program := os.Getenv("CTHULHU_REMOTE_BINARY")
|
||||
if program == "" {
|
||||
@@ -53,6 +53,9 @@ func remote_command_fixture(t *testing.T) (*tentacleRuntime, *consentTestForms,
|
||||
runtime.newManager = func() (*tentacle.Manager, error) { return tentacle.NewManager(tentacle.DefaultLimits(), nil) }
|
||||
forms := &consentTestForms{requests: make(chan consentFormRequest, 1)}
|
||||
runtime.forms = forms
|
||||
for _, configure := range setup {
|
||||
configure(runtime)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
t.Cleanup(func() {
|
||||
cancel()
|
||||
@@ -224,15 +227,25 @@ func TestRemoteConnectionCommandCancelsInspectionOnHandoff(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
for _, test := range []struct{ role, ending string }{
|
||||
{"controller", "disconnect"}, {"controlled", "disconnect"},
|
||||
{"controller", "lock"}, {"controlled", "lock"},
|
||||
{"controller", "handoff"}, {"controlled", "handoff"},
|
||||
{"controller", "disable"}, {"controlled", "disable"},
|
||||
for _, test := range []struct {
|
||||
role, ending string
|
||||
playback bool
|
||||
}{
|
||||
{"controller", "disconnect", false}, {"controlled", "disconnect", false},
|
||||
{"controller", "lock", false}, {"controlled", "lock", false},
|
||||
{"controller", "handoff", false}, {"controlled", "handoff", false},
|
||||
{"controller", "disable", false}, {"controlled", "disable", false},
|
||||
{"controller", "disconnect", true}, {"controller", "lock", true},
|
||||
{"controller", "handoff", true}, {"controller", "disable", true},
|
||||
{"controller", "backend-failure", true},
|
||||
} {
|
||||
role, ending := test.role, test.ending
|
||||
t.Run(role+"/"+ending, func(t *testing.T) {
|
||||
remoteText := " " + strings.Repeat("é\x00世界 ", 1500) + " "
|
||||
name := role + "/" + ending
|
||||
if test.playback {
|
||||
name += "/production-speech"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
remoteText := " " + strings.Repeat("é\t世界 ", 1500) + " "
|
||||
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -248,6 +261,7 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
}
|
||||
defer listener.Close()
|
||||
probed, joined, closed := make(chan struct{}), make(chan struct{}), make(chan struct{})
|
||||
speechReady := make(chan struct{})
|
||||
relayOutput := make(chan []byte, 64)
|
||||
go func() {
|
||||
probe, err := listener.Accept()
|
||||
@@ -290,14 +304,19 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
peerRole = "slave"
|
||||
}
|
||||
io.WriteString(connection, `{"type":"channel_joined","channel":"synthetic-fixture","clients":[{"id":7,"connection_type":"`+peerRole+`"}]}`+"\n")
|
||||
close(joined)
|
||||
if role == "controller" {
|
||||
select {
|
||||
case <-speechReady:
|
||||
case <-t.Context().Done():
|
||||
return
|
||||
}
|
||||
json.NewEncoder(connection).Encode(map[string]any{"type": "speak", "origin": 7, "priority": 2, "sequence": []any{
|
||||
[]any{"CharacterModeCommand", map[string]any{"state": true}},
|
||||
[]any{"LangChangeCommand", map[string]any{"lang": "fr"}}, remoteText,
|
||||
[]any{"EndUtteranceCommand", map[string]any{}},
|
||||
}})
|
||||
}
|
||||
close(joined)
|
||||
for {
|
||||
frame, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
@@ -307,7 +326,29 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
}
|
||||
close(closed)
|
||||
}()
|
||||
runtime, forms, shortcut := remote_command_fixture(t)
|
||||
var server *isolatedVoiceServer
|
||||
var voice *remoteSpeechVoice
|
||||
var rendered isolatedVoiceUtterance
|
||||
var configure []func(*tentacleRuntime)
|
||||
if test.playback {
|
||||
speaker, fixture := isolated_voice_fixture(t)
|
||||
server = fixture
|
||||
configure = append(configure, func(runtime *tentacleRuntime) {
|
||||
worker := runtime.output.speech
|
||||
worker.speaker = speaker
|
||||
if err := worker.Start(t.Context()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := worker.Stop(ctx); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
runtime, forms, shortcut := remote_command_fixture(t, configure...)
|
||||
if role == "controlled" {
|
||||
worker := runtime.output.speech
|
||||
worker.speaker = &observationSpeaker{fakeSpeaker{spoken: make(chan string, 16)}}
|
||||
@@ -323,10 +364,10 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
// Incoming speech still needs a test owner; production observation
|
||||
// is attached through makeManager on the controlled side.
|
||||
// Keep the explicit mailbox assembly fixture alongside production
|
||||
// playback, which is attached by the actual runtime startup path.
|
||||
var deliveries <-chan tentacle.RemoteSpeechDelivery
|
||||
if role == "controller" {
|
||||
if role == "controller" && !test.playback {
|
||||
runtime.mutex.Lock()
|
||||
for _, command := range runtime.commands {
|
||||
if command.id == "remote" {
|
||||
@@ -371,13 +412,74 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
if status.ctx.Err() == nil {
|
||||
t.Fatal("background connection retained its form context")
|
||||
}
|
||||
if test.playback {
|
||||
waitTentacle(t, func() bool {
|
||||
worker := runtime.output.speech
|
||||
worker.mutex.Lock()
|
||||
defer worker.mutex.Unlock()
|
||||
return worker.activeDone == nil && len(worker.pending) == 0
|
||||
})
|
||||
if ending == "backend-failure" {
|
||||
server.mutex.Lock()
|
||||
server.queryReplies = map[string]string{"GET LANGUAGE": "400 Rejected\r\n"}
|
||||
server.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
close(speechReady)
|
||||
if ending == "backend-failure" {
|
||||
remote_stream_wait(t, closed)
|
||||
// Backend failure retires the lease, while the process and
|
||||
// mailbox owner remain usable for an explicit new connection.
|
||||
runtime.Invoke(t.Context(), shortcut)
|
||||
selection := next_remote_form(t, forms)
|
||||
if selection.form.Title == "Remote relay status" {
|
||||
// An ended attempt remains inspectable until explicitly
|
||||
// closed, as with ordinary transport failure.
|
||||
selection.reply <- internalUIResult{Activated: "cancel"}
|
||||
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
|
||||
runtime.Invoke(t.Context(), shortcut)
|
||||
selection = next_remote_form(t, forms)
|
||||
}
|
||||
if !strings.Contains(selection.form.Title, "Remote relay and role") {
|
||||
t.Fatal("backend failure retained a connected workflow")
|
||||
}
|
||||
selection.reply <- internalUIResult{Cancelled: true}
|
||||
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
|
||||
server.mutex.Lock()
|
||||
for _, connection := range server.connections {
|
||||
if connection.priority == "TEXT" && connection.pending != "" {
|
||||
t.Error("failed rendering exposed a speech prefix")
|
||||
}
|
||||
}
|
||||
server.mutex.Unlock()
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-closed:
|
||||
t.Fatal("return to desktop disconnected the relay")
|
||||
default:
|
||||
}
|
||||
var delivery tentacle.RemoteSpeechDelivery
|
||||
if role == "controller" {
|
||||
if test.playback {
|
||||
utterance := server.next_utterance(t)
|
||||
for utterance.priority == "MESSAGE" {
|
||||
// Returning to desktop produces local status on its ordinary
|
||||
// client. The production Remote voice must stay separate.
|
||||
utterance = server.next_utterance(t)
|
||||
}
|
||||
rendered = utterance
|
||||
if utterance.text != remoteText || utterance.language != "fr" || utterance.spelling != "on" ||
|
||||
utterance.ssml != "off" || utterance.priority != "TEXT" {
|
||||
t.Fatal("production owner lost literal speech, metadata or local priority")
|
||||
}
|
||||
speaker := runtime.output.speech.speaker.(*configuredSpeaker)
|
||||
speaker.mu.RLock()
|
||||
voice = speaker.remote
|
||||
speaker.mu.RUnlock()
|
||||
if voice == nil {
|
||||
t.Fatal("production owner did not acquire isolated synthesis")
|
||||
}
|
||||
} else if role == "controller" {
|
||||
select {
|
||||
case delivery = <-deliveries:
|
||||
case <-time.After(5 * time.Second):
|
||||
@@ -448,6 +550,28 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
if err := worker.EnqueueAndWait(t.Context(), request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request = current_line_observation_request(t, review.Result{CurrentLineBrowse: true,
|
||||
DocumentCaretText: "Current colour.", DocumentPosition: review.DocumentPosition{Snapshot: atspi.Snapshot{Role: "static"}},
|
||||
DocumentCaretContext: []atspi.Snapshot{{Role: "static"}, {Role: "paragraph"}, {Role: "document web"}}})
|
||||
if err := worker.EnqueueAndWait(t.Context(), request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A joined line can contain a private neighboring fragment even
|
||||
// when its caret source and the line's presentation are ordinary.
|
||||
caret := atspi.Snapshot{Role: "static"}
|
||||
privateLine := atspi.DocumentLine{Contents: []atspi.DocumentContent{{Snapshot: caret, Text: "private joined text"}},
|
||||
SourceContexts: [][]atspi.Snapshot{{caret}, {{Role: "static"}, {Role: "password text"}}}}
|
||||
for _, kind := range []string{"caret", "read-all", "current-line"} {
|
||||
request := document_observation_request(t, kind, review.Result{
|
||||
Moved: true, DocumentCaretCommand: review.CaretNextLine,
|
||||
DocumentPosition: review.DocumentPosition{Snapshot: caret}, DocumentCaretText: "private joined text",
|
||||
DocumentCaretContext: []atspi.Snapshot{caret, {Role: "document web"}},
|
||||
DocumentCaretLine: &privateLine, ReadAllContext: t.Context(), ReadAllLines: []atspi.DocumentLine{privateLine},
|
||||
})
|
||||
if err := worker.EnqueueAndWait(t.Context(), request); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
worker.InterruptForUser()
|
||||
var received strings.Builder
|
||||
ends, frames := 0, 0
|
||||
@@ -483,7 +607,7 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
t.Fatal("reader output did not reach the relay in order")
|
||||
}
|
||||
}
|
||||
if received.String() != "color "+remoteText+"First color.Second color." || frames < 4 || ends != 3 {
|
||||
if received.String() != "color "+remoteText+"First color.Second color.Current color." || frames < 5 || ends != 4 {
|
||||
t.Fatal("observation leaked private text or lost resolved output/boundaries")
|
||||
}
|
||||
}
|
||||
@@ -512,7 +636,15 @@ func TestRemoteConnectionCommandPrivateRelay(t *testing.T) {
|
||||
t.Fatal("background connection retained the relay after " + ending)
|
||||
}
|
||||
waitTentacle(t, func() bool { return len(runtime.resultSlots) == 0 })
|
||||
if role == "controller" {
|
||||
if test.playback {
|
||||
remote_stream_wait(t, voice.closed)
|
||||
server.mutex.Lock()
|
||||
pending := server.connections[rendered.id].pending
|
||||
server.mutex.Unlock()
|
||||
if pending != "" {
|
||||
t.Fatal("revoked production speech survived backend cleanup")
|
||||
}
|
||||
} else if role == "controller" {
|
||||
// Socket cancellation precedes the final sequenced lease close.
|
||||
// A reopened status interaction did not issue the original consent,
|
||||
// so its result-slot completion is not the lease-close barrier.
|
||||
|
||||
Reference in new issue
Block a user