diff options
| author | t <t@tjp.lol> | 2026-06-18 13:38:32 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-18 13:39:49 -0600 |
| commit | 7c2d8825660f73198149c0b2ce26166b16ba3532 (patch) | |
| tree | ae57a23e5dcb905b29718c63a0ed2f6c65d93d53 /libpanto-go/panto_test.go | |
| parent | fc77c3900ef80ff0e2934c03ca37dfc309a6e7b9 (diff) | |
Preserve content blocks and thinking origin in C store append
Round-trip persistent message content through the C session store bridge
instead of dropping it, including tool-use, tool-result, system, and
thinking blocks. Reconstruct borrowed C content blocks with arena-backed
helpers, carry message identity fields like API style/model/reasoning, and
add tests covering append marshalling and thinking signature origin
round-tripping.
Diffstat (limited to 'libpanto-go/panto_test.go')
| -rw-r--r-- | libpanto-go/panto_test.go | 126 |
1 files changed, 126 insertions, 0 deletions
diff --git a/libpanto-go/panto_test.go b/libpanto-go/panto_test.go index ef21f1e..5cfed01 100644 --- a/libpanto-go/panto_test.go +++ b/libpanto-go/panto_test.go @@ -2,6 +2,132 @@ 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 MessageBuilder, 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() + + ub, err := NewMessageBuilder(RoleUser) + if err != nil { + t.Fatalf("NewMessageBuilder(user): %v", err) + } + if err := ub.AddToolResult("call-1", "result text", false); err != nil { + t.Fatalf("AddToolResult: %v", err) + } + if err := conv.AddMessage(ub, nil); err != nil { + t.Fatalf("AddMessage(user): %v", err) + } + + ab, err := NewMessageBuilder(RoleAssistant) + if err != nil { + t.Fatalf("NewMessageBuilder(assistant): %v", err) + } + if err := ab.AddText("let me search"); err != nil { + t.Fatalf("AddText: %v", err) + } + if err := ab.AddThinking("hmm", "sig", AnthropicMessages, "https://api", "claude"); err != nil { + t.Fatalf("AddThinking: %v", err) + } + if err := ab.AddToolUse("call-1", "search", `{"q":1}`); err != nil { + t.Fatalf("AddToolUse: %v", err) + } + if err := conv.AddMessage(ab, &Usage{Input: 10, Output: 5}); 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) + } +} + func testConfig() Config { return Config{ Provider: ProviderConfig{ |
