146 lines
4.5 KiB
Go
146 lines
4.5 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/smallnest/pigo/internal/agentcore"
|
|
)
|
|
|
|
// newRemoteTestSession builds a fresh run session over a temp-dir store for the
|
|
// remote-control lifecycle tests (no resume, no tools).
|
|
func newRemoteTestSession(t *testing.T) *runSession {
|
|
t.Helper()
|
|
store := newTestStore(t)
|
|
s, _, err := newRunSessionWithStore(store, Options{})
|
|
if err != nil {
|
|
t.Fatalf("newRunSessionWithStore: %v", err)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// TestStartStopRemote covers the start→already-running→stop lifecycle on
|
|
// runSession: startRemote binds a listener and stores the session, a second
|
|
// start reports "already running" without replacing it, and stopRemote clears
|
|
// the session so a later start can rebind.
|
|
func TestStartStopRemote(t *testing.T) {
|
|
s := newRemoteTestSession(t)
|
|
if s.remote != nil {
|
|
t.Fatal("remote should be nil before start")
|
|
}
|
|
|
|
url, err := s.startRemote()
|
|
if err != nil {
|
|
t.Fatalf("startRemote: %v", err)
|
|
}
|
|
if url == "" {
|
|
t.Fatal("startRemote returned empty url")
|
|
}
|
|
if s.remote == nil {
|
|
t.Fatal("remote should be set after start")
|
|
}
|
|
first := s.remote
|
|
|
|
// A second start is a no-op that reports the existing url and an error,
|
|
// leaving the running session untouched.
|
|
url2, err := s.startRemote()
|
|
if err == nil {
|
|
t.Error("second startRemote should report already-running error")
|
|
}
|
|
if url2 != url {
|
|
t.Errorf("second startRemote url = %q, want %q", url2, url)
|
|
}
|
|
if s.remote != first {
|
|
t.Error("second startRemote must not replace the running session")
|
|
}
|
|
|
|
s.stopRemote()
|
|
if s.remote != nil {
|
|
t.Error("remote should be nil after stop")
|
|
}
|
|
|
|
// Stop again is a no-op.
|
|
s.stopRemote()
|
|
|
|
// After stopping, a fresh start rebinds cleanly.
|
|
if _, err := s.startRemote(); err != nil {
|
|
t.Fatalf("restart after stop: %v", err)
|
|
}
|
|
s.stopRemote()
|
|
}
|
|
|
|
// TestBuildConfigInstallsRemoteSeam asserts buildConfig only installs the
|
|
// BeforeToolCall confirm seam while a remote session is present: off by default
|
|
// (up-front trust unchanged), wired once /remote-control is running.
|
|
func TestBuildConfigInstallsRemoteSeam(t *testing.T) {
|
|
s := newRemoteTestSession(t)
|
|
|
|
if cfg := s.buildConfig(); cfg.Batch.ToolExecutorConfig.BeforeToolCall != nil {
|
|
t.Error("BeforeToolCall should be nil when remote control is off")
|
|
}
|
|
|
|
if _, err := s.startRemote(); err != nil {
|
|
t.Fatalf("startRemote: %v", err)
|
|
}
|
|
defer s.stopRemote()
|
|
|
|
if cfg := s.buildConfig(); cfg.Batch.ToolExecutorConfig.BeforeToolCall == nil {
|
|
t.Error("BeforeToolCall should be installed when remote control is on")
|
|
}
|
|
}
|
|
|
|
// TestRemoteConfirmSeamAllowsWhenNoClient verifies the confirm seam is a no-op
|
|
// (returns nil = allow under up-front trust) when no browser is connected, so a
|
|
// running-but-unpaired server never blocks tool calls.
|
|
func TestRemoteConfirmSeamAllowsWhenNoClient(t *testing.T) {
|
|
s := newRemoteTestSession(t)
|
|
if _, err := s.startRemote(); err != nil {
|
|
t.Fatalf("startRemote: %v", err)
|
|
}
|
|
defer s.stopRemote()
|
|
|
|
// No client is paired, so hasClient() is false and the seam must allow.
|
|
seam := remoteConfirmSeam(s.remote, nil, "/tmp/project")
|
|
if d := seam(t.Context(), agentcore.AgentToolCall{Name: "bash"}); d != nil {
|
|
t.Errorf("seam should allow (nil) with no client, got %+v", d)
|
|
}
|
|
}
|
|
|
|
// TestRemoteInputIgnoredWhileRunning routes a remoteInputMsg into an idle vs a
|
|
// running Model: while a run is in flight the prompt is not started (a busy note
|
|
// is shown instead), and the listener is always re-issued so later submissions
|
|
// keep arriving.
|
|
func TestRemoteInputIgnoredWhileRunning(t *testing.T) {
|
|
s := newRemoteTestSession(t)
|
|
if _, err := s.startRemote(); err != nil {
|
|
t.Fatalf("startRemote: %v", err)
|
|
}
|
|
defer s.stopRemote()
|
|
|
|
m := NewModel(Options{})
|
|
m.session = s
|
|
m.running = true
|
|
|
|
updated, _ := m.Update(remoteInputMsg{text: "do something"})
|
|
got := updated.(Model)
|
|
if !hasSystemBlockContaining(got.transcript, "a run is in progress") {
|
|
t.Errorf("expected a busy note in the transcript while running, blocks=%v",
|
|
blockTexts(got.transcript))
|
|
}
|
|
// A run in progress must not consume the remote prompt as a new turn.
|
|
if !got.running {
|
|
t.Error("model should still be running; the remote prompt must not start a turn")
|
|
}
|
|
}
|
|
|
|
// hasSystemBlockContaining reports whether any transcript block's text contains
|
|
// sub (case-insensitive substring over the rendered block texts).
|
|
func hasSystemBlockContaining(t transcript, sub string) bool {
|
|
for _, s := range blockTexts(t) {
|
|
if strings.Contains(strings.ToLower(s), strings.ToLower(sub)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|