package runtime import ( "os" "testing" "time" "github.com/steveyegge/gastown/internal/config" ) func TestSessionIDFromEnv_Default(t *testing.T) { // Clear all environment variables oldGSEnv := os.Getenv("GT_SESSION_ID_ENV") oldClaudeID := os.Getenv("CLAUDE_SESSION_ID") defer func() { if oldGSEnv != "" { os.Setenv("GT_SESSION_ID_ENV", oldGSEnv) } else { os.Unsetenv("GT_SESSION_ID_ENV") } if oldClaudeID != "" { os.Setenv("CLAUDE_SESSION_ID", oldClaudeID) } else { os.Unsetenv("CLAUDE_SESSION_ID") } }() os.Unsetenv("GT_SESSION_ID_ENV") os.Unsetenv("CLAUDE_SESSION_ID") result := SessionIDFromEnv() if result != "" { t.Errorf("SessionIDFromEnv() with no env vars should return empty, got %q", result) } } func TestSessionIDFromEnv_ClaudeSessionID(t *testing.T) { oldGSEnv := os.Getenv("GT_SESSION_ID_ENV") oldClaudeID := os.Getenv("CLAUDE_SESSION_ID") defer func() { if oldGSEnv != "" { os.Setenv("GT_SESSION_ID_ENV", oldGSEnv) } else { os.Unsetenv("GT_SESSION_ID_ENV") } if oldClaudeID != "" { os.Setenv("CLAUDE_SESSION_ID", oldClaudeID) } else { os.Unsetenv("CLAUDE_SESSION_ID") } }() os.Unsetenv("GT_SESSION_ID_ENV") os.Setenv("CLAUDE_SESSION_ID", "test-session-123") result := SessionIDFromEnv() if result != "test-session-123" { t.Errorf("SessionIDFromEnv() = %q, want %q", result, "test-session-123") } } func TestSessionIDFromEnv_CustomEnvVar(t *testing.T) { oldGSEnv := os.Getenv("GT_SESSION_ID_ENV") oldCustomID := os.Getenv("CUSTOM_SESSION_ID") oldClaudeID := os.Getenv("CLAUDE_SESSION_ID") defer func() { if oldGSEnv != "" { os.Setenv("GT_SESSION_ID_ENV", oldGSEnv) } else { os.Unsetenv("GT_SESSION_ID_ENV") } if oldCustomID != "" { os.Setenv("CUSTOM_SESSION_ID", oldCustomID) } else { os.Unsetenv("CUSTOM_SESSION_ID") } if oldClaudeID != "" { os.Setenv("CLAUDE_SESSION_ID", oldClaudeID) } else { os.Unsetenv("CLAUDE_SESSION_ID") } }() os.Setenv("GT_SESSION_ID_ENV", "CUSTOM_SESSION_ID") os.Setenv("CUSTOM_SESSION_ID", "custom-session-456") os.Setenv("CLAUDE_SESSION_ID", "claude-session-789") result := SessionIDFromEnv() if result != "custom-session-456" { t.Errorf("SessionIDFromEnv() with custom env = %q, want %q", result, "custom-session-456") } } func TestSleepForReadyDelay_NilConfig(t *testing.T) { // Should not panic with nil config SleepForReadyDelay(nil) } func TestSleepForReadyDelay_ZeroDelay(t *testing.T) { rc := &config.RuntimeConfig{ Tmux: &config.RuntimeTmuxConfig{ ReadyDelayMs: 0, }, } start := time.Now() SleepForReadyDelay(rc) elapsed := time.Since(start) // Should return immediately if elapsed > 100*time.Millisecond { t.Errorf("SleepForReadyDelay() with zero delay took too long: %v", elapsed) } } func TestSleepForReadyDelay_WithDelay(t *testing.T) { rc := &config.RuntimeConfig{ Tmux: &config.RuntimeTmuxConfig{ ReadyDelayMs: 10, // 10ms delay }, } start := time.Now() SleepForReadyDelay(rc) elapsed := time.Since(start) // Should sleep for at least 10ms if elapsed < 10*time.Millisecond { t.Errorf("SleepForReadyDelay() should sleep for at least 10ms, took %v", elapsed) } // But not too long if elapsed > 50*time.Millisecond { t.Errorf("SleepForReadyDelay() slept too long: %v", elapsed) } } func TestSleepForReadyDelay_NilTmuxConfig(t *testing.T) { rc := &config.RuntimeConfig{ Tmux: nil, } start := time.Now() SleepForReadyDelay(rc) elapsed := time.Since(start) // Should return immediately if elapsed > 100*time.Millisecond { t.Errorf("SleepForReadyDelay() with nil Tmux config took too long: %v", elapsed) } } func TestStartupFallbackCommands_NoHooks(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: &config.RuntimeHooksConfig{ Provider: "none", }, } commands := StartupFallbackCommands("polecat", rc) if commands == nil { t.Error("StartupFallbackCommands() with no hooks should return commands") } if len(commands) == 0 { t.Error("StartupFallbackCommands() should return at least one command") } } func TestStartupFallbackCommands_WithHooks(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: &config.RuntimeHooksConfig{ Provider: "claude", }, } commands := StartupFallbackCommands("polecat", rc) if commands != nil { t.Error("StartupFallbackCommands() with hooks provider should return nil") } } func TestStartupFallbackCommands_NilConfig(t *testing.T) { // Nil config defaults to claude provider, which has hooks // So it returns nil (no fallback commands needed) commands := StartupFallbackCommands("polecat", nil) if commands != nil { t.Error("StartupFallbackCommands() with nil config should return nil (defaults to claude with hooks)") } } func TestStartupFallbackCommands_AutonomousRole(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: &config.RuntimeHooksConfig{ Provider: "none", }, } autonomousRoles := []string{"polecat", "witness", "refinery", "deacon"} for _, role := range autonomousRoles { t.Run(role, func(t *testing.T) { commands := StartupFallbackCommands(role, rc) if commands == nil || len(commands) == 0 { t.Error("StartupFallbackCommands() should return commands for autonomous role") } // Should contain mail check found := false for _, cmd := range commands { if contains(cmd, "mail check --inject") { found = true break } } if !found { t.Errorf("Commands for %s should contain mail check --inject", role) } }) } } func TestStartupFallbackCommands_NonAutonomousRole(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: &config.RuntimeHooksConfig{ Provider: "none", }, } nonAutonomousRoles := []string{"mayor", "crew", "keeper"} for _, role := range nonAutonomousRoles { t.Run(role, func(t *testing.T) { commands := StartupFallbackCommands(role, rc) if commands == nil || len(commands) == 0 { t.Error("StartupFallbackCommands() should return commands for non-autonomous role") } // Should NOT contain mail check for _, cmd := range commands { if contains(cmd, "mail check --inject") { t.Errorf("Commands for %s should NOT contain mail check --inject", role) } } }) } } func TestStartupFallbackCommands_RoleCasing(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: &config.RuntimeHooksConfig{ Provider: "none", }, } // Role should be lowercased internally commands := StartupFallbackCommands("POLECAT", rc) if commands == nil { t.Error("StartupFallbackCommands() should handle uppercase role") } } func TestEnsureSettingsForRole_NilConfig(t *testing.T) { // Should not panic with nil config err := EnsureSettingsForRole("/tmp/test", "polecat", nil) if err != nil { t.Errorf("EnsureSettingsForRole() with nil config should not error, got %v", err) } } func TestEnsureSettingsForRole_NilHooks(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: nil, } err := EnsureSettingsForRole("/tmp/test", "polecat", rc) if err != nil { t.Errorf("EnsureSettingsForRole() with nil hooks should not error, got %v", err) } } func TestEnsureSettingsForRole_UnknownProvider(t *testing.T) { rc := &config.RuntimeConfig{ Hooks: &config.RuntimeHooksConfig{ Provider: "unknown", }, } err := EnsureSettingsForRole("/tmp/test", "polecat", rc) if err != nil { t.Errorf("EnsureSettingsForRole() with unknown provider should not error, got %v", err) } } // Helper function func contains(s, substr string) bool { return len(s) >= len(substr) && findSubstring(s, substr) } func findSubstring(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { match := true for j := 0; j < len(substr); j++ { if s[i+j] != substr[j] { match = false break } } if match { return true } } return false }