1package core
2
3import (
4 "os"
5 "testing"
6 "time"
7
8 "github.com/patriceckhart/zot/internal/provider"
9)
10
11func TestSessionRoundTrip(t *testing.T) {
12 dir := t.TempDir()
13 os.Setenv("ZOT_HOME", dir)
14
15 sess, err := NewSession(dir, "/tmp/project", "anthropic", "claude-sonnet-4-5", "test")
16 if err != nil {
17 t.Fatal(err)
18 }
19 msg := provider.Message{
20 Role: provider.RoleUser,
21 Content: []provider.Content{
22 provider.TextBlock{Text: "hello"},
23 },
24 Time: time.Now().UTC(),
25 }
26 if err := sess.AppendMessage(msg); err != nil {
27 t.Fatal(err)
28 }
29 if err := sess.Close(); err != nil {
30 t.Fatal(err)
31 }
32
33 reopened, msgs, err := OpenSession(sess.Path)
34 if err != nil {
35 t.Fatal(err)
36 }
37 // OpenSession returns a live append writer; close it before t.TempDir
38 // runs cleanup, otherwise windows refuses to delete the open file.
39 t.Cleanup(func() { _ = reopened.Close() })
40 if len(msgs) != 1 {
41 t.Fatalf("got %d messages", len(msgs))
42 }
43 tb, ok := msgs[0].Content[0].(provider.TextBlock)
44 if !ok || tb.Text != "hello" {
45 t.Fatalf("got %+v", msgs[0])
46 }
47}
48
49// TestNewSessionAtPathCreatesAtExplicitPath proves the swarm child's
50// session-persistence fallback works: NewSessionAtPath creates the
51// file (and parent dirs) at the exact path the caller chose,
52// independent of SessionsDir(root, cwd). Without this helper, the
53// swarm child's --session <path> with a non-existent path would
54// fall through to NewSession and write the session under a
55// different (autogenerated) name, leaving the supervisor-chosen
56// path empty.
57func TestNewSessionAtPathCreatesAtExplicitPath(t *testing.T) {
58 dir := t.TempDir()
59 want := dir + "/nested/sub/session.json"
60
61 s, err := NewSessionAtPath(want, "/tmp/proj", "anthropic", "claude", "test")
62 if err != nil {
63 t.Fatalf("NewSessionAtPath: %v", err)
64 }
65 if s.Path != want {
66 t.Errorf("Path = %q; want %q", s.Path, want)
67 }
68 if err := s.AppendMessage(provider.Message{
69 Role: provider.RoleUser,
70 Content: []provider.Content{provider.TextBlock{Text: "hi"}},
71 Time: time.Now().UTC(),
72 }); err != nil {
73 t.Fatal(err)
74 }
75 if err := s.Close(); err != nil {
76 t.Fatal(err)
77 }
78
79 // Reopen at the same path and the message must still be there.
80 reopened, msgs, err := OpenSession(want)
81 if err != nil {
82 t.Fatalf("OpenSession at fixed path: %v", err)
83 }
84 t.Cleanup(func() { _ = reopened.Close() })
85 if len(msgs) != 1 {
86 t.Fatalf("reopen got %d msgs; want 1", len(msgs))
87 }
88
89 // A second call to NewSessionAtPath at the same path must fail
90 // (O_EXCL): callers should use OpenSession to reattach to an
91 // existing file.
92 if _, err := NewSessionAtPath(want, "/tmp/proj", "anthropic", "claude", "test"); err == nil {
93 t.Error("NewSessionAtPath on existing path returned nil; want error")
94 }
95}
96
97func TestCostAdd(t *testing.T) {
98 var c CostTracker
99 c.Add(provider.Usage{InputTokens: 100, OutputTokens: 50, CostUSD: 0.01})
100 c.Add(provider.Usage{InputTokens: 200, OutputTokens: 25, CostUSD: 0.02})
101 if c.Total.InputTokens != 300 || c.Total.OutputTokens != 75 {
102 t.Fatalf("got %+v", c.Total)
103 }
104 if c.Total.CostUSD < 0.0299 || c.Total.CostUSD > 0.0301 {
105 t.Fatalf("got cost %v", c.Total.CostUSD)
106 }
107}