package core import ( "os" "testing" "time" "github.com/patriceckhart/zot/packages/provider" ) func TestSessionRoundTrip(t *testing.T) { dir := t.TempDir() os.Setenv("ZOT_HOME", dir) sess, err := NewSession(dir, "/tmp/project", "anthropic", "claude-sonnet-4-5", "test") if err != nil { t.Fatal(err) } msg := provider.Message{ Role: provider.RoleUser, Content: []provider.Content{ provider.TextBlock{Text: "hello"}, }, Time: time.Now().UTC(), } if err := sess.AppendMessage(msg); err != nil { t.Fatal(err) } if err := sess.Close(); err != nil { t.Fatal(err) } reopened, msgs, err := OpenSession(sess.Path) if err != nil { t.Fatal(err) } // OpenSession returns a live append writer; close it before t.TempDir // runs cleanup, otherwise windows refuses to delete the open file. t.Cleanup(func() { _ = reopened.Close() }) if len(msgs) != 1 { t.Fatalf("got %d messages", len(msgs)) } tb, ok := msgs[0].Content[0].(provider.TextBlock) if !ok || tb.Text != "hello" { t.Fatalf("got %+v", msgs[0]) } } func TestSessionRoundTripPreservesThoughtSignatures(t *testing.T) { dir := t.TempDir() sess, err := NewSession(dir, "/tmp/project", "google", "gemini-3-flash", "test") if err != nil { t.Fatal(err) } msg := provider.Message{ Role: provider.RoleAssistant, Content: []provider.Content{ provider.ReasoningBlock{Summary: "thinking"}, provider.TextBlock{Text: "answer", ThoughtSignature: "text-sig"}, provider.ImageBlock{MimeType: "image/png", Data: []byte("png"), ThoughtSignature: "image-sig"}, provider.ToolCallBlock{ ID: "call-1", Name: "read", Arguments: []byte(`{"path":"a"}`), ThoughtSignature: "tool-sig", }, }, } if err := sess.AppendMessage(msg); err != nil { t.Fatal(err) } if err := sess.AppendMessage(provider.Message{ Role: provider.RoleTool, Content: []provider.Content{provider.ToolResultBlock{ CallID: "call-1", Content: []provider.Content{provider.TextBlock{Text: "result"}}, }}, }); err != nil { t.Fatal(err) } if err := sess.Close(); err != nil { t.Fatal(err) } reopened, msgs, err := OpenSession(sess.Path) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = reopened.Close() }) if len(msgs) != 2 || len(msgs[0].Content) != 4 { t.Fatalf("round trip content = %+v", msgs) } if rb, ok := msgs[0].Content[0].(provider.ReasoningBlock); !ok || rb.Summary != "thinking" { t.Fatalf("reasoning block = %#v", msgs[0].Content[0]) } if tb, ok := msgs[0].Content[1].(provider.TextBlock); !ok || tb.ThoughtSignature != "text-sig" { t.Fatalf("text block = %#v", msgs[0].Content[1]) } if ib, ok := msgs[0].Content[2].(provider.ImageBlock); !ok || ib.ThoughtSignature != "image-sig" { t.Fatalf("image block = %#v", msgs[0].Content[2]) } if tc, ok := msgs[0].Content[3].(provider.ToolCallBlock); !ok || tc.ThoughtSignature != "tool-sig" { t.Fatalf("tool call block = %#v", msgs[0].Content[3]) } } // TestNewSessionAtPathCreatesAtExplicitPath proves the swarm child's // session-persistence fallback works: NewSessionAtPath creates the // file (and parent dirs) at the exact path the caller chose, // independent of SessionsDir(root, cwd). Without this helper, the // swarm child's --session with a non-existent path would // fall through to NewSession and write the session under a // different (autogenerated) name, leaving the supervisor-chosen // path empty. func TestNewSessionAtPathCreatesAtExplicitPath(t *testing.T) { dir := t.TempDir() want := dir + "/nested/sub/session.json" s, err := NewSessionAtPath(want, "/tmp/proj", "anthropic", "claude", "test") if err != nil { t.Fatalf("NewSessionAtPath: %v", err) } if s.Path != want { t.Errorf("Path = %q; want %q", s.Path, want) } if err := s.AppendMessage(provider.Message{ Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "hi"}}, Time: time.Now().UTC(), }); err != nil { t.Fatal(err) } if err := s.Close(); err != nil { t.Fatal(err) } // Reopen at the same path and the message must still be there. reopened, msgs, err := OpenSession(want) if err != nil { t.Fatalf("OpenSession at fixed path: %v", err) } t.Cleanup(func() { _ = reopened.Close() }) if len(msgs) != 1 { t.Fatalf("reopen got %d msgs; want 1", len(msgs)) } // A second call to NewSessionAtPath at the same path must fail // (O_EXCL): callers should use OpenSession to reattach to an // existing file. if _, err := NewSessionAtPath(want, "/tmp/proj", "anthropic", "claude", "test"); err == nil { t.Error("NewSessionAtPath on existing path returned nil; want error") } } func TestCostAdd(t *testing.T) { var c CostTracker c.Add(provider.Usage{InputTokens: 100, OutputTokens: 50, CostUSD: 0.01}) c.Add(provider.Usage{InputTokens: 200, OutputTokens: 25, CostUSD: 0.02}) if c.Total.InputTokens != 300 || c.Total.OutputTokens != 75 { t.Fatalf("got %+v", c.Total) } if c.Total.CostUSD < 0.0299 || c.Total.CostUSD > 0.0301 { t.Fatalf("got cost %v", c.Total.CostUSD) } }