88 lines
2.5 KiB
Go
88 lines
2.5 KiB
Go
package control
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"reasonix/internal/agent"
|
|
"reasonix/internal/event"
|
|
"reasonix/internal/provider"
|
|
"reasonix/internal/tool"
|
|
)
|
|
|
|
func steerFallbackController(t *testing.T) (*Controller, *agent.Agent) {
|
|
t.Helper()
|
|
prov := &scriptedTurns{turns: [][]provider.Chunk{textTurn("ok")}}
|
|
ag := agent.New(prov, tool.NewRegistry(), agent.NewSession(""), agent.Options{}, event.Discard)
|
|
return New(Options{Runner: ag, Executor: ag, Sink: event.Discard}), ag
|
|
}
|
|
|
|
func sessionHasUserText(ag *agent.Agent, text string) bool {
|
|
for _, m := range ag.Session().Messages {
|
|
if m.Role == provider.RoleUser && strings.Contains(m.Content, text) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TestSteerFallbackParksWhileRunning forces the turn-exit window: the
|
|
// controller still reports running (the previous body has not returned), but
|
|
// the agent's steer intake is already closed, so exec.Steer rejects the text.
|
|
// The fallback must park the steer as a turn and deliver it when the window
|
|
// closes — runGuarded's deliberately-silent running drop would lose the
|
|
// user's words.
|
|
func TestSteerFallbackParksWhileRunning(t *testing.T) {
|
|
c, ag := steerFallbackController(t)
|
|
|
|
block := make(chan struct{})
|
|
started := make(chan struct{})
|
|
c.runGuarded(func(context.Context) error {
|
|
close(started)
|
|
<-block
|
|
return nil
|
|
})
|
|
<-started
|
|
|
|
c.Steer("queued while exiting")
|
|
|
|
c.mu.Lock()
|
|
parked := len(c.parkedTurns)
|
|
c.mu.Unlock()
|
|
if parked != 1 {
|
|
t.Fatalf("steer fallback should park while running, parked=%d", parked)
|
|
}
|
|
|
|
close(block)
|
|
waitIdleAdmission(t, c)
|
|
deadline := time.Now().Add(30 * time.Second)
|
|
for !sessionHasUserText(ag, "queued while exiting") {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("parked steer was never delivered as a turn")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
waitIdleAdmission(t, c)
|
|
}
|
|
}
|
|
|
|
// TestSteerBetweenTurnsRunsNewTurn pins the idle fallback: with no turn
|
|
// running the executor rejects the steer, and the controller must submit it
|
|
// as a regular turn instead of returning silently (also covers the
|
|
// executor-rejection path a stale frontend running flag would hit).
|
|
func TestSteerBetweenTurnsRunsNewTurn(t *testing.T) {
|
|
c, ag := steerFallbackController(t)
|
|
|
|
c.Steer("late steer")
|
|
|
|
deadline := time.Now().Add(30 * time.Second)
|
|
for !sessionHasUserText(ag, "late steer") {
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("idle steer was never delivered as a turn")
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
waitIdleAdmission(t, c)
|
|
}
|
|
}
|