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>
152 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|