Files
BlackBean/pigo/internal/agentcore/helpers_test.go
T
2026-08-14 23:41:57 +08:00

142 lines
3.9 KiB
Go

package agentcore
import (
"encoding/json"
"testing"
)
func TestContentToText(t *testing.T) {
cases := []struct {
name string
list ContentList
want string
}{
{"empty", nil, ""},
{"single text", ContentList{NewTextContent("hello")}, "hello"},
{
"skips non-text blocks",
ContentList{
NewTextContent("a"),
NewThinkingContent("ignored"),
NewToolCallContent("c1", "ls", json.RawMessage(`{}`)),
NewTextContent("b"),
NewImageContent("data", "image/png"),
},
"ab",
},
{
"only non-text",
ContentList{NewThinkingContent("x"), NewImageContent("d", "image/png")},
"",
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := ContentToText(tc.list); got != tc.want {
t.Errorf("ContentToText = %q, want %q", got, tc.want)
}
})
}
}
func TestLastAssistantOf(t *testing.T) {
t.Run("nil when absent", func(t *testing.T) {
msgs := []AgentMessage{
UserMessage{RoleField: RoleUser, Content: ContentList{NewTextContent("hi")}},
ToolResultMessage{RoleField: RoleToolResult, ToolCallID: "c1"},
}
if got := LastAssistantOf(msgs); got != nil {
t.Errorf("want nil, got %+v", got)
}
})
t.Run("nil for empty slice", func(t *testing.T) {
if got := LastAssistantOf(nil); got != nil {
t.Errorf("want nil, got %+v", got)
}
})
t.Run("returns last assistant", func(t *testing.T) {
msgs := []AgentMessage{
AssistantMessage{RoleField: RoleAssistant, Model: "first"},
UserMessage{RoleField: RoleUser},
AssistantMessage{RoleField: RoleAssistant, Model: "last"},
ToolResultMessage{RoleField: RoleToolResult},
}
got := LastAssistantOf(msgs)
if got == nil {
t.Fatal("want an assistant message, got nil")
}
if got.Model != "last" {
t.Errorf("want the last assistant (model %q), got %q", "last", got.Model)
}
})
}
func TestToolCallsEmpty(t *testing.T) {
m := AssistantMessage{
RoleField: RoleAssistant,
Content: ContentList{NewTextContent("no tools here"), NewThinkingContent("hmm")},
}
if calls := m.ToolCalls(); calls != nil {
t.Errorf("want nil for a message with no tool calls, got %+v", calls)
}
}
func TestToolCallsPreservesOrder(t *testing.T) {
m := AssistantMessage{
RoleField: RoleAssistant,
Content: ContentList{
NewToolCallContent("c1", "read", json.RawMessage(`{}`)),
NewTextContent("between"),
NewToolCallContent("c2", "write", json.RawMessage(`{}`)),
},
}
calls := m.ToolCalls()
if len(calls) != 2 {
t.Fatalf("want 2 tool calls, got %d", len(calls))
}
if calls[0].ID != "c1" || calls[1].ID != "c2" {
t.Errorf("tool call order lost: %+v", calls)
}
}
// TestNewContentConstructorsSetType guards the invariant that every constructor
// sets its type discriminant, so a marshalled block always carries a "type".
func TestNewContentConstructorsSetType(t *testing.T) {
cases := []struct {
got Content
want string
}{
{NewTextContent("t"), ContentTypeText},
{NewThinkingContent("th"), ContentTypeThinking},
{NewToolCallContent("id", "n", json.RawMessage(`{}`)), ContentTypeToolCall},
{NewImageContent("d", "image/png"), ContentTypeImage},
}
for _, tc := range cases {
data, err := json.Marshal(tc.got)
if err != nil {
t.Fatalf("marshal %T: %v", tc.got, err)
}
var probe struct {
Type string `json:"type"`
}
if err := json.Unmarshal(data, &probe); err != nil {
t.Fatalf("unmarshal probe %T: %v", tc.got, err)
}
if probe.Type != tc.want {
t.Errorf("%T type = %q, want %q", tc.got, probe.Type, tc.want)
}
}
}
func TestRoleAccessors(t *testing.T) {
if got := (UserMessage{}).Role(); got != RoleUser {
t.Errorf("UserMessage.Role = %q, want %q", got, RoleUser)
}
if got := (AssistantMessage{}).Role(); got != RoleAssistant {
t.Errorf("AssistantMessage.Role = %q, want %q", got, RoleAssistant)
}
if got := (ToolResultMessage{}).Role(); got != RoleToolResult {
t.Errorf("ToolResultMessage.Role = %q, want %q", got, RoleToolResult)
}
}