507 lines
20 KiB
Go
507 lines
20 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentacle
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
sdk "git.stormux.org/storm/cthulhu/sdk/go/tentacle"
|
|
)
|
|
|
|
func TestSessionSDKChild(t *testing.T) {
|
|
if len(os.Args) < 2 || os.Args[len(os.Args)-1] != "session-sdk-child" {
|
|
return
|
|
}
|
|
client := sdk.NewSessionClient()
|
|
commands := []sdk.Command{
|
|
{ID: "large", Title: "Large", Handle: func(ctx context.Context, args json.RawMessage) (any, error) { return strings.Repeat("x", 48*1024), nil }},
|
|
{ID: "request", Title: "Request", Handle: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
var request struct {
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
if err := json.Unmarshal(args, &request); err != nil {
|
|
return nil, err
|
|
}
|
|
var value json.RawMessage
|
|
err := client.Call(ctx, request.Method, request.Params, &value)
|
|
var failure *sdk.SessionError
|
|
if errors.As(err, &failure) {
|
|
return map[string]any{"code": failure.Code}, nil
|
|
}
|
|
return map[string]any{"value": value}, err
|
|
}},
|
|
{ID: "event", Title: "Event", Handle: func(ctx context.Context, args json.RawMessage) (any, error) {
|
|
select {
|
|
case event := <-client.Events():
|
|
return event, nil
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}},
|
|
{ID: "wait", Title: "Wait", Handle: func(ctx context.Context, args json.RawMessage) (any, error) { <-ctx.Done(); return nil, ctx.Err() }},
|
|
}
|
|
if sdk.ServeSession(os.Stdin, os.Stdout, commands, nil, client) != nil {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
|
|
func sessionSDKProgram(t *testing.T, language string) []string {
|
|
t.Helper()
|
|
if language == "go" {
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return []string{executable, "-test.run=^TestSessionSDKChild$", "--", "session-sdk-child"}
|
|
}
|
|
python, err := exec.LookPath("python3")
|
|
if err != nil {
|
|
t.Fatal("Python is required for session SDK conformance")
|
|
}
|
|
peer, err := filepath.Abs("testdata/session_peer.py")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return []string{python, "-B", peer}
|
|
}
|
|
|
|
func startSessionSDK(t *testing.T, manager *Manager, language, id string, extraGrants ...string) *Session {
|
|
t.Helper()
|
|
grants := append([]string{"output-observe", "remote-present", "clipboard-exchange"}, extraGrants...)
|
|
session, err := manager.Start(t.Context(), Spec{ID: id, Program: sessionSDKProgram(t, language), SessionEvents: true, RemoteGrants: grants})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_signal(t, session.Ready())
|
|
if err := session.Activate(); err != nil {
|
|
state, reason := session.Status()
|
|
t.Fatalf("activate: %v, %s %v", err, state, reason)
|
|
}
|
|
return session
|
|
}
|
|
|
|
func invokeSessionSDK(t *testing.T, session *Session, command string, params any) json.RawMessage {
|
|
t.Helper()
|
|
data, err := json.Marshal(params)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pending, err := session.Invoke(t.Context(), command, data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := wait_result(t, pending)
|
|
if result.Err != nil {
|
|
t.Fatal(result.Err)
|
|
}
|
|
return result.Value
|
|
}
|
|
|
|
func remoteCall(t *testing.T, session *Session, method string, params any) (json.RawMessage, int) {
|
|
t.Helper()
|
|
raw := invokeSessionSDK(t, session, "request", map[string]any{"method": method, "params": params})
|
|
var result struct {
|
|
Value json.RawMessage `json:"value"`
|
|
Code int `json:"code"`
|
|
}
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result.Value, result.Code
|
|
}
|
|
|
|
func TestRemoteWorkflowEpochCleanup(t *testing.T) {
|
|
manager, err := NewManager(DefaultLimits(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := manager.Shutdown(ctx); err != nil {
|
|
t.Error(err)
|
|
}
|
|
})
|
|
session := startSessionSDK(t, manager, "go", "workflow")
|
|
first, err := session.AuthorizeRemote("controller", []string{"remote-present"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := session.AuthorizeRemote("controller", []string{"remote-present"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.RevokeRemoteEpoch(first.Epoch)
|
|
raw, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: second.Action, Epoch: second.Epoch, Role: second.Role, Grants: second.Grants})
|
|
var lease tentaclewire.RemoteLease
|
|
if code != 0 || json.Unmarshal(raw, &lease) != nil {
|
|
t.Fatal("old workflow revoked its replacement")
|
|
}
|
|
delivery, err := session.RemoteContext(lease, "remote-present")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.RevokeRemoteEpoch(second.Epoch)
|
|
if delivery.Err() == nil {
|
|
t.Fatal("workflow close retained its live lease")
|
|
}
|
|
// Pending actions also release exclusive ownership on close, before their
|
|
// timeout and without requiring a remote.open/remote.close pair.
|
|
pending, err := session.AuthorizeRemote("controller", []string{"remote-present"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.RevokeRemoteEpoch(pending.Epoch)
|
|
other := startSessionSDK(t, manager, "go", "other-workflow")
|
|
if _, err := other.AuthorizeRemote("controlled", []string{"output-observe"}); err != nil {
|
|
t.Fatal("closed setup retained exclusive ownership", err)
|
|
}
|
|
}
|
|
|
|
func openRemote(t *testing.T, session *Session) tentaclewire.RemoteLease {
|
|
t.Helper()
|
|
action, err := session.AuthorizeRemote("controlled", []string{"output-observe"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw := invokeSessionSDK(t, session, "event", nil)
|
|
var event tentaclewire.SessionEvent
|
|
if json.Unmarshal(raw, &event) != nil || event.Action != action.Action || event.Kind != "remote.action" {
|
|
t.Fatal("lost local action", string(raw))
|
|
}
|
|
value, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: event.Action, Epoch: event.Epoch, Role: event.Role, Grants: event.Grants})
|
|
var lease tentaclewire.RemoteLease
|
|
if code != 0 || json.Unmarshal(value, &lease) != nil || lease.Lease == "" || lease.Epoch != event.Epoch {
|
|
t.Fatal("open", code, string(value))
|
|
}
|
|
return lease
|
|
}
|
|
|
|
func TestSessionSDKLifecycleConformance(t *testing.T) {
|
|
for _, language := range []string{"go", "python"} {
|
|
t.Run(language, func(t *testing.T) {
|
|
limits := DefaultLimits()
|
|
limits.RequestTimeout = 5 * time.Second
|
|
manager := test_manager(t, limits, nil)
|
|
session := startSessionSDK(t, manager, language, "session-sdk")
|
|
if _, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: strings.Repeat("a", 32), Epoch: 1, Role: "controlled", Grants: []string{"output-observe"}}); code != -32001 {
|
|
t.Fatal("child minted consent", code)
|
|
}
|
|
if _, err := session.AuthorizeRemote("controller", []string{"keyboard-capture"}); !errors.Is(err, ErrPermission) {
|
|
t.Fatal("ungranted approval", err)
|
|
}
|
|
if _, err := session.AuthorizeRemote("controller", []string{"output-observe"}); !errors.Is(err, ErrPermission) {
|
|
t.Fatal("wrong role", err)
|
|
}
|
|
lease := openRemote(t, session)
|
|
delivery, err := session.RemoteContext(lease, "output-observe")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := session.RemoteContext(lease, "clipboard-exchange"); !errors.Is(err, ErrPermission) {
|
|
t.Fatal("install grant became session grant")
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.key", map[string]any{}); code != -32602 {
|
|
t.Fatal("malformed typed key operation accepted", code)
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.braille", map[string]any{}); code != -32601 {
|
|
t.Fatal("unimplemented desktop operation accepted", code)
|
|
}
|
|
// One blocked handler cannot block the endpoint receive loop or lifecycle calls.
|
|
waiting, err := session.Invoke(t.Context(), "wait", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
heartbeat := tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: 1}
|
|
if _, code := remoteCall(t, session, "remote.heartbeat", heartbeat); code != 0 {
|
|
t.Fatal("heartbeat", code)
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.heartbeat", heartbeat); code != -32001 {
|
|
t.Fatal("duplicate operation accepted")
|
|
}
|
|
heartbeat.Sequence++
|
|
if _, code := remoteCall(t, session, "remote.close", heartbeat); code != 0 {
|
|
t.Fatal("close", code)
|
|
}
|
|
if delivery.Err() == nil {
|
|
t.Fatal("buffered delivery survived close")
|
|
}
|
|
var event tentaclewire.SessionEvent
|
|
raw := invokeSessionSDK(t, session, "event", nil)
|
|
if json.Unmarshal(raw, &event) != nil || event.Kind != "remote.revoked" || event.Reason != "closed" {
|
|
t.Fatal("missing revocation", string(raw))
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.close", heartbeat); code != 0 {
|
|
t.Fatal("close not idempotent")
|
|
}
|
|
newLease := openRemote(t, session)
|
|
if newLease.Epoch <= lease.Epoch {
|
|
t.Fatal("epoch reused")
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.heartbeat", heartbeat); code != -32001 {
|
|
t.Fatal("stale heartbeat accepted")
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.close", heartbeat); code != 0 {
|
|
t.Fatal("stale close failed")
|
|
}
|
|
current, err := session.RemoteContext(newLease, "output-observe")
|
|
if err != nil {
|
|
t.Fatal("stale close revoked new lease")
|
|
}
|
|
session.Disable()
|
|
if current.Err() == nil {
|
|
t.Fatal("disable left lease live")
|
|
}
|
|
if result := wait_result(t, waiting); result.Err == nil {
|
|
t.Fatal("blocked handler survived disable")
|
|
}
|
|
wait_signal(t, session.Done())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionLeaseExpiryAndExclusiveOwner(t *testing.T) {
|
|
limits := DefaultLimits()
|
|
limits.RequestTimeout = 5 * time.Second
|
|
manager := test_manager(t, limits, nil)
|
|
first := startSessionSDK(t, manager, "go", "first")
|
|
second := startSessionSDK(t, manager, "python", "second")
|
|
lease := openRemote(t, first)
|
|
delivery, err := first.RemoteContext(lease, "output-observe")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := second.AuthorizeRemote("controlled", []string{"output-observe"}); !errors.Is(err, ErrBusy) {
|
|
t.Fatal("two owners", err)
|
|
}
|
|
select {
|
|
case <-delivery.Done():
|
|
case <-time.After(RemoteHeartbeatTimeout + time.Second):
|
|
t.Fatal("heartbeat did not expire")
|
|
}
|
|
if _, code := remoteCall(t, first, "remote.heartbeat", tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: 1}); code != -32001 {
|
|
t.Fatal("expired lease revived")
|
|
}
|
|
openRemote(t, second)
|
|
}
|
|
|
|
func TestSessionSDKCallTimeout(t *testing.T) {
|
|
for _, blockedWriter := range []bool{false, true} {
|
|
for _, language := range []string{"go", "python"} {
|
|
t.Run(language, func(t *testing.T) {
|
|
program := sessionSDKProgram(t, language)
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
command := exec.CommandContext(ctx, program[0], program[1:]...)
|
|
input, err := command.StdinPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
output, err := command.StdoutPipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := command.Start(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer input.Close()
|
|
writer := json.NewEncoder(input)
|
|
reader := bufio.NewReader(output)
|
|
writer.Encode(map[string]any{"jsonrpc": "2.0", "id": "1", "method": "initialize", "params": Initialization{Protocol: Version{1, 1}, Session: "test", APIs: []string{"commands", "session-events"}, Capabilities: []string{}}})
|
|
if _, err := read_frame(reader); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer.Encode(map[string]any{"jsonrpc": "2.0", "method": "initialized", "params": map[string]string{"session": "test"}})
|
|
requestID := "2"
|
|
if blockedWriter {
|
|
writer.Encode(map[string]any{"jsonrpc": "2.0", "id": "2", "method": "command.invoke", "params": map[string]any{"command": "large", "arguments": nil}})
|
|
requestID = "3"
|
|
}
|
|
writer.Encode(map[string]any{"jsonrpc": "2.0", "id": requestID, "method": "command.invoke", "params": map[string]any{"command": "request", "arguments": map[string]any{"method": "remote.heartbeat", "params": map[string]any{}}}})
|
|
if !blockedWriter {
|
|
frame, err := read_frame(reader)
|
|
if err != nil || !strings.Contains(string(frame), `"id":"c:1"`) {
|
|
t.Fatal("missing child request", string(frame), err)
|
|
}
|
|
}
|
|
// Keep stdin open and never reply: SDK must wake a blocked receive loop.
|
|
_ = command.Wait()
|
|
if ctx.Err() != nil {
|
|
t.Fatal("SDK stayed blocked after request timeout")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func TestSessionQueueOverflowRevokesLease(t *testing.T) {
|
|
manager := test_manager(t, DefaultLimits(), nil)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
output, revoke := context.WithCancel(ctx)
|
|
session := &Session{manager: manager, spec: Spec{SessionEvents: true, RemoteGrants: []string{"output-observe"}}, ctx: ctx, cancel: cancel, outputContext: output, revokeOutput: revoke, state: Active, outbound: make(chan outgoing, 1), revoked: make(chan struct{})}
|
|
action, err := session.AuthorizeRemote("controlled", []string{"output-observe"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
params, _ := json.Marshal(tentaclewire.RemoteOpen{Action: action.Action, Epoch: action.Epoch, Role: action.Role, Grants: action.Grants})
|
|
session.mutex.Lock()
|
|
value, failure := session.remote_call_locked("remote.open", params)
|
|
session.mutex.Unlock()
|
|
if failure != nil {
|
|
t.Fatal(failure)
|
|
}
|
|
delivery, err := session.RemoteContext(value.(tentaclewire.RemoteLease), "output-observe")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.RevokeRemote() // Revocation cannot be queued behind the unread action.
|
|
if delivery.Err() == nil {
|
|
t.Fatal("queue blocked immediate revocation")
|
|
}
|
|
if state, _ := session.Status(); state != Failed {
|
|
t.Fatal("overflow did not retire session", state)
|
|
}
|
|
}
|
|
|
|
func TestSessionHostRejectsMalformedChildRequests(t *testing.T) {
|
|
for _, frame := range []string{
|
|
`{"jsonrpc":"2.0","id":"1","method":"remote.close","params":{}}`,
|
|
`{"jsonrpc":"2.0","id":"c:01","method":"remote.close","params":{}}`,
|
|
`{"jsonrpc":"2.0","id":"c:0","method":"remote.close","params":{}}`,
|
|
`{"jsonrpc":"2.0","id":null,"method":"remote.close","params":{}}`,
|
|
`{"jsonrpc":"2.0","id":"c:1","method":"remote.close","method":"remote.open","params":{}}`,
|
|
`{"jsonrpc":"2.0","id":"c:1","method":"remote.close","params":{},"result":{}}`,
|
|
`{"jsonrpc":"2.0","id":"c:1","method":"remote.close"}`,
|
|
} {
|
|
t.Run(frame, func(t *testing.T) {
|
|
session := &Session{spec: Spec{SessionEvents: true}, ctx: t.Context(), state: Active, outbound: make(chan outgoing, 32)}
|
|
if err := session.accept_child_request([]byte(frame)); err == nil {
|
|
t.Fatal("malformed request accepted")
|
|
}
|
|
})
|
|
}
|
|
session := &Session{spec: Spec{SessionEvents: true}, ctx: t.Context(), state: Active, outbound: make(chan outgoing, 32)}
|
|
frame := []byte(`{"jsonrpc":"2.0","id":"c:1","method":"remote.unknown","params":{}}`)
|
|
if err := session.accept_child_request(frame); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reply, err := decode_response((<-session.outbound).frame)
|
|
if err != nil || reply.Error == nil || reply.Error.Code != -32601 {
|
|
t.Fatal("unknown method response", reply, err)
|
|
}
|
|
if err := session.accept_child_request(frame); err == nil {
|
|
t.Fatal("duplicate child ID accepted")
|
|
}
|
|
session.spec.SessionEvents = false
|
|
if err := session.accept_child_request([]byte(`{"jsonrpc":"2.0","id":"c:2","method":"remote.close","params":{}}`)); err == nil {
|
|
t.Fatal("1.0 accepted child request")
|
|
}
|
|
}
|
|
|
|
func TestSessionSDKRejectsMalformedHostMessages(t *testing.T) {
|
|
initialize := `{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocol":{"major":1,"minor":1},"session":"test","apis":["commands","session-events"],"capabilities":[]}}` + "\n"
|
|
activate := `{"jsonrpc":"2.0","method":"initialized","params":{"session":"test"}}` + "\n"
|
|
event := `{"jsonrpc":"2.0","method":"session.event","params":{"sequence":1,"kind":"remote.action","action":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","epoch":1,"role":"controlled","grants":["output-observe"]}}` + "\n"
|
|
cases := map[string]string{
|
|
"null capabilities": strings.Replace(initialize, `"capabilities":[]`, `"capabilities":null`, 1),
|
|
"null event field": initialize + activate + strings.Replace(event, `"epoch":1`, `"epoch":1,"lease":null`, 1),
|
|
"old host": strings.Replace(initialize, `"minor":1`, `"minor":0`, 1),
|
|
"missing event API": strings.Replace(initialize, `"commands","session-events"`, `"commands"`, 1),
|
|
"legacy capabilities": strings.Replace(initialize, `"capabilities":[]`, `"capabilities":["commands"]`, 1),
|
|
"unrequested response": initialize + activate + `{"jsonrpc":"2.0","id":"c:1","result":null}` + "\n",
|
|
"event before activation": initialize + event,
|
|
"event with request ID": initialize + activate + strings.Replace(event, `"method":`, `"id":"2","method":`, 1),
|
|
"duplicate event": initialize + activate + event + event,
|
|
"skipped sequence": initialize + activate + strings.Replace(event, `"sequence":1`, `"sequence":2`, 1),
|
|
"event role mismatch": initialize + activate + strings.Replace(event, `"controlled"`, `"controller"`, 1),
|
|
"unknown event": initialize + activate + strings.Replace(event, `"remote.action"`, `"remote.execute"`, 1),
|
|
}
|
|
overflow := initialize + activate
|
|
for sequence := 1; sequence <= 33; sequence++ {
|
|
overflow += strings.Replace(event, `"sequence":1`, `"sequence":`+strconv.Itoa(sequence), 1)
|
|
}
|
|
cases["event overflow"] = overflow
|
|
outputEvent := `{"jsonrpc":"2.0","method":"session.event","params":{"sequence":1,"kind":"remote.output","lease":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","epoch":1,"speech":{"kind":"cancel"}}}` + "\n"
|
|
cases["output without lease"] = initialize + activate + strings.Replace(outputEvent, `"lease":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",`, "", 1)
|
|
cases["output with role"] = initialize + activate + strings.Replace(outputEvent, `"epoch":1`, `"epoch":1,"role":"controller"`, 1)
|
|
cases["null speech"] = initialize + activate + strings.Replace(outputEvent, `{"kind":"cancel"}`, `null`, 1)
|
|
cases["malformed speech"] = initialize + activate + strings.Replace(outputEvent, `{"kind":"cancel"}`, `{"kind":"pause","paused":null}`, 1)
|
|
cases["speech on action"] = initialize + activate + strings.Replace(event, `"epoch":1`, `"epoch":1,"speech":{"kind":"cancel"}`, 1)
|
|
for _, language := range []string{"go", "python"} {
|
|
for name, input := range cases {
|
|
t.Run(language+"/"+name, func(t *testing.T) {
|
|
program := sessionSDKProgram(t, language)
|
|
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
|
defer cancel()
|
|
command := exec.CommandContext(ctx, program[0], program[1:]...)
|
|
command.Stdin = strings.NewReader(input)
|
|
if err := command.Run(); err == nil {
|
|
t.Fatal("invalid host message accepted")
|
|
}
|
|
if ctx.Err() != nil {
|
|
t.Fatal("SDK hung on invalid message")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionAggregateQueueBound(t *testing.T) {
|
|
session := &Session{spec: Spec{SessionEvents: true}, outbound: make(chan outgoing, 32)}
|
|
frame := make([]byte, MaxFrameBytes)
|
|
for range 4 {
|
|
if !session.enqueue_locked(outgoing{frame: frame}) {
|
|
t.Fatal("queue rejected boundary")
|
|
}
|
|
}
|
|
if session.enqueue_locked(outgoing{frame: []byte{1}}) {
|
|
t.Fatal("aggregate byte ceiling exceeded")
|
|
}
|
|
}
|
|
|
|
func TestPendingRemoteRevocationReachesBothSDKs(t *testing.T) {
|
|
for _, language := range []string{"go", "python"} {
|
|
for _, reason := range []string{"local", "expired", "replaced"} {
|
|
t.Run(language+"/"+reason, func(t *testing.T) {
|
|
session := startSessionSDK(t, test_manager(t, DefaultLimits(), nil), language, "pending-revoke")
|
|
action, err := session.AuthorizeRemote("controller", []string{"remote-present"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
invokeSessionSDK(t, session, "event", nil)
|
|
session.mutex.Lock()
|
|
if reason == "expired" {
|
|
session.refresh_remote_locked(time.Millisecond)
|
|
} else {
|
|
session.revoke_remote_locked(reason, true)
|
|
}
|
|
session.mutex.Unlock()
|
|
var event tentaclewire.SessionEvent
|
|
raw := invokeSessionSDK(t, session, "event", nil)
|
|
if json.Unmarshal(raw, &event) != nil || event.Kind != "remote.revoked" || event.Action != action.Action || event.Lease != "" || event.Epoch != action.Epoch || event.Reason != reason {
|
|
t.Fatal("pending action revocation was not delivered", string(raw))
|
|
}
|
|
if _, code := remoteCall(t, session, "remote.open", tentaclewire.RemoteOpen{Action: action.Action, Epoch: action.Epoch, Role: action.Role, Grants: action.Grants}); code != -32001 {
|
|
t.Fatal("retired action opened a lease", code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|