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>
This commit is contained in:
237
internal/townlog/logger_test.go
Normal file
237
internal/townlog/logger_test.go
Normal file
@@ -0,0 +1,237 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user