Files
ZacharyZhang-NY d60d53ef90 Add agent components 451-465 and icon.Crystal
Fifteen components that dress an AI agent's work as an RPG party:
AgentCard, ToolCall, PlanQuest, ThinkingOrb, TokenMana, ApprovalPrompt,
AgentLog, SubAgentParty, MemoryCrystals, AgentStatus, DiffScroll,
ModelSelect, PromptBox, CostCoins and Checkpoints.

The context window is HP and the token budget MP; tool calls are spells
that CAST, glow, then land OK or FAILED; the transcript reads as a battle
log whose newest line types itself out; models line up like fighters.
Busy signs keep moving under Reduce Motion, decoration stops. Commands
and prompts never reach the log, and a veiled PromptBox casts nothing.

icon.Crystal is the new 12x12 save and memory crystal.
2026-10-12 12:03:44 -04:00

199 lines
6.0 KiB
Go

package agent_test
import (
"strings"
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/agent"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestApprovalPromptAnswersAndKeepsTheCommandSecret(t *testing.T) {
allowed, denied, off := 0, 0, false
cmd := "curl -H 'Authorization: Bearer SECRET' https://example.com/deploy && rm -rf build/cache/old/artifacts"
tt := view(600, 300, newWorld(), func(c *ui.Context) {
r := agent.ApprovalPrompt(c, "Approval", cmd, agent.ApprovalPromptOptions{Danger: true, Disabled: off})
if r.Allowed {
allowed++
}
if r.Denied {
denied++
}
})
log := logged(func() {
click(t, tt, "YES")
click(t, tt, "NO")
})
if allowed != 1 || denied != 1 || !strings.Contains(log, "allowed=true") || strings.Contains(log, "SECRET") {
t.Fatalf("allowed %d, denied %d, log:\n%s", allowed, denied, log)
}
// The whole command reads out; Danger inks it Bad.
x, y, w, h := dots(find(t, tt, cmd))
if count(tt.Image(), x, y, w, h, pixel.Bad) == 0 || h != 3*16+6 {
t.Fatalf("the command well is %d dots high, or not Bad", h)
}
off = true
tt.Frame()
click(t, tt, "YES")
if allowed != 1 {
t.Fatal("a disabled prompt took a YES")
}
}
func TestAgentLogTypesTheNewestStep(t *testing.T) {
for _, reduce := range []bool{false, true} {
wd := newWorld()
wd.reduce = reduce
steps := []agent.LogStep{{Kind: agent.Cast, Text: "read_file"}}
tt := view(600, 300, wd, func(c *ui.Context) { agent.AgentLog(c, "Log", "ROBO", steps, agent.AgentLogOptions{}) })
ink := func(label string, s pixel.Slot) int {
x, y, w, h := dots(find(t, tt, label))
return count(tt.Image(), x, y, w, h, s)
}
// Opened, the log shows whole: nothing types.
if ink("ROBO casts read_file!", pixel.Info) < 20 {
t.Fatal("the first line is not shown whole")
}
steps = append(steps, agent.LogStep{Kind: agent.Hit, Text: "3 files found"})
tt.Frame()
first := ink("A hit! 3 files found", pixel.Good)
later(tt, wd, time.Second)
full := ink("A hit! 3 files found", pixel.Good)
if full == 0 || reduce != (first == full) {
t.Errorf("reduce %v: %d dots typed at once, %d after a second", reduce, first, full)
}
}
}
func TestMemoryCrystalsBrowseAndPick(t *testing.T) {
memories := []string{"Prefers tabs", "Ships on Fridays"}
picked, off := -1, false
tt := view(400, 200, newWorld(), func(c *ui.Context) {
if r := agent.MemoryCrystals(c, "Memory", memories, agent.MemoryCrystalsOptions{Slots: 4, Disabled: off}); r.Picked >= 0 {
picked = r.Picked
}
})
x, y, _, _ := dots(find(t, tt, "Memory"))
img := tt.Image()
for i := range 4 {
if held := count(img, x+i*26, y, 26, 30, pixel.Magic) > 0; held != (i < 2) {
t.Errorf("crystal %d lit is %v", i, held)
}
}
// A click picks the crystal under it; so does Enter.
click(t, tt, "Memory")
tt.Key(0, ui.KeyHome)
tt.Key(0, ui.KeyRight)
if picked != 2 {
t.Fatalf("the click picked %d", picked)
}
tt.Key(0, ui.KeyEnter)
if picked != 1 {
t.Fatalf("Home, Right, Enter picked %d", picked)
}
off = true
tt.Frame()
tt.Key(0, ui.KeyEnd)
tt.Key(0, ui.KeyEnter)
if picked != 1 {
t.Fatalf("a disabled shelf picked %d", picked)
}
}
func TestModelSelectPicksLikeAFighter(t *testing.T) {
models := []agent.Model{{Name: "SWIFT", Speed: 5, Power: 2, Cost: 1}, {Name: "TITAN", Speed: 2, Power: 5, Cost: 5}, {Name: "SAGE", Speed: 3, Power: 4, Cost: 3}}
chosen, changed, off := 0, 0, false
tt := view(600, 300, newWorld(), func(c *ui.Context) {
if agent.ModelSelect(c, "Models", models, &chosen, agent.ModelSelectOptions{Disabled: off}).Changed {
changed++
}
})
click(t, tt, "Models")
if chosen != 1 || changed != 1 {
t.Fatalf("the click chose %d, %d changes", chosen, changed)
}
tt.Key(0, ui.KeyRight)
x, y, w, _ := dots(find(t, tt, "Models"))
img := tt.Image()
// The panel shows SAGE, under the cursor: three SPD pips.
for j := range 5 {
if want := j < 3; (at(img, x+w+8+50+j*12+1, y+46+6) == pixel.Info) != want {
t.Errorf("SPD pip %d lit is not %v", j, want)
}
}
tt.Key(0, ui.KeyEnter)
if chosen != 2 || changed != 2 || at(tt.Image(), x+60+3, y+3) != pixel.Accent {
t.Fatalf("Right and Enter chose %d, %d changes, or no Accent corner", chosen, changed)
}
off = true
tt.Frame()
tt.Key(0, ui.KeyLeft)
tt.Key(0, ui.KeyEnter)
if chosen != 2 {
t.Fatalf("a disabled select chose %d", chosen)
}
}
func TestPromptBoxCastsOnEnterOrCast(t *testing.T) {
draft, cast := "", []string{}
tt := view(600, 300, newWorld(), func(c *ui.Context) {
if r := agent.PromptBox(c, "Prompt", &draft, agent.PromptBoxOptions{}); r.Cast != "" {
cast = append(cast, r.Cast)
}
})
click(t, tt, "CAST")
click(t, tt, "Prompt")
log := logged(func() {
tt.Type("fix")
tt.Key(ui.Shift, ui.KeyEnter)
tt.Type("it")
tt.Key(0, ui.KeyEnter)
})
if len(cast) != 1 || cast[0] != "fix\nit" || draft != "" {
t.Fatalf("cast %q, draft %q", cast, draft)
}
if !strings.Contains(log, "runes=6") || strings.Contains(log, "fix") {
t.Fatalf("the log should count, not quote:\n%s", log)
}
tt.Type("ship")
click(t, tt, "CAST")
if len(cast) != 2 || cast[1] != "ship" || draft != "" {
t.Fatalf("cast %q, draft %q", cast, draft)
}
}
func TestCheckpointsRestoreByName(t *testing.T) {
wd := newWorld()
now := wd.clk.t
points := []agent.Checkpoint{{Name: "Before refactor", At: now.Add(-2 * time.Hour)}, {Name: "Tests green", At: now.Add(-5 * time.Minute)}}
restored, off := -1, false
tt := view(600, 300, wd, func(c *ui.Context) {
if r := agent.Checkpoints(c, "Saves", points, agent.CheckpointsOptions{Disabled: off}); r.Restored >= 0 {
restored = r.Restored
}
})
find(t, tt, "5 MIN AGO")
click(t, tt, "RESTORE Tests green")
if restored != 1 {
t.Fatalf("restored %d", restored)
}
// Rows are keyed: losing the first keeps the focus put.
points = points[1:]
tt.Frame()
if !tt.Focused("RESTORE Tests green") {
t.Fatal("the focus left with the removed row")
}
off, restored = true, -1
tt.Frame()
click(t, tt, "RESTORE Tests green")
if restored != -1 {
t.Fatal("a disabled list restored")
}
points = nil
tt.Frame()
find(t, tt, "NO ITEMS")
}