Files
gastown/internal/townlog/logger_test.go
Steve Yegge 2117eb66f5 feat: Add tmux crash detection hooks (gt-i9s7o)
- Add SetPaneDiedHook to tmux package for crash detection
- Add gt log crash subcommand for hook callback
- Set pane-died hook when starting polecat sessions
- Distinguish exit types: 0=done, 130=kill (Ctrl+C), other=crash
- Rename townlog/townlog.go to townlog/logger.go

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 16:18:44 -08:00

238 lines
5.4 KiB
Go

package townlog
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestFormatLogLine(t *testing.T) {
ts := time.Date(2025, 12, 26, 15, 30, 45, 0, time.UTC)
tests := []struct {
name string
event Event
contains []string
}{
{
name: "spawn event",
event: Event{
Timestamp: ts,
Type: EventSpawn,
Agent: "gastown/crew/max",
Context: "gt-xyz",
},
contains: []string{"2025-12-26 15:30:45", "[spawn]", "gastown/crew/max", "spawned for gt-xyz"},
},
{
name: "nudge event",
event: Event{
Timestamp: ts,
Type: EventNudge,
Agent: "gastown/crew/max",
Context: "start work",
},
contains: []string{"[nudge]", "gastown/crew/max", "nudged with"},
},
{
name: "done event",
event: Event{
Timestamp: ts,
Type: EventDone,
Agent: "gastown/crew/max",
Context: "gt-abc",
},
contains: []string{"[done]", "completed gt-abc"},
},
{
name: "crash event",
event: Event{
Timestamp: ts,
Type: EventCrash,
Agent: "gastown/polecats/Toast",
Context: "signal 9",
},
contains: []string{"[crash]", "exited unexpectedly", "signal 9"},
},
{
name: "kill event",
event: Event{
Timestamp: ts,
Type: EventKill,
Agent: "gastown/polecats/Toast",
Context: "gt stop",
},
contains: []string{"[kill]", "killed", "gt stop"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
line := formatLogLine(tt.event)
for _, want := range tt.contains {
if !strings.Contains(line, want) {
t.Errorf("formatLogLine() = %q, want it to contain %q", line, want)
}
}
})
}
}
func TestParseLogLine(t *testing.T) {
tests := []struct {
name string
line string
wantErr bool
check func(Event) bool
}{
{
name: "valid spawn line",
line: "2025-12-26 15:30:45 [spawn] gastown/crew/max spawned for gt-xyz",
check: func(e Event) bool {
return e.Type == EventSpawn && e.Agent == "gastown/crew/max"
},
},
{
name: "valid nudge line",
line: "2025-12-26 15:31:02 [nudge] gastown/crew/max nudged with \"start\"",
check: func(e Event) bool {
return e.Type == EventNudge && e.Agent == "gastown/crew/max"
},
},
{
name: "too short",
line: "short",
wantErr: true,
},
{
name: "missing bracket",
line: "2025-12-26 15:30:45 spawn gastown/crew/max",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
event, err := parseLogLine(tt.line)
if tt.wantErr {
if err == nil {
t.Errorf("parseLogLine() expected error, got nil")
}
return
}
if err != nil {
t.Errorf("parseLogLine() unexpected error: %v", err)
return
}
if tt.check != nil && !tt.check(event) {
t.Errorf("parseLogLine() check failed for event: %+v", event)
}
})
}
}
func TestLoggerLogEvent(t *testing.T) {
// Create temp directory
tmpDir, err := os.MkdirTemp("", "townlog-test")
if err != nil {
t.Fatalf("creating temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
logger := NewLogger(tmpDir)
// Log an event
err = logger.Log(EventSpawn, "gastown/crew/max", "gt-xyz")
if err != nil {
t.Fatalf("Log() error: %v", err)
}
// Verify log file was created
logPath := filepath.Join(tmpDir, "logs", "town.log")
content, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("reading log file: %v", err)
}
if !strings.Contains(string(content), "[spawn]") {
t.Errorf("log file should contain [spawn], got: %s", content)
}
if !strings.Contains(string(content), "gastown/crew/max") {
t.Errorf("log file should contain agent name, got: %s", content)
}
}
func TestFilterEvents(t *testing.T) {
now := time.Now()
events := []Event{
{Timestamp: now.Add(-2 * time.Hour), Type: EventSpawn, Agent: "gastown/crew/max", Context: "gt-1"},
{Timestamp: now.Add(-1 * time.Hour), Type: EventNudge, Agent: "gastown/crew/max", Context: "hi"},
{Timestamp: now.Add(-30 * time.Minute), Type: EventDone, Agent: "gastown/polecats/Toast", Context: "gt-2"},
{Timestamp: now.Add(-10 * time.Minute), Type: EventSpawn, Agent: "wyvern/crew/joe", Context: "gt-3"},
}
tests := []struct {
name string
filter Filter
wantCount int
}{
{
name: "no filter",
filter: Filter{},
wantCount: 4,
},
{
name: "filter by type",
filter: Filter{Type: EventSpawn},
wantCount: 2,
},
{
name: "filter by agent prefix",
filter: Filter{Agent: "gastown/"},
wantCount: 3,
},
{
name: "filter by time",
filter: Filter{Since: now.Add(-45 * time.Minute)},
wantCount: 2,
},
{
name: "combined filters",
filter: Filter{Type: EventSpawn, Agent: "gastown/"},
wantCount: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FilterEvents(events, tt.filter)
if len(result) != tt.wantCount {
t.Errorf("FilterEvents() got %d events, want %d", len(result), tt.wantCount)
}
})
}
}
func TestTruncate(t *testing.T) {
tests := []struct {
input string
maxLen int
want string
}{
{"short", 10, "short"},
{"exactly10c", 10, "exactly10c"},
{"this is a longer string", 10, "this is..."},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := truncate(tt.input, tt.maxLen)
if got != tt.want {
t.Errorf("truncate(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want)
}
})
}
}