Files
2026-08-14 23:41:57 +08:00

193 lines
4.3 KiB
Go

package remotecontrol
import (
"context"
"sync"
"testing"
"time"
)
type fakeSink struct {
mu sync.Mutex
outputs []string
confirms []confirmReq
connected bool
}
type confirmReq struct {
id, tool, summary string
}
func (f *fakeSink) SendOutput(text string) {
f.mu.Lock()
defer f.mu.Unlock()
f.outputs = append(f.outputs, text)
}
func (f *fakeSink) SendConfirm(id, tool, summary string) {
f.mu.Lock()
defer f.mu.Unlock()
f.confirms = append(f.confirms, confirmReq{id, tool, summary})
}
func (f *fakeSink) HasClient() bool {
f.mu.Lock()
defer f.mu.Unlock()
return f.connected
}
func (f *fakeSink) lastConfirmID() string {
f.mu.Lock()
defer f.mu.Unlock()
if len(f.confirms) == 0 {
return ""
}
return f.confirms[len(f.confirms)-1].id
}
func TestBridgeOutputWriterTees(t *testing.T) {
sink := &fakeSink{}
b := NewBridge(sink)
w := b.OutputWriter()
n, err := w.Write([]byte("hello world"))
if err != nil || n != len("hello world") {
t.Fatalf("Write = (%d,%v), want (%d,nil)", n, err, len("hello world"))
}
sink.mu.Lock()
defer sink.mu.Unlock()
if len(sink.outputs) != 1 || sink.outputs[0] != "hello world" {
t.Fatalf("outputs = %v, want [hello world]", sink.outputs)
}
}
func TestBridgeRemoteInput(t *testing.T) {
b := NewBridge(&fakeSink{})
b.OnInput("do a thing")
select {
case got := <-b.RemoteInput():
if got != "do a thing" {
t.Fatalf("input = %q, want 'do a thing'", got)
}
case <-time.After(time.Second):
t.Fatal("no input delivered")
}
}
func TestBridgeOnInputDropsOldestWhenFull(t *testing.T) {
b := NewBridge(&fakeSink{})
// Fill beyond capacity; must not block and must retain the newest items.
total := remoteInputBuffer + 10
for i := range total {
b.OnInput(itoa(uint64(i)))
}
// Drain and ensure we get exactly buffer-size items, ending at the newest.
var got []string
for {
select {
case v := <-b.RemoteInput():
got = append(got, v)
continue
default:
}
break
}
if len(got) != remoteInputBuffer {
t.Fatalf("drained %d items, want %d", len(got), remoteInputBuffer)
}
if last := got[len(got)-1]; last != itoa(uint64(total-1)) {
t.Fatalf("newest = %q, want %q", last, itoa(uint64(total-1)))
}
}
func TestBridgeConfirmResolvedRemotely(t *testing.T) {
sink := &fakeSink{connected: true}
b := NewBridge(sink)
done := make(chan struct {
d Decision
remote bool
}, 1)
go func() {
d, remote := b.Confirm(context.Background(), "shell", "rm -rf /tmp/x")
done <- struct {
d Decision
remote bool
}{d, remote}
}()
// Wait for the confirm to be sent, then answer it via OnDecide.
deadline := time.Now().Add(time.Second)
for sink.lastConfirmID() == "" {
if time.Now().After(deadline) {
t.Fatal("confirm never sent")
}
time.Sleep(2 * time.Millisecond)
}
b.OnDecide(sink.lastConfirmID(), true, true)
select {
case r := <-done:
if !r.remote {
t.Fatal("remote flag = false, want true")
}
if !r.d.Approve || !r.d.Always {
t.Fatalf("decision = %+v, want approve+always", r.d)
}
case <-time.After(time.Second):
t.Fatal("Confirm did not return")
}
}
func TestBridgeConfirmCancelledByContext(t *testing.T) {
sink := &fakeSink{connected: true}
b := NewBridge(sink)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan bool, 1)
go func() {
_, remote := b.Confirm(ctx, "shell", "ls")
done <- remote
}()
// Let the confirm register, then cancel (simulating a local answer).
deadline := time.Now().Add(time.Second)
for sink.lastConfirmID() == "" {
if time.Now().After(deadline) {
t.Fatal("confirm never sent")
}
time.Sleep(2 * time.Millisecond)
}
cancel()
select {
case remote := <-done:
if remote {
t.Fatal("remote flag = true, want false on ctx cancel")
}
case <-time.After(time.Second):
t.Fatal("Confirm did not return on cancel")
}
}
func TestBridgeResolveConfirmUnknown(t *testing.T) {
b := NewBridge(&fakeSink{})
if b.ResolveConfirm("nope", true, false) {
t.Fatal("ResolveConfirm on unknown id = true, want false")
}
}
func TestBridgeEnabled(t *testing.T) {
var nilBridge *Bridge
if nilBridge.Enabled() {
t.Fatal("nil bridge Enabled = true, want false")
}
sink := &fakeSink{connected: false}
b := NewBridge(sink)
if b.Enabled() {
t.Fatal("Enabled = true with no client, want false")
}
sink.connected = true
if !b.Enabled() {
t.Fatal("Enabled = false with client, want true")
}
}