diff options
Diffstat (limited to 'libpanto-go/panto/panto_test.go')
| -rw-r--r-- | libpanto-go/panto/panto_test.go | 213 |
1 files changed, 213 insertions, 0 deletions
diff --git a/libpanto-go/panto/panto_test.go b/libpanto-go/panto/panto_test.go new file mode 100644 index 0000000..1270ead --- /dev/null +++ b/libpanto-go/panto/panto_test.go @@ -0,0 +1,213 @@ +package panto + +import "testing" + +// memStore is a minimal custom SessionStore that mints its sessions via +// NewSession (gap #1) rather than dispatching back into the vtable. +type memStore struct { + self *GoSessionStore + created bool +} + +func (m *memStore) Create() (*Session, error) { + m.created = true + return NewSession(m.self, SessionInfo{ID: "sess-1"}) +} +func (m *memStore) List() ([]SessionInfo, error) { return nil, nil } +func (m *memStore) Resolve(id string) (*Session, bool, error) { + if id != "sess-1" { + return nil, false, nil + } + s, err := NewSession(m.self, SessionInfo{ID: id}) + return s, err == nil, err +} +func (m *memStore) Latest() (*Session, bool, error) { return m.Resolve("sess-1") } +func (m *memStore) Load(id string) (*Conversation, bool, error) { return nil, false, nil } +func (m *memStore) AppendMessages(string, []PersistentMessage) error { + return nil +} + +// Gap #1: a Go store can mint and return a *Session from its own Create() +// without recursing back into the vtable. +func TestCustomStoreMintsSession(t *testing.T) { + Init() + defer Deinit() + + impl := &memStore{} + gss, err := NewGoSessionStore(impl) + if err != nil { + t.Fatalf("NewGoSessionStore: %v", err) + } + defer gss.Close() + impl.self = gss + + sess, err := gss.Create() + if err != nil { + t.Fatalf("Create: %v", err) + } + if sess == nil || sess.ptr == nil { + t.Fatal("Create returned nil session") + } + if !impl.created { + t.Fatal("custom Create() was not invoked") + } + + got, ok, err := gss.Resolve("sess-1") + if err != nil || !ok || got == nil { + t.Fatalf("Resolve: got=%v ok=%v err=%v", got, ok, err) + } +} + +// Gap #3: rebuild a conversation containing tool_use / tool_result / thinking +// via the value-block API, then read the blocks back losslessly. +func TestMessageBuilderRoundTrip(t *testing.T) { + Init() + defer Deinit() + + conv, err := NewConversation() + if err != nil { + t.Fatalf("NewConversation: %v", err) + } + defer conv.Close() + + if err := conv.AddMessage(UserMessage( + ToolResultBlock("call-1", "result text", false), + )); err != nil { + t.Fatalf("AddMessage(user): %v", err) + } + + if err := conv.AddMessage(AssistantMessage(&Usage{Input: 10, Output: 5}, + TextBlock("let me search"), + ThinkingBlock("hmm", "sig", AnthropicMessages, "https://api", "claude"), + ToolUseBlock("call-1", "search", `{"q":1}`), + )); err != nil { + t.Fatalf("AddMessage(assistant): %v", err) + } + + if got := conv.MessageCount(); got != 2 { + t.Fatalf("MessageCount = %d, want 2", got) + } + + user := conv.Message(0).Snapshot() + if user.Role != RoleUser || len(user.Content) != 1 { + t.Fatalf("user message: role=%v blocks=%d", user.Role, len(user.Content)) + } + if tr := user.Content[0]; tr.Tag != BlockToolResult || tr.ToolID != "call-1" || tr.Text != "result text" || tr.IsError { + t.Fatalf("tool_result block: %+v", tr) + } + + asst := conv.Message(1).Snapshot() + if asst.Role != RoleAssistant || len(asst.Content) != 3 { + t.Fatalf("assistant message: role=%v blocks=%d", asst.Role, len(asst.Content)) + } + if b := asst.Content[0]; b.Tag != BlockText || b.Text != "let me search" { + t.Fatalf("text block: %+v", b) + } + if b := asst.Content[1]; b.Tag != BlockThinking || b.Text != "hmm" { + t.Fatalf("thinking block: %+v", b) + } + if b := asst.Content[2]; b.Tag != BlockToolUse || b.ToolID != "call-1" || b.ToolName != "search" || b.Text != `{"q":1}` { + t.Fatalf("tool_use block: %+v", b) + } + if asst.Usage == nil || asst.Usage.Input != 10 || asst.Usage.Output != 5 { + t.Fatalf("assistant usage: %+v", asst.Usage) + } +} + +// TestMessageBuilderSetIdentity rebuilds a message with a per-message wire +// identity (and no per-block thinking origin), the resumption path that lets +// callers avoid copying the identity onto every thinking block. +func TestMessageBuilderSetIdentity(t *testing.T) { + Init() + defer Deinit() + + conv, err := NewConversation() + if err != nil { + t.Fatalf("NewConversation: %v", err) + } + defer conv.Close() + + // Thinking block with a signature but NO per-block origin; provenance is + // carried at the message level instead, via WithIdentity. + msg := AssistantMessage(&Usage{Input: 10, Output: 5}, + ThinkingBlock("reasoned", "sig", OpenAIChat, "", ""), + TextBlock("answer"), + ).WithIdentity(MessageIdentity{ + APIStyle: AnthropicMessages, + BaseURL: "https://api.anthropic.com", + Model: "claude", + Reasoning: ReasoningDefault, + }) + if err := conv.AddMessage(msg); err != nil { + t.Fatalf("AddMessage: %v", err) + } + + asst := conv.Message(0).Snapshot() + if asst.Role != RoleAssistant || len(asst.Content) != 2 { + t.Fatalf("assistant message: role=%v blocks=%d", asst.Role, len(asst.Content)) + } + if b := asst.Content[0]; b.Tag != BlockThinking || b.Text != "reasoned" { + t.Fatalf("thinking block: %+v", b) + } +} + +func testConfig() Config { + return Config{ + Provider: ProviderConfig{ + OpenAIChat: &OpenAIChatConfig{ + APIKey: "test-key", + BaseURL: "http://localhost", + Model: "test-model", + MaxTokens: 16, + }, + }, + } +} + +func TestOpenAIResponsesProviderConfigPassesThrough(t *testing.T) { + provider := ProviderConfig{ + OpenAIResponses: &OpenAIResponsesConfig{ + APIKey: "test-key", + BaseURL: "http://localhost", + Model: "test-model", + Reasoning: ReasoningMedium, + MaxTokens: 128, + }, + } + c, strings, err := provider.c() + if err != nil { + t.Fatalf("ProviderConfig.c: %v", err) + } + defer strings.free() + + if APIStyle(c.tag) != OpenAIResponses { + t.Fatalf("tag = %v, want %v", APIStyle(c.tag), OpenAIResponses) + } + + Init() + defer Deinit() + + a, err := NewAgent(Config{Provider: provider}) + if err != nil { + t.Fatalf("NewAgent: %v", err) + } + defer a.Close() +} + +func TestNewAgentSetSystemPromptDoesNotCrash(t *testing.T) { + Init() + defer Deinit() + + a, err := NewAgent(testConfig()) + if err != nil { + t.Fatalf("NewAgent: %v", err) + } + defer a.Close() + + if err := a.SetSystemPrompt("be brief"); err != nil { + t.Fatalf("SetSystemPrompt: %v", err) + } + if err := a.AddSystemMessage("also be accurate"); err != nil { + t.Fatalf("AddSystemMessage: %v", err) + } +} |
