Files
2026-07-13 13:00:08 +08:00

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)
}
}