Files
aego/tests/integration/agent_panel_test.go

187 lines
5.2 KiB
Go

package integration
import (
"slices"
"testing"
"time"
"aego/app/agentui/backend"
"aego/app/edit"
"aego/core/log"
"aego/editor/core/agentui"
agentpanel "aego/editor/panels/agent"
"aego/project"
)
func agentSession(t *testing.T, b backend.Backend) (*edit.Session, string) {
t.Helper()
root := copyExample(t, "platformer")
p, err := project.Load(root)
if err != nil {
t.Fatal(err)
}
s, err := edit.Open(p, edit.Config{Headless: true, NoWatch: true, Logger: log.Nop(), AgentBackend: b}, combatModule{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(s.Close)
s.Step()
if _, err := s.App.Context().Docs.Open(bridgeScene); err != nil {
t.Fatal(err)
}
s.App.Workspace().Show(agentpanel.ID)
return s, root
}
func stepUntil(t *testing.T, s *edit.Session, done func() bool) {
t.Helper()
deadline := time.Now().Add(30 * time.Second)
for !done() {
if time.Now().After(deadline) {
t.Fatal("timed out")
}
s.Step()
}
}
// Allocation budgets measure a settled editor, not background preview work.
func waitAgentEditorIdle(t *testing.T, s *edit.Session) {
t.Helper()
deadline := time.Now().Add(30 * time.Second)
stable := 0
for stable < 10 {
s.Step()
if s.App.Idle() && !s.App.Previews().Busy() {
stable++
} else {
stable = 0
}
if time.Now().After(deadline) {
t.Fatal("editor previews did not settle before measuring allocations")
}
time.Sleep(5 * time.Millisecond)
}
}
// The panel shows the proposal as a card, Apply makes one Undo step, and the session log holds every kind of entry.
func TestAgentPanelAppliesACardAndLogsTheSession(t *testing.T) {
fake := &backend.Fake{Turns: [][]backend.Step{{
{Text: "Adding a crate."},
{Tool: "scene_add_node", Args: map[string]any{"file": bridgeScene, "name": "AgentCrate"}},
{Text: "Done."},
}}}
s, root := agentSession(t, fake)
panel := s.App.Agent()
if panel == nil {
t.Fatal("no agent panel")
}
panel.Submit("add a crate")
stepUntil(t, s, func() bool { return len(panel.Pending()) == 1 })
panel.Decide(panel.Pending()[0], true)
stepUntil(t, s, func() bool {
d := s.App.Context().Docs.Edit()
return len(d.History.Steps(nil)) == 1 && !d.Scene().Find(d.Scene().Root(), "AgentCrate").IsZero()
})
for range 30 {
s.Step()
}
ids, err := agentui.List(root)
if err != nil || len(ids) != 1 {
t.Fatalf("sessions %v %v", ids, err)
}
entries, err := agentui.Load(root, ids[0])
if err != nil {
t.Fatal(err)
}
var kinds, states []string
for _, e := range entries {
kinds = append(kinds, e.Kind)
if e.Kind == "card" {
states = append(states, e.State)
}
}
for _, k := range []string{"user", "text", "tool", "result", "card"} {
if !slices.Contains(kinds, k) {
t.Fatalf("the log has no %s entry: %v", k, kinds)
}
}
if !slices.Contains(states, "pending") || !slices.Contains(states, "applied") {
t.Fatalf("card states %v", states)
}
}
func TestAgentPanelExplainsAMissingClaude(t *testing.T) {
s, _ := agentSession(t, backend.Claude{})
panel := s.App.Agent()
s.App.SetAgentCommand("/nonexistent/claude-for-aego-tests")
stepUntil(t, s, panel.Checked)
panel.Recheck()
stepUntil(t, s, func() bool { return panel.Missing() != nil })
}
func TestAgentAskFillsTheInput(t *testing.T) {
s, _ := agentSession(t, &backend.Fake{})
panel := s.App.Agent()
panel.Ask("In scenes/main.aescene, Player: ")
s.Step()
if panel.Input() != "In scenes/main.aescene, Player: " {
t.Fatalf("input %q", panel.Input())
}
}
// An idle Agent panel adds no allocations to a frame until the first message.
func TestAgentPanelIdleAllocs(t *testing.T) {
skipUnderRace(t)
measure := func(b backend.Backend) float64 {
root := copyExample(t, "platformer")
p, err := project.Load(root)
if err != nil {
t.Fatal(err)
}
s, err := edit.Open(p, edit.Config{Headless: true, NoWatch: true, Logger: log.Nop(), AgentBackend: b}, combatModule{})
if err != nil {
t.Fatal(err)
}
defer s.Close()
s.Step()
if _, err := s.App.Context().Docs.Open(bridgeScene); err != nil {
t.Fatal(err)
}
for range 60 {
s.Step()
}
waitAgentEditorIdle(t, s)
return testing.AllocsPerRun(30, func() { s.Step() })
}
without := measure(nil)
with := measure(&backend.Fake{})
if with > without {
t.Fatalf("frame allocations: %v without the agent panel, %v with it idle", without, with)
}
}
// Apply All takes the waiting change and the rest of the answer's, but leaves a removal to the user.
func TestAgentPanelApplyAllForTheTurn(t *testing.T) {
fake := &backend.Fake{Turns: [][]backend.Step{{
{Tool: "scene_add_node", Args: map[string]any{"file": bridgeScene, "name": "AgentA"}},
{Tool: "scene_add_node", Args: map[string]any{"file": bridgeScene, "name": "AgentB"}},
{Tool: "scene_remove_node", Args: map[string]any{"file": bridgeScene, "node": "AgentA"}},
{Text: "done"},
}}}
s, _ := agentSession(t, fake)
panel := s.App.Agent()
panel.Submit("add two nodes")
stepUntil(t, s, func() bool { return len(panel.Pending()) == 1 })
panel.ApplyAll()
d := s.App.Context().Docs.Edit()
stepUntil(t, s, func() bool { return len(d.History.Steps(nil)) == 2 && len(panel.Pending()) == 1 })
for range 20 {
s.Step()
}
if n := len(d.History.Steps(nil)); n != 2 {
t.Fatalf("Apply All applied a removal: %d steps", n)
}
panel.Decide(panel.Pending()[0], false)
stepUntil(t, s, func() bool { return !panel.Busy() })
}