Files
8bitMyGoUI/agent/party_test.go
T
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

184 lines
7.1 KiB
Go

package agent_test
import (
"testing"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/agent"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
)
func TestAgentCardInksHPByContextLeft(t *testing.T) {
for used, want := range map[int]pixel.Slot{20_000: pixel.Good, 120_000: pixel.Warn, 180_000: pixel.Bad} {
a := agent.Agent{Name: "ROBO", Class: "CODER", Level: 7, Used: used, Window: 200_000}
tt := view(300, 120, newWorld(), func(c *ui.Context) { agent.AgentCard(c, a, agent.AgentCardOptions{}) })
x, y, _, _ := dots(find(t, tt, "ROBO"))
// The fill starts two dots into the gauge's well.
fill := count(tt.Image(), x+38, y+40, 92, 4, want)
if left := 92 * (200_000 - used) / 200_000; fill < left*4-8 || fill > left*4+8 {
t.Errorf("%d of 200K used: %d dots of %v, want about %d", used, fill, want, left*4)
}
}
}
func TestToolCallStripesItsOutcome(t *testing.T) {
for out, ink := range map[agent.Outcome]pixel.Slot{agent.Casting: pixel.Info, agent.OK: pixel.Good, agent.Failed: pixel.Bad} {
wd := newWorld()
wd.reduce = true
tt := view(500, 100, wd, func(c *ui.Context) {
agent.ToolCall(c, "read_file", out, agent.ToolCallOptions{Args: "main.go"})
})
x, y, w, h := dots(find(t, tt, "CAST read_file"))
if got := at(tt.Image(), x+2, y+5); got != ink || h != 38 {
t.Errorf("outcome %d: stripe %v, want %v; %d dots high", out, got, ink, h)
}
// Casting is busy: it moves even under Reduce Motion.
if moving := moves(tt, wd); moving != (out == agent.Casting) {
t.Errorf("outcome %d: moving is %v", out, moving)
}
if out == agent.Failed && count(tt.Image(), x+w-60, y, 56, 22, pixel.Good) > 0 {
t.Error("a failed cast shows Good")
}
}
}
func TestPlanQuestMarksEachObjective(t *testing.T) {
steps := []agent.PlanStep{{Text: "Read the code", Done: true}, {Text: "Write a test"}, {Text: "Ship it"}}
tt := view(400, 200, newWorld(), func(c *ui.Context) { agent.PlanQuest(c, "Fix the bug", steps, agent.PlanQuestOptions{}) })
x, y, _, h := dots(find(t, tt, "Fix the bug"))
row := func(i int, s pixel.Slot) int { return count(tt.Image(), x, y+20+i*16, 12, 16, s) }
if row(0, pixel.Good) == 0 || row(1, pixel.Accent) == 0 || row(2, pixel.Well) == 0 || row(2, pixel.Accent) > 0 {
t.Fatal("not a check, a ▶ and an open box")
}
for i := range steps {
steps[i].Done = true
}
tt.Frame()
_, _, _, done := dots(find(t, tt, "Fix the bug"))
if done != h+18 || count(tt.Image(), x, y+h, 200, 18, pixel.Good) == 0 {
t.Fatalf("all done: %d dots high, was %d, and no QUEST COMPLETE!", done, h)
}
}
func TestThinkingOrbSwirlsOnlyWhileThinking(t *testing.T) {
for _, thinking := range []bool{false, true} {
wd := newWorld()
wd.reduce = true
tt := view(100, 100, wd, func(c *ui.Context) { agent.ThinkingOrb(c, "Mind", thinking, agent.ThinkingOrbOptions{}) })
x, y, _, _ := dots(find(t, tt, "Mind"))
if moves(tt, wd) != thinking || (count(tt.Image(), x+3, y+1, 39, 39, pixel.Magic) > 0) != thinking {
t.Errorf("thinking %v: the swirl does not match", thinking)
}
}
}
func TestTokenManaRunsDry(t *testing.T) {
for used, dry := range map[int]bool{0: false, 99_999: false, 100_000: true, 250_000: true} {
tt := view(300, 100, newWorld(), func(c *ui.Context) {
agent.TokenMana(c, "Budget", used, 100_000, agent.TokenManaOptions{})
})
x, y, _, _ := dots(find(t, tt, "Budget"))
img := tt.Image()
if got := count(img, x+2, y+9, 116, 4, pixel.Bad) > 0; got != dry {
t.Errorf("%d used: dry veil %v", used, got)
}
if full := count(img, x+2, y+9, 116, 4, pixel.Info); used == 0 && full < 116*4-9*4 {
t.Errorf("a full budget fills %d dots", full)
}
}
}
func TestAgentStatusRings(t *testing.T) {
for s, want := range map[agent.Status]struct {
ink pixel.Slot
moves, still bool
}{agent.Idle: {pixel.Faint, true, false}, agent.Working: {pixel.Info, true, true}, agent.Blocked: {pixel.Warn, false, false}, agent.Done: {pixel.Good, false, false}} {
for _, reduce := range []bool{false, true} {
wd := newWorld()
wd.reduce = reduce
tt := view(200, 200, wd, func(c *ui.Context) { agent.AgentStatus(c, "Robo", s, agent.AgentStatusOptions{}) })
x, y, w, _ := dots(find(t, tt, "Robo"))
if count(tt.Image(), x, y, w, 36, want.ink) == 0 {
t.Errorf("status %d: no %v ring", s, want.ink)
}
// Working runs on; Idle's Z rests under Reduce Motion.
if got, wantMove := moves(tt, wd), want.moves && (!reduce || want.still); got != wantMove {
t.Errorf("status %d, reduce %v: moving %v", s, reduce, got)
}
}
}
}
func TestSubAgentPartyDrawsTheSummoning(t *testing.T) {
party := []agent.Member{{Name: "LEAD", Status: agent.Working, Summoned: []agent.Member{
{Name: "SCOUT", Status: agent.Done}, {Name: "SMITH", Status: agent.Blocked, Summoned: []agent.Member{{Name: "IMP", Status: agent.Idle}}},
}}}
tt := view(400, 200, newWorld(), func(c *ui.Context) { agent.SubAgentParty(c, "Party", party, agent.SubAgentPartyOptions{}) })
x, y, _, h := dots(find(t, tt, "Party"))
img := tt.Image()
if h != 4*16 {
t.Fatalf("four members in %d dots", h)
}
// Robot inks: LEAD Info, SCOUT Good, SMITH Warn, IMP Faint.
for i, s := range []pixel.Slot{pixel.Info, pixel.Good, pixel.Warn, pixel.Faint} {
if count(img, x+[]int{0, 12, 12, 24}[i], y+i*16+2, 12, 12, s) == 0 {
t.Errorf("row %d has no %v robot", i, s)
}
}
// SCOUT's line runs on to SMITH; SMITH's ends at IMP's.
if at(img, x+5, y+16+12) != pixel.Edge || at(img, x+17, y+48+12) == pixel.Edge {
t.Error("the summoning lines run wrong")
}
}
func TestDiffScrollSharesBlocks(t *testing.T) {
files := []agent.FileDiff{{Path: "a.go", Added: 10}, {Path: "b.go", Removed: 10}, {Path: "c.go"}}
tt := view(500, 200, newWorld(), func(c *ui.Context) { agent.DiffScroll(c, "Changes", files, agent.DiffScrollOptions{}) })
x, y, w, _ := dots(find(t, tt, "Changes"))
img := tt.Image()
for i, want := range [][5]pixel.Slot{
{pixel.Good, pixel.Good, pixel.Good, pixel.Good, pixel.Good}, {pixel.Bad, pixel.Bad, pixel.Bad, pixel.Bad, pixel.Bad},
{pixel.Shade, pixel.Shade, pixel.Shade, pixel.Shade, pixel.Shade}, {pixel.Good, pixel.Good, pixel.Good, pixel.Bad, pixel.Bad},
} {
// The total row sits four dots lower, past the rule.
ry := y + 10 + i*16 + 6
if i == 3 {
ry += 4
}
for j, s := range want {
if got := at(img, x+w-32+j*5+1, ry+1); got != s {
t.Errorf("row %d block %d is %v, want %v", i, j, got, s)
}
}
}
}
func TestCostCoinsDropOnARise(t *testing.T) {
for _, reduce := range []bool{false, true} {
wd := newWorld()
wd.reduce = reduce
cents := 1234
tt := view(200, 200, wd, func(c *ui.Context) { agent.CostCoins(c, "Spent", cents, agent.CostCoinsOptions{}) })
x, y, _, _ := dots(find(t, tt, "Spent"))
// Bronze cents stack third; a fifth coin lands at 26.
air := func() int {
img := tt.Image()
return count(img, x+38, y+21, 10, 2, pixel.Warn) + count(img, x+38, y+21, 10, 2, pixel.Bad)
}
if air() > 0 {
t.Fatal("a coin hangs before any change")
}
cents = 1235
tt.Frame()
if got := air() > 0; got == reduce {
t.Errorf("reduce %v: the new coin falling is %v", reduce, got)
}
later(tt, wd, time.Second)
if air() > 0 {
t.Errorf("reduce %v: the coin never lands", reduce)
}
}
}