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>
498 lines
13 KiB
Go
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))
|
|
}
|
|
}
|