1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
|
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// Both fixtures are the shapes the real files use: Claude Code writes one
// record per message with content blocks, codex wraps everything in a payload
// and keeps the model's history in response_items.
const claudeJSONL = `{"type":"user","timestamp":"2026-07-20T09:57:00.000Z","sessionId":"s1","cwd":"/repo","message":{"role":"user","content":"read lib.rs and tell me about the cache"}}
{"type":"assistant","timestamp":"2026-07-20T09:57:30.000Z","message":{"role":"assistant","model":"claude-opus-5","content":[{"type":"thinking","thinking":"start with the struct","signature":"CAIS"},{"type":"text","text":"Reading it now."},{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"vr read src/lib.rs:1-30","description":"read the file"}}]}}
{"type":"user","timestamp":"2026-07-20T09:59:10.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_1","content":"1\tuse std::collections::HashMap;\n2\t\n3\tstruct Cache;\n"}]}}
{"type":"mode","timestamp":"2026-07-20T09:59:11.000Z","mode":"default"}
{"type":"assistant","timestamp":"2026-07-20T09:59:40.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_2","name":"Read","input":{"file_path":"/repo/src/main.rs"}}]}}
{"type":"user","timestamp":"2026-07-20T09:59:45.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_2","is_error":true,"content":[{"type":"text","text":"no such file"}]}]}}
{"type":"assistant","timestamp":"2026-07-20T10:02:30.000Z","message":{"role":"assistant","content":[{"type":"text","text":"It is a two-level map."}]}}
`
const codexJSONL = `{"timestamp":"2026-07-20T12:00:00.000Z","type":"session_meta","payload":{"id":"019f0000-0000-7000-8000-00000000abcd","cwd":"/repo"}}
{"timestamp":"2026-07-20T12:00:01.000Z","type":"response_item","payload":{"type":"message","role":"developer","content":[{"type":"input_text","text":"You are Codex, an agent."}]}}
{"timestamp":"2026-07-20T12:00:02.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"<environment_context>\n <cwd>/repo</cwd>\n</environment_context>"}]}}
{"timestamp":"2026-07-20T12:00:03.000Z","type":"response_item","payload":{"type":"message","role":"user","content":[{"type":"input_text","text":"review the cache"}]}}
{"timestamp":"2026-07-20T12:00:04.000Z","type":"response_item","payload":{"type":"reasoning","summary":[{"type":"summary_text","text":"start with lib.rs"}],"encrypted_content":"gAAAA"}}
{"timestamp":"2026-07-20T12:00:05.000Z","type":"response_item","payload":{"type":"function_call","name":"exec_command","call_id":"call_1","arguments":"{\"cmd\":\"vr read src/lib.rs\",\"workdir\":\"/repo\"}"}}
{"timestamp":"2026-07-20T12:00:06.000Z","type":"response_item","payload":{"type":"function_call_output","call_id":"call_1","output":"Chunk ID: abc123\nOutput:\n1\tstruct Cache;\n"}}
{"timestamp":"2026-07-20T12:00:07.000Z","type":"event_msg","payload":{"type":"agent_message","message":"the cache is two-level"}}
{"timestamp":"2026-07-20T12:00:08.000Z","type":"response_item","payload":{"type":"custom_tool_call","name":"apply_patch","call_id":"call_2","input":"*** Begin Patch\n*** Update File: src/lib.rs\n@@\n-old\n+new\n"}}
{"timestamp":"2026-07-20T12:00:09.000Z","type":"response_item","payload":{"type":"custom_tool_call_output","call_id":"call_2","output":"Exit code: 0\n"}}
{"timestamp":"2026-07-20T12:00:10.000Z","type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"The cache is two-level."}]}}
`
func writeFile(t *testing.T, path, body string) string {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
return path
}
func turnLine(tn Turn) string {
s := tn.Role
if tn.Tool != "" {
s += " " + tn.Tool
}
if tn.Text != "" {
s += " " + tn.Text
}
if tn.Detail != "" {
s += " " + tn.Detail
}
return s
}
func TestParseClaudeTranscript(t *testing.T) {
path := writeFile(t, filepath.Join(t.TempDir(), "s.jsonl"), claudeJSONL)
tr := readTranscript(path, int64(len(claudeJSONL)))
if tr.Harness != "claude-code" || tr.Err != "" {
t.Fatalf("harness = %q err = %q", tr.Harness, tr.Err)
}
want := []string{
"user read lib.rs and tell me about the cache",
"thinking start with the struct",
"assistant Reading it now.",
"tool Bash vr read src/lib.rs:1-30",
"result Bash 1\tuse std::collections::HashMap; … +2 lines",
"tool Read /repo/src/main.rs",
"result Read no such file",
"assistant It is a two-level map.",
}
var got []string
for _, tn := range tr.Turns {
got = append(got, turnLine(tn))
}
if strings.Join(got, "\n") != strings.Join(want, "\n") {
t.Errorf("turns:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
}
if tr.Turns[6].Kind != "error" {
t.Error("a failed tool result is not marked")
}
if at := tr.Turns[0].When; at.Format(time.RFC3339) != "2026-07-20T09:57:00Z" {
t.Errorf("first turn at %v", at)
}
}
func TestParseCodexTranscript(t *testing.T) {
path := writeFile(t, filepath.Join(t.TempDir(), "r.jsonl"), codexJSONL)
tr := readTranscript(path, int64(len(codexJSONL)))
if tr.Harness != "codex" || tr.Err != "" {
t.Fatalf("harness = %q err = %q", tr.Harness, tr.Err)
}
want := []string{
"user review the cache",
"thinking start with lib.rs",
"tool exec_command vr read src/lib.rs",
"result exec_command Chunk ID: abc123 … +2 lines",
"tool apply_patch *** Begin Patch … +4 lines",
"result apply_patch Exit code: 0",
"assistant The cache is two-level.",
}
var got []string
for _, tn := range tr.Turns {
got = append(got, turnLine(tn))
}
if strings.Join(got, "\n") != strings.Join(want, "\n") {
t.Errorf("turns:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
}
}
// Tool output is what makes these files megabytes; a page gets one line of it.
func TestToolOutputIsSummarized(t *testing.T) {
huge := strings.Repeat("x", 400) + "\n" + strings.Repeat("line\n", 50000)
line := fmt.Sprintf(`{"type":"user","timestamp":"2026-07-20T10:00:00.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t","content":%q}]}}`, huge)
path := writeFile(t, filepath.Join(t.TempDir(), "big.jsonl"), line+"\n")
tr := readTranscript(path, int64(len(line)+1))
if len(tr.Turns) != 1 {
t.Fatalf("turns = %d", len(tr.Turns))
}
if n := len(tr.Turns[0].Detail); n > 4*briefLine {
t.Errorf("summary of a 250 KB result is %d bytes", n)
}
if !strings.Contains(tr.Turns[0].Detail, "+50000 lines") {
t.Errorf("summary does not say what was left out: %q", tr.Turns[0].Detail)
}
}
func TestTranscriptTooLargeIsRefused(t *testing.T) {
path := writeFile(t, filepath.Join(t.TempDir(), "x.jsonl"), "{}\n")
if tr := readTranscript(path, maxTranscript+1); !strings.Contains(tr.Err, "too large") {
t.Errorf("err = %q", tr.Err)
}
}
// ── locating ─────────────────────────────────────────────────────────────
func TestSafeSession(t *testing.T) {
for _, ok := range []string{"s1", "web:goblin", "24983541-2e59-4d47-94bb-837acf8147c3",
"019fa0f1-49de-7e33-af62-6db956589080"} {
if err := safeSession(ok); err != nil {
t.Errorf("safeSession(%q) = %v", ok, err)
}
}
for _, bad := range []string{"", "..", "a/b", `a\b`, "../../etc/passwd", "a..b", "a b",
"a*b", "a\x00b", strings.Repeat("x", 129)} {
if err := safeSession(bad); err == nil {
t.Errorf("safeSession(%q) was accepted", bad)
}
}
}
func TestFindTranscript(t *testing.T) {
claudeRoot, codexRoot := t.TempDir(), t.TempDir()
const (
cid = "24983541-2e59-4d47-94bb-837acf8147c3"
xid = "019fa0f1-49de-7e33-af62-6db956589080"
)
want := writeFile(t, filepath.Join(claudeRoot, "-home-u-0x4200-cafe-proj", cid+".jsonl"), claudeJSONL)
wantX := writeFile(t, filepath.Join(codexRoot, "2026", "07", "26",
"rollout-2026-07-26T21-19-55-"+xid+".jsonl"), codexJSONL)
// a file outside every root must stay invisible
outside := writeFile(t, filepath.Join(t.TempDir(), cid+".jsonl"), claudeJSONL)
tx := newTranscripts([]string{claudeRoot, codexRoot})
if got := tx.find(cid, "/home/u/0x4200.cafe/proj"); got != want {
t.Errorf("with the logged cwd: %q, want %q", got, want)
}
if got := tx.find(cid, "/somewhere/else"); got != want {
t.Errorf("with a stale cwd: %q, want %q", got, want)
}
if got := tx.find(xid, ""); got != wantX {
t.Errorf("codex thread: %q, want %q", got, wantX)
}
if got := tx.find("nosuchsession", ""); got != "" {
t.Errorf("unknown session found %q", got)
}
if got := newTranscripts(nil).find(cid, filepath.Dir(outside)); got != "" {
t.Errorf("no roots configured, yet found %q", got)
}
if under(claudeRoot, outside) {
t.Error("a path outside a root passed the check")
}
}
// A 5 MB transcript is parsed once and then only re-read when it changes.
func TestTranscriptCacheKeyedByFile(t *testing.T) {
root := t.TempDir()
path := writeFile(t, filepath.Join(root, "-repo", "s1.jsonl"), claudeJSONL)
tx := newTranscripts([]string{root})
first := tx.load("s1", "/repo")
if first == nil || len(first.Turns) == 0 {
t.Fatal("nothing parsed")
}
if again := tx.load("s1", "/repo"); again != first {
t.Error("an unchanged transcript was parsed twice")
}
writeFile(t, path, claudeJSONL+`{"type":"assistant","timestamp":"2026-07-20T11:00:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"one more thing"}]}}`+"\n")
future := time.Now().Add(time.Second)
os.Chtimes(path, future, future)
after := tx.load("s1", "/repo")
if after == first || len(after.Turns) != len(first.Turns)+1 {
t.Errorf("a changed transcript was not re-parsed: %d -> %d turns", len(first.Turns), len(after.Turns))
}
if tx.load("nope", "/repo") != nil {
t.Error("a missing transcript should be nil, not empty")
}
}
// ── windowing ────────────────────────────────────────────────────────────
func TestConvWindowIsBounded(t *testing.T) {
turns := make([]Turn, 500)
for i := range turns {
turns[i] = Turn{Role: "assistant", Text: fmt.Sprint(i)}
}
from, got := convWindowAt(turns, -1, false)
if len(got) != convWindow || from != 500-convWindow || got[len(got)-1].Text != "499" {
t.Errorf("unanchored window = %d turns from %d", len(got), from)
}
from, got = convWindowAt(turns, 200, false)
if len(got) != convWindow || from > 200 || from+len(got) <= 200 {
t.Errorf("window %d..%d does not hold turn 200", from, from+len(got))
}
from, got = convWindowAt(turns, 498, false)
if len(got) != convWindow || from+len(got) != 500 {
t.Errorf("window near the end = %d..%d", from, from+len(got))
}
if _, got = convWindowAt(turns, 200, true); len(got) != 500 {
t.Errorf("show all gave %d of 500 turns", len(got))
}
if _, got = convWindowAt(turns[:10], -1, false); len(got) != 10 {
t.Errorf("short conversation was cut to %d", len(got))
}
}
// ── the session page ─────────────────────────────────────────────────────
// sessionServer is testServer with a Claude Code transcript for session s1,
// filed under the repo path the log entries carry.
func sessionServer(t *testing.T) (*server, string) {
t.Helper()
sv, _, _ := testServer(t)
root := t.TempDir()
writeFile(t, filepath.Join(root, mangleDir(sv.repo), "s1.jsonl"), claudeJSONL)
sv.tx = newTranscripts([]string{root})
return sv, root
}
func TestSessionPageInterleavesLogAndTranscript(t *testing.T) {
sv, _ := sessionServer(t)
body := get(t, sv.handler(), "/session/s1?note=1", false).Body.String()
for _, want := range []string{
"read lib.rs and tell me about the cache", // the conversation
"It is a two-level map.",
"vr read src/lib.rs:1-30", // a tool call, summarized
"read hello.txt:1-2", // the vr log, woven in
"second line matters", // the note itself
`data-anchor="note-1"`, // and it is what the page lands on
`class="turn t-note here"`, // marked as the anchored moment
} {
if !strings.Contains(body, want) {
t.Errorf("session page is missing %q", want)
}
}
// the whole file is never in the page
if strings.Contains(body, "use std::collections::HashMap;\n2") {
t.Error("a tool result was dumped in full")
}
// ordering: the note was logged after the first message, before the last
iFirst := strings.Index(body, "read lib.rs and tell me")
iNote := strings.Index(body, "second line matters")
iLast := strings.Index(body, "It is a two-level map.")
if !(iFirst < iNote && iNote < iLast) {
t.Errorf("turns are out of order: %d %d %d", iFirst, iNote, iLast)
}
}
func TestSessionPageWithoutATranscript(t *testing.T) {
sv, _, _ := testServer(t)
sv.tx = newTranscripts([]string{t.TempDir()})
body := get(t, sv.handler(), "/session/s1", false).Body.String()
if !strings.Contains(body, "no transcript found") {
t.Error("the page does not say the transcript is missing")
}
if !strings.Contains(body, sv.tx.roots[0]) {
t.Error("the page does not say where it looked")
}
if !strings.Contains(body, "read hello.txt:1-2") || !strings.Contains(body, "second line matters") {
t.Error("the vr timeline is missing from the fallback")
}
}
func TestSessionPageRejectsUnsafeIDs(t *testing.T) {
sv, _ := sessionServer(t)
h := sv.handler()
if w := get(t, h, "/session/a%20b", false); w.Code != 400 {
t.Errorf("GET /session/a b = %d, want 400", w.Code)
}
if w := get(t, h, "/session/unknown-session", false); w.Code != 200 ||
!strings.Contains(w.Body.String(), "no transcript found") {
t.Errorf("unknown session = %d", w.Code)
}
}
// Live pages link a note to its conversation; the static export has no server
// to serve one from.
func TestConversationLinksAreLiveOnly(t *testing.T) {
sv, _ := sessionServer(t)
h := sv.handler()
if b := get(t, h, "/notes", false).Body.String(); !strings.Contains(b, `href="/session/s1?note=1#note-1"`) {
t.Error("no conversation link on a note card")
}
if b := get(t, h, "/", false).Body.String(); !strings.Contains(b, `href="/session/s1"`) {
t.Error("the agents table does not link the session")
}
if b := get(t, h, "/activity", true).Body.String(); !strings.Contains(b, `href="/session/s1"`) {
t.Error("the activity table does not link the session")
}
if b := get(t, h, "/session/s1?note=1", false).Body.String(); strings.Contains(b, ">conversation<") {
t.Error("the session page links back to itself")
}
static := &page{Root: "../"}
if got := static.ConvHref(&Note{ID: 1, Session: "s1"}); got != "" {
t.Errorf("static conversation href = %q, want none", got)
}
}
|