144 lines
5.2 KiB
Go
144 lines
5.2 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentacle
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
)
|
|
|
|
func TestRemoteSpeechOutputSDKTicket(t *testing.T) {
|
|
for _, language := range []string{"go", "python"} {
|
|
t.Run(language, func(t *testing.T) {
|
|
manager := test_manager(t, DefaultLimits(), nil)
|
|
session := startSessionSDK(t, manager, language, "speech")
|
|
if manager.CaptureRemoteSpeechOutput() != nil {
|
|
t.Fatal("captured output without consent")
|
|
}
|
|
lease := speech_fixture_lease(t, session, "controller", []string{"remote-present"})
|
|
if manager.CaptureRemoteSpeechOutput() != nil {
|
|
t.Fatal("controller acquired observation")
|
|
}
|
|
session.RevokeRemote()
|
|
invokeSessionSDK(t, session, "event", nil) // Revocation.
|
|
lease = speech_fixture_lease(t, session, "controlled", []string{"output-observe"})
|
|
output := manager.CaptureRemoteSpeechOutput()
|
|
if output == nil {
|
|
t.Fatal("live controlled output unavailable")
|
|
}
|
|
text := " " + strings.Repeat("é\x00世界\t", 170) + " "
|
|
if err := output.PublishText(text, true); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var received strings.Builder
|
|
frames, ends := 0, 0
|
|
for ends == 0 {
|
|
var event tentaclewire.SessionEvent
|
|
if err := json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
frames++
|
|
if event.Kind != "remote.output" || event.Lease != lease.Lease || event.Epoch != lease.Epoch ||
|
|
event.Speech == nil || !tentaclewire.ValidRemoteSpeech(*event.Speech) {
|
|
t.Fatal("invalid observed event", event)
|
|
}
|
|
for _, part := range event.Speech.Parts {
|
|
if part.End {
|
|
ends++
|
|
} else {
|
|
if !part.Characters || part.Language != "" {
|
|
t.Fatal("changed character/default-language state")
|
|
}
|
|
received.WriteString(part.Text)
|
|
}
|
|
}
|
|
}
|
|
if frames < 2 || ends != 1 || received.String() != text {
|
|
t.Fatal("changed split text or boundaries")
|
|
}
|
|
if err := output.Cancel(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var event tentaclewire.SessionEvent
|
|
if json.Unmarshal(invokeSessionSDK(t, session, "event", nil), &event) != nil || event.Speech.Kind != "cancel" {
|
|
t.Fatal("lost cancellation order")
|
|
}
|
|
session.RevokeRemote()
|
|
invokeSessionSDK(t, session, "event", nil)
|
|
_ = speech_fixture_lease(t, session, "controlled", []string{"output-observe"})
|
|
if !errors.Is(output.PublishText("old", false), ErrPermission) || !errors.Is(output.Cancel(), ErrPermission) {
|
|
t.Fatal("old ticket followed replacement lease")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// No pipe writer: prove that a split output reserves all queue space before
|
|
// publishing a prefix, including encoded-byte and in-flight accounting.
|
|
func TestRemoteSpeechOutputAdmissionAndPolicy(t *testing.T) {
|
|
for _, mode := range []string{"slots", "bytes", "frames", "invalid", "policy", "expired", "pending"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
manager := test_manager(t, DefaultLimits(), nil)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
outputContext, revoke := context.WithCancel(ctx)
|
|
defer revoke()
|
|
session := &Session{manager: manager, spec: Spec{SessionEvents: true}, ctx: ctx, cancel: cancel,
|
|
outputContext: outputContext, revokeOutput: revoke, state: Active,
|
|
outbound: make(chan outgoing, 2), revoked: make(chan struct{})}
|
|
accessContext, retire := context.WithCancel(ctx)
|
|
defer retire()
|
|
session.remote = &remoteAccess{lease: strings.Repeat("a", 32), epoch: 1, role: "controlled",
|
|
grants: []string{"output-observe"}, ctx: accessContext, cancel: retire, deadline: time.Now().Add(time.Minute)}
|
|
manager.remoteOwner = session
|
|
output := manager.CaptureRemoteSpeechOutput()
|
|
text := strings.Repeat("x", 800)
|
|
want := ErrLimit
|
|
switch mode {
|
|
case "slots":
|
|
session.outbound <- outgoing{frame: []byte("existing")}
|
|
case "bytes":
|
|
session.queuedBytes = tentaclewire.SessionQueueBytes - 1
|
|
case "frames":
|
|
text = strings.Repeat("x", 768*tentaclewire.RemoteSpeechQueueSize+1)
|
|
case "invalid":
|
|
text = "\xff"
|
|
case "policy":
|
|
manager.remotePolicy = NewRemotePolicy()
|
|
session.remote.ctx, session.remote.cancel = context.WithCancel(manager.remotePolicy.Context())
|
|
output.ctx = session.remote.ctx
|
|
defer session.remote.cancel()
|
|
manager.remotePolicy.SetAvailable(false)
|
|
manager.remotePolicy.SetAvailable(true)
|
|
want = ErrPermission
|
|
case "expired":
|
|
session.remote.deadline = time.Now().Add(-time.Second)
|
|
want = ErrPermission
|
|
case "pending":
|
|
session.remote.lease = ""
|
|
want = ErrPermission
|
|
}
|
|
before := len(session.outbound)
|
|
if err := output.PublishText(text, false); !errors.Is(err, want) {
|
|
t.Fatalf("publish = %v, want %v", err, want)
|
|
}
|
|
if len(session.outbound) != before || session.eventSequence != 0 {
|
|
t.Fatal("failed admission published a prefix or consumed event sequence")
|
|
}
|
|
if want == ErrLimit && (session.state != Failed || accessContext.Err() == nil) {
|
|
t.Fatal("overflow retained live stream")
|
|
}
|
|
if want == ErrPermission && manager.CaptureRemoteSpeechOutput() != nil {
|
|
t.Fatal("captured invalid output authority")
|
|
}
|
|
})
|
|
}
|
|
}
|