Files
vmux/session_test.go
Pierre Martin e7ced9c3a3 test(01-02): add failing tests then implement session + state
- TailReadJSONL: reverse-read JSONL without loading entire file
- FindSessionForProcess: match PID to most recent JSONL via EncodePath
- DetectState: heuristic based on last message stop_reason + tool name
- ExtractPreview: extract first 3 lines of last assistant text
- All 17 tests pass (session_test.go + state_test.go)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 13:29:19 +01:00

214 lines
6.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"time"
)
func writeJSONL(t *testing.T, path string, messages []JSONLMessage) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
enc := json.NewEncoder(f)
for _, msg := range messages {
if err := enc.Encode(msg); err != nil {
t.Fatal(err)
}
}
}
func makeMessages(n int) []JSONLMessage {
msgs := make([]JSONLMessage, n)
for i := range msgs {
msgs[i] = JSONLMessage{
Type: "assistant",
Timestamp: time.Now().UTC().Format(time.RFC3339),
SessionID: "test-session",
Cwd: "/tmp",
GitBranch: "main",
Message: &MessagePayload{
Role: "assistant",
Content: []ContentBlock{{Type: "text", Text: fmt.Sprintf("line %d", i+1)}},
StopReason: "end_turn",
},
}
}
return msgs
}
func TestTailReadJSONL(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.jsonl")
writeJSONL(t, path, makeMessages(50))
msgs, err := TailReadJSONL(path, 3)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(msgs) != 3 {
t.Fatalf("expected 3 messages, got %d", len(msgs))
}
// Should be the last 3 (chronological order: 48, 49, 50)
if msgs[0].Message.Content[0].Text != "line 48" {
t.Errorf("first message text = %q, want %q", msgs[0].Message.Content[0].Text, "line 48")
}
if msgs[2].Message.Content[0].Text != "line 50" {
t.Errorf("last message text = %q, want %q", msgs[2].Message.Content[0].Text, "line 50")
}
}
func TestTailReadJSONL_TruncatedLastLine(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test.jsonl")
// Write 5 complete lines then a truncated line (no trailing \n)
writeJSONL(t, path, makeMessages(5))
// Append truncated JSON (no newline)
f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
t.Fatal(err)
}
f.WriteString(`{"type":"assistant","timestamp":"2026-01-01T00:00:00Z"`)
f.Close()
msgs, err := TailReadJSONL(path, 3)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(msgs) != 3 {
t.Fatalf("expected 3 messages (truncated ignored), got %d", len(msgs))
}
// Last complete line should be line 5
if msgs[2].Message.Content[0].Text != "line 5" {
t.Errorf("last message text = %q, want %q", msgs[2].Message.Content[0].Text, "line 5")
}
}
func TestTailReadJSONL_EmptyFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "empty.jsonl")
os.WriteFile(path, []byte{}, 0o644)
msgs, err := TailReadJSONL(path, 3)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(msgs) != 0 {
t.Errorf("expected 0 messages for empty file, got %d", len(msgs))
}
}
func TestTailReadJSONL_SmallFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "small.jsonl")
writeJSONL(t, path, makeMessages(2))
msgs, err := TailReadJSONL(path, 10)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(msgs) != 2 {
t.Fatalf("expected 2 messages, got %d", len(msgs))
}
}
func TestFindSessionForProcess(t *testing.T) {
dir := t.TempDir()
cwd := "/home/user/my-project"
encoded := EncodePath(cwd)
projectDir := filepath.Join(dir, encoded)
os.MkdirAll(projectDir, 0o755)
// Create two JSONL files with different mtimes
older := filepath.Join(projectDir, "old-session.jsonl")
newer := filepath.Join(projectDir, "new-session.jsonl")
olderMsgs := []JSONLMessage{{
Type: "assistant", Timestamp: "2026-03-22T10:00:00Z",
SessionID: "old-id", Cwd: cwd, GitBranch: "old-branch",
Message: &MessagePayload{Role: "assistant", Content: []ContentBlock{{Type: "text", Text: "old"}}, StopReason: "end_turn"},
}}
newerMsgs := []JSONLMessage{{
Type: "assistant", Timestamp: "2026-03-23T12:00:00Z",
SessionID: "new-id", Cwd: cwd, GitBranch: "feat-x",
Message: &MessagePayload{Role: "assistant", Content: []ContentBlock{{Type: "text", Text: "new"}}, StopReason: "end_turn"},
}}
writeJSONL(t, older, olderMsgs)
// Ensure newer has a later mtime
time.Sleep(10 * time.Millisecond)
writeJSONL(t, newer, newerMsgs)
proc := Process{PID: 42, Cwd: cwd}
jsonlPath, msgs, err := FindSessionForProcess(dir, proc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if jsonlPath != newer {
t.Errorf("expected newest JSONL %q, got %q", newer, jsonlPath)
}
if len(msgs) == 0 {
t.Fatal("expected at least 1 message")
}
if msgs[0].SessionID != "new-id" {
t.Errorf("sessionID = %q, want %q", msgs[0].SessionID, "new-id")
}
}
func TestFindSessionForProcess_NoMatch(t *testing.T) {
dir := t.TempDir()
proc := Process{PID: 42, Cwd: "/nonexistent/path"}
_, _, err := FindSessionForProcess(dir, proc)
if err == nil {
t.Fatal("expected error for no matching JSONL, got nil")
}
}
func TestFindSessionForProcess_ExcludesSubagents(t *testing.T) {
dir := t.TempDir()
cwd := "/home/user/project"
encoded := EncodePath(cwd)
projectDir := filepath.Join(dir, encoded)
os.MkdirAll(projectDir, 0o755)
// Create a main session JSONL
mainJSONL := filepath.Join(projectDir, "main-session.jsonl")
writeJSONL(t, mainJSONL, []JSONLMessage{{
Type: "assistant", Timestamp: "2026-03-23T12:00:00Z",
SessionID: "main-id", Cwd: cwd, GitBranch: "main",
Message: &MessagePayload{Role: "assistant", Content: []ContentBlock{{Type: "text", Text: "main"}}, StopReason: "end_turn"},
}})
// Create a subagent JSONL in a subdirectory (should be excluded)
subDir := filepath.Join(projectDir, "some-uuid", "subagents")
os.MkdirAll(subDir, 0o755)
subJSONL := filepath.Join(subDir, "agent-1.jsonl")
writeJSONL(t, subJSONL, []JSONLMessage{{
Type: "assistant", Timestamp: "2026-03-23T13:00:00Z",
SessionID: "sub-id", Cwd: cwd, GitBranch: "main",
Message: &MessagePayload{Role: "assistant", Content: []ContentBlock{{Type: "text", Text: "sub"}}, StopReason: "end_turn"},
}})
proc := Process{PID: 42, Cwd: cwd}
jsonlPath, msgs, err := FindSessionForProcess(dir, proc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if jsonlPath != mainJSONL {
t.Errorf("expected main JSONL %q, got %q", mainJSONL, jsonlPath)
}
if msgs[0].SessionID != "main-id" {
t.Errorf("should use main session, got sessionID=%q", msgs[0].SessionID)
}
}