first commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"github.com/smallnest/pigo/internal/agentcore"
|
||||
)
|
||||
|
||||
// drain collects every msg queued on ch without blocking, stopping at the first
|
||||
// would-block. The bridge sends are synchronous into a buffered channel, so once
|
||||
// the fake sequence has been driven the msgs are all present and this returns
|
||||
// them in order.
|
||||
func drain(ch chan tea.Msg) []tea.Msg {
|
||||
var out []tea.Msg
|
||||
for {
|
||||
select {
|
||||
case m := <-ch:
|
||||
out = append(out, m)
|
||||
default:
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStreamHandlerConversion drives a synthetic event sequence directly through
|
||||
// the StreamHandler the bridge builds and asserts each callback produces the
|
||||
// matching tea.Msg, in order. It fakes the run entirely (no provider), exercising
|
||||
// the callback→msg conversion + channel ordering in isolation.
|
||||
func TestStreamHandlerConversion(t *testing.T) {
|
||||
ch := newEventChan()
|
||||
h := newStreamHandler(ch, nil)
|
||||
|
||||
// A representative sequence: two text deltas, a tool start/update/end, a
|
||||
// telemetry summary, a compaction, and a turn end.
|
||||
h.OnText("Hello ")
|
||||
h.OnText("world")
|
||||
h.OnEvent(agentcore.ToolExecutionStartEvent{
|
||||
ToolCallID: "call-1",
|
||||
ToolName: "read_file",
|
||||
Args: map[string]any{"path": "/tmp/x"},
|
||||
})
|
||||
h.OnEvent(agentcore.ToolExecutionUpdateEvent{
|
||||
ToolCallID: "call-1",
|
||||
ToolName: "read_file",
|
||||
PartialResult: agentcore.AgentToolResult{Content: agentcore.ContentList{agentcore.NewTextContent("partial")}},
|
||||
})
|
||||
h.OnEvent(agentcore.ToolExecutionEndEvent{
|
||||
ToolCallID: "call-1",
|
||||
ToolName: "read_file",
|
||||
Result: agentcore.AgentToolResult{Content: agentcore.ContentList{agentcore.NewTextContent("done")}},
|
||||
IsError: false,
|
||||
})
|
||||
h.OnEvent(agentcore.TelemetryEvent{Turns: 3})
|
||||
h.OnEvent(agentcore.CompactionEvent{Reason: "threshold"})
|
||||
h.OnTurnEnd(
|
||||
agentcore.AssistantMessage{Content: agentcore.ContentList{agentcore.NewTextContent("Hello world")}},
|
||||
[]agentcore.ToolResultMessage{{ToolCallID: "call-1"}},
|
||||
)
|
||||
// Simulate drain-done: the pump appends runEndMsg after DrainStream returns.
|
||||
ch <- runEndMsg{err: nil}
|
||||
|
||||
got := drain(ch)
|
||||
|
||||
if len(got) != 9 {
|
||||
t.Fatalf("expected 9 msgs, got %d: %#v", len(got), got)
|
||||
}
|
||||
|
||||
if m, ok := got[0].(textDeltaMsg); !ok || m.delta != "Hello " {
|
||||
t.Errorf("msg[0] = %#v, want textDeltaMsg{delta:%q}", got[0], "Hello ")
|
||||
}
|
||||
if m, ok := got[1].(textDeltaMsg); !ok || m.delta != "world" {
|
||||
t.Errorf("msg[1] = %#v, want textDeltaMsg{delta:%q}", got[1], "world")
|
||||
}
|
||||
if m, ok := got[2].(toolStartMsg); !ok || m.id != "call-1" || m.name != "read_file" || m.input["path"] != "/tmp/x" {
|
||||
t.Errorf("msg[2] = %#v, want toolStartMsg for call-1", got[2])
|
||||
}
|
||||
if m, ok := got[3].(toolUpdateMsg); !ok || m.id != "call-1" || m.partial != "partial" {
|
||||
t.Errorf("msg[3] = %#v, want toolUpdateMsg{partial:%q}", got[3], "partial")
|
||||
}
|
||||
if m, ok := got[4].(toolEndMsg); !ok || m.id != "call-1" || !m.ok || m.result != "done" {
|
||||
t.Errorf("msg[4] = %#v, want toolEndMsg{ok:true, result:%q}", got[4], "done")
|
||||
}
|
||||
if m, ok := got[5].(telemetryMsg); !ok || m.ev.Turns != 3 {
|
||||
t.Errorf("msg[5] = %#v, want telemetryMsg{Turns:3}", got[5])
|
||||
}
|
||||
if _, ok := got[6].(compactionMsg); !ok {
|
||||
t.Errorf("msg[6] = %#v, want compactionMsg", got[6])
|
||||
}
|
||||
if m, ok := got[7].(turnEndMsg); !ok || len(m.results) != 1 || m.results[0].ToolCallID != "call-1" {
|
||||
t.Errorf("msg[7] = %#v, want turnEndMsg with one result", got[7])
|
||||
}
|
||||
if m, ok := got[8].(runEndMsg); !ok || m.err != nil {
|
||||
t.Errorf("msg[8] = %#v, want runEndMsg{err:nil}", got[8])
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolEndError verifies the ok flag inverts IsError.
|
||||
func TestToolEndError(t *testing.T) {
|
||||
ch := newEventChan()
|
||||
h := newStreamHandler(ch, nil)
|
||||
h.OnEvent(agentcore.ToolExecutionEndEvent{
|
||||
ToolCallID: "c",
|
||||
Result: agentcore.AgentToolResult{Content: agentcore.ContentList{agentcore.NewTextContent("boom")}},
|
||||
IsError: true,
|
||||
})
|
||||
got := drain(ch)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected 1 msg, got %d", len(got))
|
||||
}
|
||||
m, ok := got[0].(toolEndMsg)
|
||||
if !ok || m.ok || m.result != "boom" {
|
||||
t.Errorf("got %#v, want toolEndMsg{ok:false, result:%q}", got[0], "boom")
|
||||
}
|
||||
}
|
||||
|
||||
// TestArgsToMap covers the object / non-object coercion of tool call args.
|
||||
func TestArgsToMap(t *testing.T) {
|
||||
if m := argsToMap(map[string]any{"k": "v"}); m == nil || m["k"] != "v" {
|
||||
t.Errorf("object args: got %#v, want map with k=v", m)
|
||||
}
|
||||
if m := argsToMap("not-an-object"); m != nil {
|
||||
t.Errorf("non-object args: got %#v, want nil", m)
|
||||
}
|
||||
if m := argsToMap(nil); m != nil {
|
||||
t.Errorf("nil args: got %#v, want nil", m)
|
||||
}
|
||||
// The tool executor emits Args as json.RawMessage; the card must decode it.
|
||||
if m := argsToMap(json.RawMessage(`{"command":"ls -la"}`)); m == nil || m["command"] != "ls -la" {
|
||||
t.Errorf("raw JSON object args: got %#v, want map with command=ls -la", m)
|
||||
}
|
||||
if m := argsToMap(json.RawMessage(`"just a string"`)); m != nil {
|
||||
t.Errorf("raw JSON non-object: got %#v, want nil", m)
|
||||
}
|
||||
if m := argsToMap(json.RawMessage(nil)); m != nil {
|
||||
t.Errorf("empty raw JSON: got %#v, want nil", m)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubAgentProgressConversion verifies a SubAgentProgressEvent maps to a
|
||||
// subagentProgressMsg carrying the id (parent task tool-call id), description,
|
||||
// activity, and token estimate.
|
||||
func TestSubAgentProgressConversion(t *testing.T) {
|
||||
ch := newEventChan()
|
||||
h := newStreamHandler(ch, nil)
|
||||
h.OnEvent(agentcore.SubAgentProgressEvent{
|
||||
ToolCallID: "task-1",
|
||||
Description: "build parser",
|
||||
Activity: "Editing",
|
||||
Tokens: 256,
|
||||
})
|
||||
got := drain(ch)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("expected 1 msg, got %d", len(got))
|
||||
}
|
||||
m, ok := got[0].(subagentProgressMsg)
|
||||
if !ok {
|
||||
t.Fatalf("got %#v, want subagentProgressMsg", got[0])
|
||||
}
|
||||
if m.id != "task-1" || m.desc != "build parser" || m.activity != "Editing" || m.tokens != 256 {
|
||||
t.Errorf("got %#v, want {id:task-1 desc:build parser activity:Editing tokens:256}", m)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWaitForEvent verifies the pump Cmd returns the next queued msg.
|
||||
func TestWaitForEvent(t *testing.T) {
|
||||
ch := newEventChan()
|
||||
want := runEndMsg{err: errors.New("stop")}
|
||||
ch <- want
|
||||
cmd := waitForEvent(ch)
|
||||
if cmd == nil {
|
||||
t.Fatal("waitForEvent returned nil Cmd")
|
||||
}
|
||||
got, ok := cmd().(runEndMsg)
|
||||
if !ok || got.err == nil || got.err.Error() != "stop" {
|
||||
t.Errorf("cmd() = %#v, want runEndMsg{err:stop}", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user