1package modes
2
3import (
4 "errors"
5 "reflect"
6 "testing"
7 "time"
8
9 "github.com/patriceckhart/zot/packages/core"
10 "github.com/patriceckhart/zot/packages/provider"
11 "github.com/patriceckhart/zot/packages/tui"
12)
13
14type timestampSettingsTestStore struct {
15 SettingsStore
16 enabled bool
17 minutes int
18 dateMode string
19 err error
20}
21
22func (s *timestampSettingsTestStore) SetChatTimestamps(enabled bool) error {
23 s.enabled = enabled
24 return s.err
25}
26
27func (s *timestampSettingsTestStore) SetChatTimestampInterval(minutes int) error {
28 s.minutes = minutes
29 return s.err
30}
31
32func (s *timestampSettingsTestStore) SetChatTimestampDate(mode string) error {
33 s.dateMode = mode
34 return s.err
35}
36
37func TestChatTimestampDateSetting(t *testing.T) {
38 // Exercise repainting independently of the developer's terminal environment.
39 t.Setenv("TERM_PROGRAM", "")
40 store := ×tampSettingsTestStore{}
41 i := NewInteractive(InteractiveConfig{Terminal: &cleanupTestTerminal{}, SettingsStore: store})
42 item := i.chatTimestampDateSetting()
43 if item.options[item.choice].value != "day_start" {
44 t.Fatal("incorrect date default")
45 }
46 before, _ := i.chatCacheKeyLocked(80)
47 i.applySettingChange(settingsAction{Key: "chat_timestamp_date", StringValue: "every_message"})
48 if store.dateMode != "every_message" || i.view.ChatTimestampDate != "every_message" || i.cfg.ChatTimestampDate != "every_message" {
49 t.Fatal("date mode not applied or persisted")
50 }
51 after, _ := i.chatCacheKeyLocked(80)
52 if before == after {
53 t.Fatal("date setting missing from cache key")
54 }
55 store.err = errors.New("cannot save")
56 i.applyChatTimestampDate("day_start")
57 if i.view.ChatTimestampDate != "every_message" || i.statusErr == "" {
58 t.Fatal("failed save changed live date setting")
59 }
60 restarted := NewInteractive(InteractiveConfig{ChatTimestampDate: "every_message"})
61 if restarted.view.ChatTimestampDate != "every_message" {
62 t.Fatal("saved date mode not restored")
63 }
64}
65
66func TestChatTimestampSettings(t *testing.T) {
67 // Exercise repainting independently of the developer's terminal environment.
68 t.Setenv("TERM_PROGRAM", "")
69 store := ×tampSettingsTestStore{}
70 i := NewInteractive(InteractiveConfig{Terminal: &cleanupTestTerminal{}, SettingsStore: store})
71 if i.view.ChatTimestamps || tui.ChatTimestampInterval(i.view.ChatTimestampIntervalMinutes) != 0 {
72 t.Fatal("incorrect startup defaults")
73 }
74 i.openSettingsDialog()
75 found := false
76 for idx, item := range i.settingsDialog.items {
77 if item.key == "chat_timestamps" || item.key == "chat_timestamp_interval_minutes" {
78 t.Fatal("timestamp controls must not be top-level settings")
79 }
80 if item.key == "timestamps" {
81 found = true
82 i.settingsDialog.cursor = idx
83 }
84 }
85 if !found {
86 t.Fatal("missing timestamps submenu")
87 }
88 i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyEnter})
89 if i.settingsDialog.title != "settings: timestamps" || len(i.settingsDialog.items) != 3 {
90 t.Fatal("timestamps submenu did not open")
91 }
92 if item := i.settingsDialog.items[0]; item.key != "chat_timestamps" || item.value {
93 t.Fatal("missing disabled-by-default toggle")
94 }
95 item := i.settingsDialog.items[1]
96 if item.key != "chat_timestamp_interval_minutes" || item.options[item.choice].value != "0" {
97 t.Fatal("incorrect default interval selection")
98 }
99 dateItem := i.settingsDialog.items[2]
100 if dateItem.key != "chat_timestamp_date" || dateItem.options[dateItem.choice].value != "day_start" {
101 t.Fatal("missing date display setting")
102 }
103 i.settingsDialog.HandleKey(tui.Key{Kind: tui.KeyEsc})
104 if !i.settingsDialog.Active() || i.settingsDialog.title != "settings" {
105 t.Fatal("escape should return to parent settings")
106 }
107 before, _ := i.chatCacheKeyLocked(80)
108 i.applySettingChange(settingsAction{Key: "chat_timestamps", Value: true})
109 if !store.enabled || !i.cfg.ChatTimestamps || !i.view.ChatTimestamps {
110 t.Fatal("toggle not persisted or applied")
111 }
112 after, _ := i.chatCacheKeyLocked(80)
113 if before == after {
114 t.Fatal("toggle did not affect chat cache key")
115 }
116 i.applySettingChange(settingsAction{Key: "chat_timestamp_interval_minutes", StringValue: "0"})
117 if store.minutes != 0 || tui.ChatTimestampInterval(i.view.ChatTimestampIntervalMinutes) != 0 {
118 t.Fatal("zero interval not applied")
119 }
120 for _, value := range []string{"-1", "invalid"} {
121 i.applyChatTimestampInterval(value)
122 if tui.ChatTimestampInterval(i.view.ChatTimestampIntervalMinutes) != 0 {
123 t.Fatal("invalid interval changed state")
124 }
125 }
126 store.err = errors.New("cannot save")
127 i.applyChatTimestamps(false)
128 i.applyChatTimestampInterval("10")
129 if !i.view.ChatTimestamps || !i.cfg.ChatTimestamps || tui.ChatTimestampInterval(i.view.ChatTimestampIntervalMinutes) != 0 || i.statusErr == "" {
130 t.Fatal("failed save changed live state or lost the error")
131 }
132}
133
134func TestChatTimestampsSurviveSessionResume(t *testing.T) {
135 root := t.TempDir()
136 session, err := core.NewSession(root, root, "ollama", "test", "test")
137 if err != nil {
138 t.Fatal(err)
139 }
140 t.Cleanup(func() { _ = session.Close() })
141 start := time.Date(2026, 9, 22, 12, 0, 0, 0, time.Local)
142 messages := []provider.Message{
143 {Role: provider.RoleUser, Time: start, Content: []provider.Content{provider.TextBlock{Text: "hello"}}},
144 {Role: provider.RoleAssistant, Time: start.Add(5 * time.Minute), Content: []provider.Content{provider.TextBlock{Text: "world"}}},
145 {Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: "legacy message"}}},
146 }
147 for _, message := range messages {
148 if err := session.AppendMessage(message); err != nil {
149 t.Fatal(err)
150 }
151 }
152 if err := session.Close(); err != nil {
153 t.Fatal(err)
154 }
155 reopened, restored, err := core.OpenSession(session.Path)
156 if err != nil {
157 t.Fatal(err)
158 }
159 t.Cleanup(func() { _ = reopened.Close() })
160 before := tui.View{Theme: tui.Dark, ChatTimestamps: true, Messages: messages}
161 after := tui.View{Theme: tui.Dark, ChatTimestamps: true, Messages: restored}
162 if !reflect.DeepEqual(before.Build(80), after.Build(80)) {
163 t.Fatal("resumed transcript changed timestamps")
164 }
165}
166
167func TestChatTimestampCustomIntervalStartup(t *testing.T) {
168 minutes := 7
169 i := NewInteractive(InteractiveConfig{ChatTimestamps: true, ChatTimestampIntervalMinutes: &minutes})
170 if !i.view.ChatTimestamps || tui.ChatTimestampInterval(i.view.ChatTimestampIntervalMinutes) != 7 {
171 t.Fatal("saved settings not applied on startup")
172 }
173 item := i.chatTimestampIntervalSetting()
174 if item.options[item.choice].value != "7" {
175 t.Fatal("custom interval not represented in picker")
176 }
177 before, _ := i.chatCacheKeyLocked(80)
178 i.view.ChatTimestampIntervalMinutes = nil
179 after, _ := i.chatCacheKeyLocked(80)
180 if before == after {
181 t.Fatal("interval missing from cache key")
182 }
183}