153 lines
5.6 KiB
Go
153 lines
5.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentaclewire
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRemoteSpeechSDKValidation(t *testing.T) {
|
|
cases := []struct {
|
|
data string
|
|
valid bool
|
|
}{
|
|
{`{"kind":"speak","parts":[{"text":" hello\t世界 ","characters":true,"language":"en_US"},{"end":true}]}`, true},
|
|
{`{"kind":"cancel"}`, true},
|
|
{`{"kind":"pause","paused":false}`, true},
|
|
{`{"kind":"pause","paused":true}`, true},
|
|
{`{"kind":"speak","priority":2,"parts":[{"text":"x"}]}`, true},
|
|
{`{"kind":"speak","priority":3,"parts":[{"text":"x"}]}`, false},
|
|
{`{"kind":"speak","priority":true,"parts":[{"text":"x"}]}`, false},
|
|
{`{"kind":"speak","parts":[]}`, false},
|
|
{`{"kind":"speak","parts":[{}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":null}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"x","end":true}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"x","language":"../en"}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"x","voice":"peer path"}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"x"}],"paused":false}`, false},
|
|
{`{"kind":"cancel","parts":null}`, false},
|
|
{`{"kind":"cancel","parts":[{"text":"x"}]}`, false},
|
|
{`{"kind":"cancel","paused":false}`, false},
|
|
{`{"kind":"pause"}`, false},
|
|
{`{"kind":"pause","paused":null}`, false},
|
|
{`{"kind":"pause","paused":0}`, false},
|
|
{`{"kind":"wave","path":"anything"}`, false},
|
|
{`{"kind":"cancel","priority":null}`, false},
|
|
{`{"kind":"cancel","extra":1}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"` + strings.Repeat("x", MaxRemoteSpeechBytes) + `"}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"` + strings.Repeat("<", 1100) + `"}]}`, false},
|
|
{`{"kind":"speak","parts":[{"text":"` + strings.Repeat("\u2028", 1100) + `"}]}`, false},
|
|
}
|
|
var pythonCases []any
|
|
for _, item := range cases {
|
|
var speech RemoteSpeech
|
|
valid := DecodeStrict([]byte(item.data), &speech) == nil && ValidRemoteSpeech(speech)
|
|
if valid != item.valid {
|
|
t.Fatalf("Go validation: %.100s: got %v", item.data, valid)
|
|
}
|
|
var value any
|
|
if err := json.Unmarshal([]byte(item.data), &value); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pythonCases = append(pythonCases, map[string]any{"speech": value, "valid": item.valid})
|
|
}
|
|
path, err := filepath.Abs("../../sdk/python")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
script := `import json, sys
|
|
sys.path.insert(0, sys.argv[1])
|
|
from cthulhu_tentacle.session import valid_remote_speech
|
|
for case in json.load(sys.stdin):
|
|
assert valid_remote_speech(case['speech']) == case['valid'], str(case)[:150]
|
|
`
|
|
data, _ := json.Marshal(pythonCases)
|
|
command := exec.Command("python3", "-B", "-c", script, path)
|
|
command.Stdin = strings.NewReader(string(data))
|
|
if output, err := command.CombinedOutput(); err != nil {
|
|
t.Fatalf("Python validation: %v: %s", err, output)
|
|
}
|
|
}
|
|
|
|
func TestRemoteSpeechEncodedBoundary(t *testing.T) {
|
|
value := RemoteSpeech{Kind: "speak", Parts: []RemoteSpeechPart{{Text: "x"}}}
|
|
data, _ := json.Marshal(value)
|
|
value.Parts[0].Text = strings.Repeat("x", MaxRemoteSpeechBytes-len(data)+1)
|
|
if !ValidRemoteSpeech(value) {
|
|
t.Fatal("rejected exact bound")
|
|
}
|
|
value.Parts[0].Text += "x"
|
|
if ValidRemoteSpeech(value) {
|
|
t.Fatal("accepted above bound")
|
|
}
|
|
value.Parts[0].Text = string([]byte{0xff})
|
|
if ValidRemoteSpeech(value) {
|
|
t.Fatal("accepted invalid UTF-8")
|
|
}
|
|
}
|
|
|
|
func TestRemoteSpeechPresentationSDKValidation(t *testing.T) {
|
|
base := `"lease":"` + strings.Repeat("a", 32) + `","epoch":1,"sequence":3,"speech":{"kind":"speak","parts":[{"text":"x"}]}`
|
|
cases := []struct {
|
|
fields string
|
|
valid bool
|
|
}{
|
|
{"", true},
|
|
{`,"utterance":3`, true},
|
|
{`,"utterance":1,"final":true`, true},
|
|
{`,"utterance":0,"final":false`, true},
|
|
{`,"utterance":4`, false},
|
|
{`,"utterance":-1`, false},
|
|
{`,"utterance":true`, false},
|
|
{`,"utterance":null`, false},
|
|
{`,"utterance":18446744073709551616`, false},
|
|
{`,"final":true`, false},
|
|
{`,"utterance":1,"final":1`, false},
|
|
{`,"utterance":1,"final":null`, false},
|
|
{`,"extra":1`, false},
|
|
}
|
|
var pythonCases []any
|
|
for _, item := range cases {
|
|
data := []byte(`{` + base + item.fields + `}`)
|
|
var request RemotePresentation
|
|
if valid := DecodeStrict(data, &request) == nil && ValidRemotePresentation(request); valid != item.valid {
|
|
t.Fatalf("Go presentation validation: %s: got %v", item.fields, valid)
|
|
}
|
|
// Use RawMessage when encoding the cases for Python, preserving uint64
|
|
// boundaries rather than passing them through float64.
|
|
pythonCases = append(pythonCases, map[string]any{"request": json.RawMessage(data), "valid": item.valid})
|
|
}
|
|
for _, kind := range []string{"cancel", "pause"} {
|
|
request := RemotePresentation{RemoteOperation: RemoteOperation{Lease: strings.Repeat("a", 32), Epoch: 1, Sequence: 1}, Utterance: 1, Speech: RemoteSpeech{Kind: kind}}
|
|
if kind == "pause" {
|
|
paused := true
|
|
request.Speech.Paused = &paused
|
|
}
|
|
if ValidRemotePresentation(request) {
|
|
t.Fatal("grouped non-speak effect")
|
|
}
|
|
pythonCases = append(pythonCases, map[string]any{"request": request, "valid": false})
|
|
}
|
|
path, err := filepath.Abs("../../sdk/python")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
script := `import json, sys
|
|
sys.path.insert(0, sys.argv[1])
|
|
from cthulhu_tentacle.session import valid_remote_presentation
|
|
for case in json.load(sys.stdin):
|
|
assert valid_remote_presentation(case['request']) == case['valid'], str(case)[:180]
|
|
`
|
|
data, _ := json.Marshal(pythonCases)
|
|
command := exec.Command("python3", "-B", "-c", script, path)
|
|
command.Stdin = strings.NewReader(string(data))
|
|
if output, err := command.CombinedOutput(); err != nil {
|
|
t.Fatalf("Python presentation validation: %v: %s", err, output)
|
|
}
|
|
}
|