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":"\n /repo\n"}]}} {"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) } }