go/pkg/session/parser_test.go
Snider 3587d0ce27 test: add coverage for lab, session, sigil, repos, plugin packages
Brings 5 packages from low/zero coverage to solid test suites:
- pkg/lab: 0% → 100% (Store pub/sub, Config env loading)
- pkg/session: 0% → 89.9% (transcript parser, HTML renderer, search, video)
- pkg/io/sigil: 43.8% → 98.5% (XOR/ShuffleMask obfuscators, ChaCha20-Poly1305)
- pkg/repos: 18.9% → 81.9% (registry, topo sort, directory scan, org detection)
- pkg/plugin: 54.8% → 67.1% (installer error paths, Remove, registry Load/Save)

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-24 13:29:15 +00:00

498 lines
13 KiB
Go

package session
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
)
// ── truncate ───────────────────────────────────────────────────────
func TestTruncate_Good_Short(t *testing.T) {
if got := truncate("hello", 10); got != "hello" {
t.Fatalf("expected hello, got %s", got)
}
}
func TestTruncate_Good_Exact(t *testing.T) {
if got := truncate("12345", 5); got != "12345" {
t.Fatalf("expected 12345, got %s", got)
}
}
func TestTruncate_Good_Long(t *testing.T) {
got := truncate("hello world", 5)
if got != "hello..." {
t.Fatalf("expected hello..., got %s", got)
}
}
func TestTruncate_Good_Empty(t *testing.T) {
if got := truncate("", 10); got != "" {
t.Fatalf("expected empty, got %s", got)
}
}
// ── shortID ────────────────────────────────────────────────────────
func TestShortID_Good_Long(t *testing.T) {
got := shortID("f3fb074c-8c72-4da6-a15a-85bae652ccaa")
if got != "f3fb074c" {
t.Fatalf("expected f3fb074c, got %s", got)
}
}
func TestShortID_Good_Short(t *testing.T) {
if got := shortID("abc"); got != "abc" {
t.Fatalf("expected abc, got %s", got)
}
}
func TestShortID_Good_ExactEight(t *testing.T) {
if got := shortID("12345678"); got != "12345678" {
t.Fatalf("expected 12345678, got %s", got)
}
}
// ── formatDuration ─────────────────────────────────────────────────
func TestFormatDuration_Good_Milliseconds(t *testing.T) {
got := formatDuration(500 * time.Millisecond)
if got != "500ms" {
t.Fatalf("expected 500ms, got %s", got)
}
}
func TestFormatDuration_Good_Seconds(t *testing.T) {
got := formatDuration(3500 * time.Millisecond)
if got != "3.5s" {
t.Fatalf("expected 3.5s, got %s", got)
}
}
func TestFormatDuration_Good_Minutes(t *testing.T) {
got := formatDuration(2*time.Minute + 30*time.Second)
if got != "2m30s" {
t.Fatalf("expected 2m30s, got %s", got)
}
}
func TestFormatDuration_Good_Hours(t *testing.T) {
got := formatDuration(1*time.Hour + 15*time.Minute)
if got != "1h15m" {
t.Fatalf("expected 1h15m, got %s", got)
}
}
// ── extractToolInput ───────────────────────────────────────────────
func TestExtractToolInput_Good_Bash(t *testing.T) {
raw := json.RawMessage(`{"command":"go test ./...","description":"run tests"}`)
got := extractToolInput("Bash", raw)
if got != "go test ./... # run tests" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_BashNoDesc(t *testing.T) {
raw := json.RawMessage(`{"command":"ls"}`)
got := extractToolInput("Bash", raw)
if got != "ls" {
t.Fatalf("expected ls, got %s", got)
}
}
func TestExtractToolInput_Good_Read(t *testing.T) {
raw := json.RawMessage(`{"file_path":"/tmp/test.go"}`)
got := extractToolInput("Read", raw)
if got != "/tmp/test.go" {
t.Fatalf("expected /tmp/test.go, got %s", got)
}
}
func TestExtractToolInput_Good_Edit(t *testing.T) {
raw := json.RawMessage(`{"file_path":"/tmp/test.go","old_string":"foo","new_string":"bar"}`)
got := extractToolInput("Edit", raw)
if got != "/tmp/test.go (edit)" {
t.Fatalf("expected /tmp/test.go (edit), got %s", got)
}
}
func TestExtractToolInput_Good_Write(t *testing.T) {
raw := json.RawMessage(`{"file_path":"/tmp/out.go","content":"package main"}`)
got := extractToolInput("Write", raw)
if got != "/tmp/out.go (12 bytes)" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_Grep(t *testing.T) {
raw := json.RawMessage(`{"pattern":"TODO","path":"/src"}`)
got := extractToolInput("Grep", raw)
if got != "/TODO/ in /src" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_GrepNoPath(t *testing.T) {
raw := json.RawMessage(`{"pattern":"TODO"}`)
got := extractToolInput("Grep", raw)
if got != "/TODO/ in ." {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_Glob(t *testing.T) {
raw := json.RawMessage(`{"pattern":"**/*.go"}`)
got := extractToolInput("Glob", raw)
if got != "**/*.go" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_Task(t *testing.T) {
raw := json.RawMessage(`{"prompt":"investigate the bug","description":"debug helper","subagent_type":"Explore"}`)
got := extractToolInput("Task", raw)
if got != "[Explore] debug helper" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_TaskNoDesc(t *testing.T) {
raw := json.RawMessage(`{"prompt":"investigate the bug","subagent_type":"Explore"}`)
got := extractToolInput("Task", raw)
if got != "[Explore] investigate the bug" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_UnknownTool(t *testing.T) {
raw := json.RawMessage(`{"alpha":"1","beta":"2"}`)
got := extractToolInput("CustomTool", raw)
if got != "alpha, beta" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractToolInput_Good_NilInput(t *testing.T) {
got := extractToolInput("Bash", nil)
if got != "" {
t.Fatalf("expected empty, got %s", got)
}
}
func TestExtractToolInput_Bad_InvalidJSON(t *testing.T) {
raw := json.RawMessage(`not json`)
got := extractToolInput("Bash", raw)
// Falls through to fallback, which also fails — returns empty.
if got != "" {
t.Fatalf("expected empty, got %s", got)
}
}
// ── extractResultContent ───────────────────────────────────────────
func TestExtractResultContent_Good_String(t *testing.T) {
got := extractResultContent("hello")
if got != "hello" {
t.Fatalf("expected hello, got %s", got)
}
}
func TestExtractResultContent_Good_Slice(t *testing.T) {
input := []any{
map[string]any{"text": "line1"},
map[string]any{"text": "line2"},
}
got := extractResultContent(input)
if got != "line1\nline2" {
t.Fatalf("unexpected: %s", got)
}
}
func TestExtractResultContent_Good_Map(t *testing.T) {
input := map[string]any{"text": "content"}
got := extractResultContent(input)
if got != "content" {
t.Fatalf("expected content, got %s", got)
}
}
func TestExtractResultContent_Good_MapNoText(t *testing.T) {
input := map[string]any{"data": 42}
got := extractResultContent(input)
if got == "" {
t.Fatal("expected non-empty fallback")
}
}
func TestExtractResultContent_Good_Other(t *testing.T) {
got := extractResultContent(42)
if got != "42" {
t.Fatalf("expected 42, got %s", got)
}
}
// ── ParseTranscript ────────────────────────────────────────────────
func writeJSONL(t *testing.T, path string, entries []any) {
t.Helper()
f, err := os.Create(path)
if err != nil {
t.Fatal(err)
}
defer f.Close()
enc := json.NewEncoder(f)
for _, e := range entries {
if err := enc.Encode(e); err != nil {
t.Fatal(err)
}
}
}
func TestParseTranscript_Good_BasicFlow(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "test-session.jsonl")
ts1 := time.Date(2026, 2, 24, 10, 0, 0, 0, time.UTC)
ts2 := time.Date(2026, 2, 24, 10, 0, 1, 0, time.UTC)
ts3 := time.Date(2026, 2, 24, 10, 0, 2, 0, time.UTC)
entries := []any{
map[string]any{
"type": "assistant", "timestamp": ts1.Format(time.RFC3339Nano),
"message": map[string]any{
"role": "assistant",
"content": []any{
map[string]any{
"type": "tool_use", "id": "tu_1", "name": "Bash",
"input": map[string]any{"command": "go test ./...", "description": "run tests"},
},
},
},
},
map[string]any{
"type": "user", "timestamp": ts2.Format(time.RFC3339Nano),
"message": map[string]any{
"role": "user",
"content": []any{
map[string]any{
"type": "tool_result", "tool_use_id": "tu_1",
"content": "ok forge.lthn.ai/core/go 1.2s",
},
},
},
},
map[string]any{
"type": "user", "timestamp": ts3.Format(time.RFC3339Nano),
"message": map[string]any{
"role": "user",
"content": []any{
map[string]any{
"type": "text", "text": "nice work",
},
},
},
},
}
writeJSONL(t, path, entries)
sess, err := ParseTranscript(path)
if err != nil {
t.Fatal(err)
}
if sess.ID != "test-session" {
t.Fatalf("expected test-session, got %s", sess.ID)
}
if len(sess.Events) != 2 {
t.Fatalf("expected 2 events, got %d", len(sess.Events))
}
// Tool use event.
tool := sess.Events[0]
if tool.Type != "tool_use" {
t.Fatalf("expected tool_use, got %s", tool.Type)
}
if tool.Tool != "Bash" {
t.Fatalf("expected Bash, got %s", tool.Tool)
}
if !tool.Success {
t.Fatal("expected success")
}
if tool.Duration != time.Second {
t.Fatalf("expected 1s duration, got %s", tool.Duration)
}
// User message.
user := sess.Events[1]
if user.Type != "user" {
t.Fatalf("expected user, got %s", user.Type)
}
if user.Input != "nice work" {
t.Fatalf("unexpected input: %s", user.Input)
}
}
func TestParseTranscript_Good_ToolError(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "err-session.jsonl")
ts1 := time.Date(2026, 2, 24, 10, 0, 0, 0, time.UTC)
ts2 := time.Date(2026, 2, 24, 10, 0, 1, 0, time.UTC)
isError := true
entries := []any{
map[string]any{
"type": "assistant", "timestamp": ts1.Format(time.RFC3339Nano),
"message": map[string]any{
"role": "assistant",
"content": []any{
map[string]any{
"type": "tool_use", "id": "tu_err", "name": "Bash",
"input": map[string]any{"command": "rm -rf /"},
},
},
},
},
map[string]any{
"type": "user", "timestamp": ts2.Format(time.RFC3339Nano),
"message": map[string]any{
"role": "user",
"content": []any{
map[string]any{
"type": "tool_result", "tool_use_id": "tu_err",
"content": "permission denied", "is_error": &isError,
},
},
},
},
}
writeJSONL(t, path, entries)
sess, err := ParseTranscript(path)
if err != nil {
t.Fatal(err)
}
if len(sess.Events) != 1 {
t.Fatalf("expected 1 event, got %d", len(sess.Events))
}
if sess.Events[0].Success {
t.Fatal("expected failure")
}
if sess.Events[0].ErrorMsg != "permission denied" {
t.Fatalf("unexpected error: %s", sess.Events[0].ErrorMsg)
}
}
func TestParseTranscript_Good_AssistantText(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "asst.jsonl")
ts := time.Date(2026, 2, 24, 10, 0, 0, 0, time.UTC)
entries := []any{
map[string]any{
"type": "assistant", "timestamp": ts.Format(time.RFC3339Nano),
"message": map[string]any{
"role": "assistant",
"content": []any{
map[string]any{"type": "text", "text": "Let me check that."},
},
},
},
}
writeJSONL(t, path, entries)
sess, err := ParseTranscript(path)
if err != nil {
t.Fatal(err)
}
if len(sess.Events) != 1 {
t.Fatalf("expected 1 event, got %d", len(sess.Events))
}
if sess.Events[0].Type != "assistant" {
t.Fatalf("expected assistant, got %s", sess.Events[0].Type)
}
}
func TestParseTranscript_Bad_MissingFile(t *testing.T) {
_, err := ParseTranscript("/nonexistent/path.jsonl")
if err == nil {
t.Fatal("expected error for missing file")
}
}
func TestParseTranscript_Good_EmptyFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "empty.jsonl")
os.WriteFile(path, []byte{}, 0644)
sess, err := ParseTranscript(path)
if err != nil {
t.Fatal(err)
}
if len(sess.Events) != 0 {
t.Fatalf("expected 0 events, got %d", len(sess.Events))
}
}
func TestParseTranscript_Good_MalformedLines(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.jsonl")
os.WriteFile(path, []byte("not json\n{also bad\n"), 0644)
sess, err := ParseTranscript(path)
if err != nil {
t.Fatal(err)
}
if len(sess.Events) != 0 {
t.Fatalf("expected 0 events from bad lines, got %d", len(sess.Events))
}
}
// ── ListSessions ───────────────────────────────────────────────────
func TestListSessions_Good(t *testing.T) {
dir := t.TempDir()
ts1 := time.Date(2026, 2, 24, 10, 0, 0, 0, time.UTC)
ts2 := time.Date(2026, 2, 24, 11, 0, 0, 0, time.UTC)
writeJSONL(t, filepath.Join(dir, "sess-a.jsonl"), []any{
map[string]any{"type": "assistant", "timestamp": ts1.Format(time.RFC3339Nano),
"message": map[string]any{"role": "assistant", "content": []any{}}},
})
writeJSONL(t, filepath.Join(dir, "sess-b.jsonl"), []any{
map[string]any{"type": "assistant", "timestamp": ts2.Format(time.RFC3339Nano),
"message": map[string]any{"role": "assistant", "content": []any{}}},
})
sessions, err := ListSessions(dir)
if err != nil {
t.Fatal(err)
}
if len(sessions) != 2 {
t.Fatalf("expected 2 sessions, got %d", len(sessions))
}
// Sorted newest first.
if sessions[0].ID != "sess-b" {
t.Fatalf("expected sess-b first, got %s", sessions[0].ID)
}
}
func TestListSessions_Good_EmptyDir(t *testing.T) {
dir := t.TempDir()
sessions, err := ListSessions(dir)
if err != nil {
t.Fatal(err)
}
if len(sessions) != 0 {
t.Fatalf("expected 0, got %d", len(sessions))
}
}