Fix orphan detection to recognize hq-* sessions (#744)

The daemon creates hq-deacon and hq-mayor sessions (headquarters sessions)
that were incorrectly flagged as orphaned by gt doctor.

Changes:
- Update orphan session check to recognize hq-* prefix in addition to gt-*
- Update orphan process check to detect 'tmux: server' process name on Linux
- Add test coverage for hq-* session validation
- Update documentation comments to reflect hq-* patterns

This fixes the false positive warnings where hq-deacon session and its
child processes were incorrectly reported as orphaned.

Co-authored-by: Roland Tritsch <roland@ailtir.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Roland Tritsch
2026-01-21 06:27:41 +00:00
committed by GitHub
parent 560431d2f5
commit 9de8859be0
2 changed files with 41 additions and 7 deletions

View File

@@ -94,8 +94,8 @@ func (c *OrphanSessionCheck) Run(ctx *CheckContext) *CheckResult {
continue
}
// Only check gt-* sessions (Gas Town sessions)
if !strings.HasPrefix(sess, "gt-") {
// Only check gt-* and hq-* sessions (Gas Town sessions)
if !strings.HasPrefix(sess, "gt-") && !strings.HasPrefix(sess, "hq-") {
continue
}
@@ -200,8 +200,8 @@ func (c *OrphanSessionCheck) getValidRigs(townRoot string) []string {
// isValidSession checks if a session name matches expected Gas Town patterns.
// Valid patterns:
// - gt-{town}-mayor (dynamic based on town name)
// - gt-{town}-deacon (dynamic based on town name)
// - hq-mayor (headquarters mayor session)
// - hq-deacon (headquarters deacon session)
// - gt-<rig>-witness
// - gt-<rig>-refinery
// - gt-<rig>-<polecat> (where polecat is any name)
@@ -354,8 +354,9 @@ func (c *OrphanProcessCheck) getTmuxSessionPIDs() (map[int]bool, error) { //noli
// Find tmux server processes using ps instead of pgrep.
// pgrep -x tmux is unreliable on macOS - it often misses the actual server.
// We use ps with awk to find processes where comm is exactly "tmux".
out, err := exec.Command("sh", "-c", `ps ax -o pid,comm | awk '$2 == "tmux" || $2 ~ /\/tmux$/ { print $1 }'`).Output()
// We use ps with awk to find processes where comm is exactly "tmux" or starts with "tmux:".
// On Linux, tmux servers show as "tmux: server" in the comm field.
out, err := exec.Command("sh", "-c", `ps ax -o pid,comm | awk '$2 == "tmux" || $2 ~ /\/tmux$/ || $2 ~ /^tmux:/ { print $1 }'`).Output()
if err != nil {
// No tmux server running
return pids, nil

View File

@@ -358,6 +358,37 @@ func TestIsCrewSession_ComprehensivePatterns(t *testing.T) {
}
}
// TestOrphanSessionCheck_HQSessions tests that hq-* sessions are properly recognized as valid.
func TestOrphanSessionCheck_HQSessions(t *testing.T) {
townRoot := t.TempDir()
mayorDir := filepath.Join(townRoot, "mayor")
if err := os.MkdirAll(mayorDir, 0o755); err != nil {
t.Fatalf("create mayor dir: %v", err)
}
if err := os.WriteFile(filepath.Join(mayorDir, "rigs.json"), []byte("{}"), 0o644); err != nil {
t.Fatalf("create rigs.json: %v", err)
}
lister := &mockSessionLister{
sessions: []string{
"hq-mayor", // valid: headquarters mayor session
"hq-deacon", // valid: headquarters deacon session
},
}
check := NewOrphanSessionCheckWithSessionLister(lister)
result := check.Run(&CheckContext{TownRoot: townRoot})
if result.Status != StatusOK {
t.Fatalf("expected StatusOK for valid hq sessions, got %v: %s", result.Status, result.Message)
}
if result.Message != "All 2 Gas Town sessions are valid" {
t.Fatalf("unexpected message: %q", result.Message)
}
if len(check.orphanSessions) != 0 {
t.Fatalf("expected no orphan sessions, got %v", check.orphanSessions)
}
}
// TestOrphanSessionCheck_Run_Deterministic tests the full Run path with a mock session
// lister, ensuring deterministic behavior without depending on real tmux state.
func TestOrphanSessionCheck_Run_Deterministic(t *testing.T) {
@@ -383,9 +414,11 @@ func TestOrphanSessionCheck_Run_Deterministic(t *testing.T) {
"gt-gastown-witness", // valid: gastown rig exists
"gt-gastown-polecat1", // valid: gastown rig exists
"gt-beads-refinery", // valid: beads rig exists
"hq-mayor", // valid: hq-mayor is recognized
"hq-deacon", // valid: hq-deacon is recognized
"gt-unknown-witness", // orphan: unknown rig doesn't exist
"gt-missing-crew-joe", // orphan: missing rig doesn't exist
"random-session", // ignored: doesn't match gt-* pattern
"random-session", // ignored: doesn't match gt-*/hq-* pattern
},
}
check := NewOrphanSessionCheckWithSessionLister(lister)