Files

135 lines
6.4 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package tentaclewire
import (
"encoding/json"
"fmt"
"git.stormux.org/storm/cthulhu/internal/config"
"git.stormux.org/storm/cthulhu/internal/interaction"
"math"
"reflect"
"testing"
)
func reader_test_extensions() Extensions {
return Extensions{
Pages: []PreferencePage{{ID: "options", Title: "Reader probe", Command: "apply", Elements: []interaction.Element{
{ID: "text", Label: "Text", Kind: interaction.Entry, Required: true, MaxLength: 32},
{ID: "enabled", Label: "Enabled", Kind: interaction.CheckBox},
{ID: "choice", Label: "Choice", Kind: interaction.List, Items: []interaction.Item{{ID: "first", Label: "First"}, {ID: "second", Label: "Second", Disabled: true}}},
}, Values: map[string]interaction.Value{"text": {Text: "Initial"}, "enabled": {}, "choice": {Selected: "first"}}}},
QuickSettings: []QuickSetting{{ID: "choice", Label: "Probe choice", Command: "apply", Options: []interaction.Item{{ID: "default", Label: "Default"}, {ID: "custom", Label: "Custom"}}, Selected: "default"}},
}
}
func TestReaderPageCanDescribeSixCompleteVoiceConfigurations(t *testing.T) {
page := PreferencePage{ID: "voices", Title: "Voice configurations", Command: "apply", Values: map[string]interaction.Value{}}
for profile := 1; profile <= 6; profile++ {
for field := 1; field <= 24; field++ {
id := fmt.Sprintf("profile%d.field%d", profile, field)
page.Elements = append(page.Elements, interaction.Element{ID: id, Label: fmt.Sprintf("Profile %d setting %d", profile, field), Kind: interaction.Entry, MaxLength: 64})
page.Values[id] = interaction.Value{Text: "Example"}
}
}
if err := ValidateExtensions(Extensions{Pages: []PreferencePage{page}}); err != nil {
t.Fatal(err)
}
if _, err := interaction.NewSession(interaction.Form{Title: page.Title, Elements: page.Elements}); err == nil {
t.Fatal("native page limit leaked into internal forms")
}
state := ReaderState{Extensions: Extensions{Pages: []PreferencePage{page}}, Speech: SpeechFromSettings(config.DefaultSettings()), DefaultSpeech: SpeechFromSettings(config.DefaultSettings())}
data, err := json.Marshal(ExtensionEvent{Event: "preferences.apply", ID: page.ID, Values: page.Values, Reader: state})
if err != nil || len(data) > 48*1024 {
t.Fatal("six configurations exceed callback budget", len(data), err)
}
}
func TestReaderExtensionsRejectInvalidSchemasAndValues(t *testing.T) {
mutations := map[string]func(*Extensions){
"duplicate page": func(v *Extensions) { v.Pages = append(v.Pages, v.Pages[0]) },
"duplicate setting": func(v *Extensions) { v.QuickSettings = append(v.QuickSettings, v.QuickSettings[0]) },
"duplicate control": func(v *Extensions) { v.Pages[0].Elements = append(v.Pages[0].Elements, v.Pages[0].Elements[0]) },
"button": func(v *Extensions) { v.Pages[0].Elements[0].Kind = interaction.Button },
"bad label": func(v *Extensions) { v.Pages[0].Title = "Probe\npage" },
"bad identifier": func(v *Extensions) { v.Pages[0].ID = "../page" },
"missing value": func(v *Extensions) { delete(v.Pages[0].Values, "text") },
"extra value": func(v *Extensions) { v.Pages[0].Values["other"] = interaction.Value{} },
"wrong value type": func(v *Extensions) { v.Pages[0].Values["enabled"] = interaction.Value{Text: "yes"} },
"required": func(v *Extensions) { v.Pages[0].Values["text"] = interaction.Value{} },
"oversized": func(v *Extensions) {
v.Pages[0].Values["text"] = interaction.Value{Text: "This string is longer than thirty-two characters"}
},
"control character": func(v *Extensions) { v.Pages[0].Values["text"] = interaction.Value{Text: "bad\x1bvalue"} },
"disabled choice": func(v *Extensions) { v.Pages[0].Values["choice"] = interaction.Value{Selected: "second"} },
"unknown quick choice": func(v *Extensions) { v.QuickSettings[0].Selected = "missing" },
"disabled quick choice": func(v *Extensions) { v.QuickSettings[0].Options[0].Disabled = true },
}
if err := ValidateExtensions(reader_test_extensions()); err != nil {
t.Fatal(err)
}
for name, mutate := range mutations {
t.Run(name, func(t *testing.T) {
value := reader_test_extensions()
mutate(&value)
if ValidateExtensions(value) == nil {
t.Fatal("accepted invalid extension")
}
})
}
}
func TestReaderSpeechConfigurationRoundTripAndIsolation(t *testing.T) {
settings := config.DefaultSettings()
rate, pitch, gain := 72.0, 6.2, 8.1
settings.DefaultVoice = config.Voice{Rate: &rate, AveragePitch: &pitch, Gain: &gain, Family: &config.VoiceFamily{Name: "Example voice", Lang: "en", Dialect: "US", Variant: "sample"}}
settings.UppercaseVoice = config.Voice{AveragePitch: &pitch}
settings.HyperlinkVoice = config.Voice{Rate: &rate}
settings.SystemVoice = config.Voice{Gain: &gain}
settings.UseCustomEchoVoice = true
settings.EchoVoice = config.Voice{Rate: &rate}
base := config.DefaultSettings()
base.EnableSpeech = false
base.EnableBraille = true
result := SpeechFromSettings(settings).Apply(base)
if !reflect.DeepEqual(SpeechFromSettings(result), SpeechFromSettings(settings)) {
t.Fatal("voice settings lost")
}
if result.EnableSpeech || !result.EnableBraille || result.KeyboardLayout != base.KeyboardLayout {
t.Fatal("unrelated settings changed")
}
value := SpeechFromSettings(settings)
value.DefaultVoice.Rate = new(float64)
*value.DefaultVoice.Rate = math.NaN()
if ValidateReaderRequest(ReaderRequest{Speech: &value}) == nil {
t.Fatal("accepted NaN")
}
}
func TestReaderInteractionValidation(t *testing.T) {
for _, raw := range []string{
`{"reader":{"snapshot":true},"next":"receive"}`,
`{"reader":{"restore_speech":true}}`,
`{"reader":{"extensions":{}}}`,
} {
if _, err := DecodeInteraction([]byte(raw)); err != nil {
t.Fatal(raw, err)
}
}
for _, raw := range []string{
`{"reader":{"snapshot":true}}`,
`{"reader":{"snapshot":true,"restore_speech":true},"next":"receive"}`,
`{"reader":{"restore_speech":true},"clipboard":"value"}`,
`{"reader":{"restore_speech":true},"done":true}`,
`{"reader":{"module":"example"}}`,
`{"reader":{"restore_speech":true,"unknown":true}}`,
`{"reader":null}`,
} {
if _, err := DecodeInteraction([]byte(raw)); err == nil {
t.Fatal("accepted", raw)
}
}
value := reader_test_extensions()
data, _ := json.Marshal(Interaction{Reader: &ReaderRequest{Extensions: &value}})
if _, err := DecodeInteraction(data); err != nil {
t.Fatal(err)
}
}