- 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>
238 lines
5.4 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|