summaryrefslogtreecommitdiff
path: root/libpanto-go/panto_test.go
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-18 13:38:32 -0600
committert <t@tjp.lol>2026-06-18 13:39:49 -0600
commit7c2d8825660f73198149c0b2ce26166b16ba3532 (patch)
treeae57a23e5dcb905b29718c63a0ed2f6c65d93d53 /libpanto-go/panto_test.go
parentfc77c3900ef80ff0e2934c03ca37dfc309a6e7b9 (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.go126
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{