summaryrefslogtreecommitdiff
path: root/libpanto-go/panto/panto_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'libpanto-go/panto/panto_test.go')
-rw-r--r--libpanto-go/panto/panto_test.go213
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)
+ }
+}