main 4509ec7

session_history_test.go RawHistoryFolder

1package core
2
3import (
4	"encoding/json"
5	"testing"
6	"time"
7
8	"github.com/bnema/zut/packages/provider"
9)
10
11func TestReadSessionHistoryRetainsPreCompactionSegments(t *testing.T) {
12	root := t.TempDir()
13	cwd := "/workspace/history"
14	session, err := NewSession(root, cwd, "test", "model", "test")
15	if err != nil {
16		t.Fatal(err)
17	}
18	oldPrompt := textMessage(provider.RoleUser, "old prompt")
19	oldAnswer := textMessage(provider.RoleAssistant, "old answer")
20	toolCall := provider.Message{
21		Role:    provider.RoleAssistant,
22		Content: []provider.Content{provider.ToolCallBlock{ID: "call-1", Name: "read"}},
23	}
24	toolResult := provider.Message{
25		Role: provider.RoleTool,
26		Content: []provider.Content{provider.ToolResultBlock{
27			CallID:  "call-1",
28			Content: []provider.Content{provider.TextBlock{Text: "old file"}},
29		}},
30	}
31	for _, message := range []provider.Message{oldPrompt, oldAnswer, toolCall, toolResult} {
32		if err := session.AppendMessage(message); err != nil {
33			t.Fatal(err)
34		}
35	}
36	if err := session.AppendCompaction([]provider.Message{
37		{
38			Role: provider.RoleUser,
39			Meta: map[string]string{"compaction": "true"},
40			Content: []provider.Content{
41				provider.TextBlock{Text: "summary"},
42			},
43		},
44	}); err != nil {
45		t.Fatal(err)
46	}
47	if err := session.AppendMessage(textMessage(provider.RoleUser, "new prompt")); err != nil {
48		t.Fatal(err)
49	}
50	path := session.Path
51	if err := session.Close(); err != nil {
52		t.Fatal(err)
53	}
54
55	history, err := ReadSessionHistory(path)
56	if err != nil {
57		t.Fatalf("ReadSessionHistory: %v", err)
58	}
59	if len(history.Segments) != 2 {
60		t.Fatalf("history segments = %d, want 2", len(history.Segments))
61	}
62	if history.Segments[0].Compacted {
63		t.Fatal("pre-compaction segment marked compacted")
64	}
65	if !history.Segments[1].Compacted {
66		t.Fatal("replacement segment not marked compacted")
67	}
68	assertMessageTexts(t, history.Segments[0].Messages, []string{"old prompt", "old answer", "", ""})
69	assertMessageTexts(t, history.Segments[1].Messages, []string{"summary", "new prompt"})
70
71	branchPath, err := BranchSessionHiddenFromHistory(path, root, cwd, "test", history.Segments[0], 1)
72	if err != nil {
73		t.Fatalf("BranchSessionHiddenFromHistory: %v", err)
74	}
75	branch, messages, err := OpenSession(branchPath)
76	if err != nil {
77		t.Fatalf("OpenSession branch: %v", err)
78	}
79	if err := branch.Close(); err != nil {
80		t.Fatal(err)
81	}
82	assertMessageTexts(t, messages, []string{"old prompt"})
83	if branch.Meta.Parent != history.Meta.ID {
84		t.Fatalf("branch parent = %q, want %q", branch.Meta.Parent, history.Meta.ID)
85	}
86}
87
88func TestReadSessionHistoryIgnoresExtensionStateRows(t *testing.T) {
89	root := t.TempDir()
90	session, err := NewSession(root, "/workspace/history-extension-state", "test", "model", "test")
91	if err != nil {
92		t.Fatal(err)
93	}
94	if err := session.AppendMessage(textMessage(provider.RoleUser, "prompt")); err != nil {
95		t.Fatal(err)
96	}
97	if err := session.AppendExtensionState("tasked-phases", json.RawMessage(`{"version":1}`)); err != nil {
98		t.Fatal(err)
99	}
100	if err := session.AppendMessage(textMessage(provider.RoleAssistant, "answer")); err != nil {
101		t.Fatal(err)
102	}
103	path := session.Path
104	if err := session.Close(); err != nil {
105		t.Fatal(err)
106	}
107
108	history, err := ReadSessionHistory(path)
109	if err != nil {
110		t.Fatalf("ReadSessionHistory: %v", err)
111	}
112	if len(history.Segments) != 1 {
113		t.Fatalf("history segments = %d, want 1", len(history.Segments))
114	}
115	assertMessageTexts(t, history.Segments[0].Messages, []string{"prompt", "answer"})
116}
117
118func TestReadSessionHistoryRepairsEachSegmentIndependently(t *testing.T) {
119	root := t.TempDir()
120	session, err := NewSession(root, "/workspace/history-repair", "test", "model", "test")
121	if err != nil {
122		t.Fatal(err)
123	}
124	call := provider.Message{
125		Role:    provider.RoleAssistant,
126		Content: []provider.Content{provider.ToolCallBlock{ID: "call-1", Name: "bash"}},
127		Time:    time.Unix(1, 0),
128	}
129	if err := session.AppendMessage(call); err != nil {
130		t.Fatal(err)
131	}
132	if err := session.AppendCompaction([]provider.Message{textMessage(provider.RoleUser, "summary")}); err != nil {
133		t.Fatal(err)
134	}
135	path := session.Path
136	if err := session.Close(); err != nil {
137		t.Fatal(err)
138	}
139
140	history, err := ReadSessionHistory(path)
141	if err != nil {
142		t.Fatal(err)
143	}
144	if len(history.Segments) != 2 {
145		t.Fatalf("history segments = %d, want 2", len(history.Segments))
146	}
147	if len(history.Segments[0].Messages) != 2 {
148		t.Fatalf("repaired old segment messages = %d, want assistant plus stub result", len(history.Segments[0].Messages))
149	}
150	if history.Segments[0].Messages[1].Role != provider.RoleTool {
151		t.Fatalf("repaired old segment tail role = %q, want tool", history.Segments[0].Messages[1].Role)
152	}
153}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close