fix(tmux): resolve claude path for alias installations (#703) (#748)

Fix "Unable to attach mayor" timeout caused by claude being installed
as a shell alias rather than in PATH. Non-interactive shells spawned
by tmux cannot resolve aliases, causing the session to exit immediately.

Changes:
- Add resolveClaudePath() to find claude at ~/.claude/local/claude
- Apply path resolution in RuntimeConfigFromPreset() for claude preset
- Make hasClaudeChild() recursive (now hasClaudeDescendant()) to search
  entire process subtree as defensive improvement
- Update fillRuntimeDefaults() to use DefaultRuntimeConfig() for
  consistent path resolution

Fixes https://github.com/steveyegge/gastown/issues/703

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Kartik Shrivastava
2026-01-21 12:02:07 +05:30
committed by John Ogle
parent 858782d657
commit 123c7407fc
4 changed files with 167 additions and 70 deletions

View File

@@ -8,6 +8,12 @@ import (
"testing"
)
// isClaudeCmd checks if a command is claude (either "claude" or a path ending in "/claude").
// Note: Named differently from loader_test.go's isClaudeCommand to avoid redeclaration.
func isClaudeCmd(cmd string) bool {
return cmd == "claude" || strings.HasSuffix(cmd, "/claude")
}
func TestBuiltinPresets(t *testing.T) {
t.Parallel()
// Ensure all built-in presets are accessible
@@ -71,7 +77,7 @@ func TestRuntimeConfigFromPreset(t *testing.T) {
preset AgentPreset
wantCommand string
}{
{AgentClaude, "claude"},
{AgentClaude, "claude"}, // Note: claude may resolve to full path
{AgentGemini, "gemini"},
{AgentCodex, "codex"},
{AgentCursor, "cursor-agent"},
@@ -82,7 +88,13 @@ func TestRuntimeConfigFromPreset(t *testing.T) {
for _, tt := range tests {
t.Run(string(tt.preset), func(t *testing.T) {
rc := RuntimeConfigFromPreset(tt.preset)
if rc.Command != tt.wantCommand {
// For claude, command may be full path due to resolveClaudePath
if tt.preset == AgentClaude {
if !isClaudeCmd(rc.Command) {
t.Errorf("RuntimeConfigFromPreset(%s).Command = %v, want claude or path ending in /claude",
tt.preset, rc.Command)
}
} else if rc.Command != tt.wantCommand {
t.Errorf("RuntimeConfigFromPreset(%s).Command = %v, want %v",
tt.preset, rc.Command, tt.wantCommand)
}
@@ -226,8 +238,8 @@ func TestMergeWithPreset(t *testing.T) {
var nilConfig *RuntimeConfig
merged = nilConfig.MergeWithPreset(AgentClaude)
if merged.Command != "claude" {
t.Errorf("nil config merge should get preset command, got %s", merged.Command)
if !isClaudeCmd(merged.Command) {
t.Errorf("nil config merge should get preset command (claude or path), got %s", merged.Command)
}
// Test empty config gets preset defaults
@@ -456,7 +468,12 @@ func TestAgentCommandGeneration(t *testing.T) {
t.Fatal("RuntimeConfigFromPreset returned nil")
}
if rc.Command != tt.wantCommand {
// For claude, command may be full path due to resolveClaudePath
if tt.preset == AgentClaude {
if !isClaudeCmd(rc.Command) {
t.Errorf("Command = %q, want claude or path ending in /claude", rc.Command)
}
} else if rc.Command != tt.wantCommand {
t.Errorf("Command = %q, want %q", rc.Command, tt.wantCommand)
}