first commit
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user