Files
cthulhu/internal/app/tentacle_remote_keyboard_owner_test.go
Chase CichorzandClaude Opus 5.5 27017bef11 Attach Remote keyboard injection to approved controlled sessions
Local launch and managed startup now start the incoming-key owner for
sessions installed with keyboard-inject, using one input-helper
Supervisor retained for the reader lifetime. Delivery rechecks lock,
XTerm handoff and internal reader UI in addition to lease, grant and
deadline. build-cthulhu.sh and both Arch packages build and install
cthulhu-input-helper; a missing helper leaves keyboard input
unavailable without affecting startup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 16:06:38 -05:00

152 lines
5.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"encoding/json"
"errors"
"log/slog"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/tentacle"
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
)
func keyboard_attach_runtime(policy *tentacle.RemotePolicy, helper string) *tentacleRuntime {
return &tentacleRuntime{logger: slog.New(slog.DiscardHandler), remotePolicy: policy, remoteInputHelper: helper,
output: &tentacleOutput{gate: &focusGate{}}, announcements: make(chan string, 1)}
}
func keyboard_attach_key(t *testing.T, session *tentacle.Session, lease tentaclewire.RemoteLease, sequence uint64, name string, pressed bool) error {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
request := tentaclewire.RemoteKeyRequest{RemoteOperation: tentaclewire.RemoteOperation{Lease: lease.Lease, Epoch: lease.Epoch, Sequence: sequence}, Key: tentaclewire.RemoteKey{Name: name, Pressed: pressed}}
data, _ := json.Marshal(map[string]any{"Method": "remote.key", "Params": request})
pending, err := session.Invoke(ctx, "rpc", data)
if err != nil {
t.Fatal(err)
}
select {
case result := <-pending:
return result.Err
case <-ctx.Done():
t.Fatal("keyboard peer call timed out")
return nil
}
}
func keyboard_attach_open(t *testing.T, session *tentacle.Session) tentaclewire.RemoteLease {
t.Helper()
approval, err := session.BeginRemoteApproval("controlled", []string{"keyboard-inject"})
if err != nil {
t.Fatal(err)
}
if _, err := session.AuthorizeRemoteWithApproval(approval, "controlled", []string{"keyboard-inject"}); err != nil {
t.Fatal(err)
}
var lease tentaclewire.RemoteLease
if json.Unmarshal(keyboard_owner_invoke(t, session, "open", nil), &lease) != nil || lease.Epoch == 0 {
t.Fatal("keyboard peer failed to open lease")
}
return lease
}
func TestRemoteKeyboardProductionAttachDeliversAndRetires(t *testing.T) {
policy := tentacle.NewRemotePolicy()
program, log := keyboard_owner_helper(t)
session := keyboard_owner_session(t, policy, []string{"keyboard-inject", "output-observe"})
runtime := keyboard_attach_runtime(policy, program)
ctx, cancel := context.WithCancel(t.Context())
done := runtime.start_remote_keyboard(ctx, session)
t.Cleanup(func() {
cancel()
select {
case <-done:
case <-time.After(6 * time.Second):
t.Error("production keyboard owner did not join")
}
})
lease := keyboard_attach_open(t, session)
if err := keyboard_attach_key(t, session, lease, 1, "a", true); err != nil {
t.Fatal(err)
}
if err := keyboard_attach_key(t, session, lease, 2, "a", false); err != nil {
t.Fatal(err)
}
f := &keyboardOwnerFixture{log: log}
prefix := strconv.FormatUint(lease.Epoch, 10) + " "
f.wait(t, func() bool { return strings.Contains(f.effects(), prefix+"a false\n") })
// Lock/handoff policy revokes the lease and releases the helper.
policy.SetAvailable(false)
f.wait(t, func() bool { return strings.Contains(f.effects(), prefix+"cleanup\n") })
policy.SetAvailable(true)
if runtime.remote_input_supervisor() != runtime.remoteInput || runtime.remoteInput == nil {
t.Fatal("supervisor was not retained for the reader lifetime")
}
}
func TestRemoteKeyboardProductionAttachRechecksReaderUI(t *testing.T) {
policy := tentacle.NewRemotePolicy()
program, log := keyboard_owner_helper(t)
session := keyboard_owner_session(t, policy, []string{"keyboard-inject"})
runtime := keyboard_attach_runtime(policy, program)
runtime.output.gate.suspended.Store(true) // XTerm handoff.
ctx, cancel := context.WithCancel(t.Context())
done := runtime.start_remote_keyboard(ctx, session)
defer func() { cancel(); <-done }()
lease := keyboard_attach_open(t, session)
_ = keyboard_attach_key(t, session, lease, 1, "a", true)
deadline := time.Now().Add(5 * time.Second)
for {
if _, err := session.RemoteContext(lease, "keyboard-inject"); err != nil {
break
}
if time.Now().After(deadline) {
t.Fatal("suspended reader kept keyboard consent")
}
time.Sleep(2 * time.Millisecond)
}
f := &keyboardOwnerFixture{log: log}
if strings.Contains(f.effects(), "a true") || strings.Contains(f.effects(), "starting") {
t.Fatal("suspended reader delivered a remote key", f.effects())
}
}
func TestRemoteKeyboardProductionAttachRequiresGrantAndHelper(t *testing.T) {
policy := tentacle.NewRemotePolicy()
program, _ := keyboard_owner_helper(t)
for name, test := range map[string]struct {
grants []string
helper string
}{
"no grant": {[]string{"output-observe"}, program},
"missing helper": {[]string{"keyboard-inject"}, filepath.Join(t.TempDir(), "absent")},
} {
t.Run(name, func(t *testing.T) {
session := keyboard_owner_session(t, policy, test.grants)
runtime := keyboard_attach_runtime(policy, test.helper)
done := runtime.start_remote_keyboard(t.Context(), session)
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("unavailable keyboard owner did not finish immediately")
}
if name == "missing helper" {
lease := keyboard_attach_open(t, session)
err := keyboard_attach_key(t, session, lease, 1, "a", true)
if err == nil || errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("missing helper accepted a key: %v", err)
}
}
})
}
}