feat(doctor): add env-vars check, remove redundant gtroot check
Adds a new `gt doctor` check that verifies tmux session environment variables match expected values from `config.RoleEnvVars()`. - Checks all Gas Town sessions (gt-*, hq-*) - Compares actual tmux env vars against expected for each role - Reports mismatches with guidance to restart sessions - Treats no sessions as success (valid when Gas Town is down) - Skips deacon (doesn't use standard env vars) Also: - Adds `tmux.GetAllEnvironment()` to retrieve all session env vars - Removes redundant gtroot_check (env-vars check covers GT_ROOT) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
1d88a73eaa
commit
52b9a95f98
@@ -133,7 +133,6 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
d.Register(doctor.NewRoutesCheck())
|
||||
d.Register(doctor.NewOrphanSessionCheck())
|
||||
d.Register(doctor.NewOrphanProcessCheck())
|
||||
d.Register(doctor.NewGTRootCheck())
|
||||
d.Register(doctor.NewWispGCCheck())
|
||||
d.Register(doctor.NewBranchCheck())
|
||||
d.Register(doctor.NewBeadsSyncOrphanCheck())
|
||||
@@ -142,6 +141,7 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
||||
d.Register(doctor.NewLinkedPaneCheck())
|
||||
d.Register(doctor.NewThemeCheck())
|
||||
d.Register(doctor.NewCrashReportCheck())
|
||||
d.Register(doctor.NewEnvVarsCheck())
|
||||
|
||||
// Patrol system checks
|
||||
d.Register(doctor.NewPatrolMoleculesExistCheck())
|
||||
|
||||
147
internal/doctor/env_check.go
Normal file
147
internal/doctor/env_check.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
"github.com/steveyegge/gastown/internal/session"
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
// SessionEnvReader abstracts tmux session environment access for testing.
|
||||
type SessionEnvReader interface {
|
||||
ListSessions() ([]string, error)
|
||||
GetAllEnvironment(session string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// tmuxEnvReader wraps real tmux operations.
|
||||
type tmuxEnvReader struct {
|
||||
t *tmux.Tmux
|
||||
}
|
||||
|
||||
func (r *tmuxEnvReader) ListSessions() ([]string, error) {
|
||||
return r.t.ListSessions()
|
||||
}
|
||||
|
||||
func (r *tmuxEnvReader) GetAllEnvironment(session string) (map[string]string, error) {
|
||||
return r.t.GetAllEnvironment(session)
|
||||
}
|
||||
|
||||
// EnvVarsCheck verifies that tmux session environment variables match expected values.
|
||||
type EnvVarsCheck struct {
|
||||
BaseCheck
|
||||
reader SessionEnvReader // nil means use real tmux
|
||||
}
|
||||
|
||||
// NewEnvVarsCheck creates a new env vars check.
|
||||
func NewEnvVarsCheck() *EnvVarsCheck {
|
||||
return &EnvVarsCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "env-vars",
|
||||
CheckDescription: "Verify tmux session environment variables match expected values",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewEnvVarsCheckWithReader creates a check with a custom reader (for testing).
|
||||
func NewEnvVarsCheckWithReader(reader SessionEnvReader) *EnvVarsCheck {
|
||||
c := NewEnvVarsCheck()
|
||||
c.reader = reader
|
||||
return c
|
||||
}
|
||||
|
||||
// Run checks environment variables for all Gas Town sessions.
|
||||
func (c *EnvVarsCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
reader := c.reader
|
||||
if reader == nil {
|
||||
reader = &tmuxEnvReader{t: tmux.NewTmux()}
|
||||
}
|
||||
|
||||
sessions, err := reader.ListSessions()
|
||||
if err != nil {
|
||||
// No tmux server - treat as success (valid when Gas Town is down)
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No tmux sessions running",
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to Gas Town sessions only (gt-* and hq-*)
|
||||
var gtSessions []string
|
||||
for _, sess := range sessions {
|
||||
if strings.HasPrefix(sess, "gt-") || strings.HasPrefix(sess, "hq-") {
|
||||
gtSessions = append(gtSessions, sess)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gtSessions) == 0 {
|
||||
// No Gas Town sessions - treat as success (valid when Gas Town is down)
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No Gas Town sessions running",
|
||||
}
|
||||
}
|
||||
|
||||
var mismatches []string
|
||||
checkedCount := 0
|
||||
|
||||
for _, sess := range gtSessions {
|
||||
identity, err := session.ParseSessionName(sess)
|
||||
if err != nil {
|
||||
// Skip unparseable sessions
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip deacon - it doesn't use the standard env vars
|
||||
if identity.Role == session.RoleDeacon {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get expected env vars based on role
|
||||
expected := config.RoleEnvVars(string(identity.Role), identity.Rig, identity.Name)
|
||||
|
||||
// Get actual tmux env vars
|
||||
actual, err := reader.GetAllEnvironment(sess)
|
||||
if err != nil {
|
||||
mismatches = append(mismatches, fmt.Sprintf("%s: could not read env vars: %v", sess, err))
|
||||
continue
|
||||
}
|
||||
|
||||
checkedCount++
|
||||
|
||||
// Compare each expected var
|
||||
for key, expectedVal := range expected {
|
||||
actualVal, exists := actual[key]
|
||||
if !exists {
|
||||
mismatches = append(mismatches, fmt.Sprintf("%s: missing %s (expected %q)", sess, key, expectedVal))
|
||||
} else if actualVal != expectedVal {
|
||||
mismatches = append(mismatches, fmt.Sprintf("%s: %s=%q (expected %q)", sess, key, actualVal, expectedVal))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mismatches) == 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("All %d session(s) have correct environment variables", checkedCount),
|
||||
}
|
||||
}
|
||||
|
||||
// Add explanation about needing restart
|
||||
details := append(mismatches,
|
||||
"",
|
||||
"Note: Mismatched session env vars won't affect running Claude until sessions restart.",
|
||||
)
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("Found %d env var mismatch(es) across %d session(s)", len(mismatches), checkedCount),
|
||||
Details: details,
|
||||
FixHint: "Run 'gt shutdown && gt up' to restart sessions with correct env vars",
|
||||
}
|
||||
}
|
||||
313
internal/doctor/env_check_test.go
Normal file
313
internal/doctor/env_check_test.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/config"
|
||||
)
|
||||
|
||||
// mockEnvReader implements SessionEnvReader for testing.
|
||||
type mockEnvReader struct {
|
||||
sessions []string
|
||||
sessionEnvs map[string]map[string]string
|
||||
listErr error
|
||||
envErrs map[string]error
|
||||
}
|
||||
|
||||
func (m *mockEnvReader) ListSessions() ([]string, error) {
|
||||
if m.listErr != nil {
|
||||
return nil, m.listErr
|
||||
}
|
||||
return m.sessions, nil
|
||||
}
|
||||
|
||||
func (m *mockEnvReader) GetAllEnvironment(session string) (map[string]string, error) {
|
||||
if m.envErrs != nil {
|
||||
if err, ok := m.envErrs[session]; ok {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if m.sessionEnvs != nil {
|
||||
if env, ok := m.sessionEnvs[session]; ok {
|
||||
return env, nil
|
||||
}
|
||||
}
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_NoSessions(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
if result.Message != "No Gas Town sessions running" {
|
||||
t.Errorf("Message = %q, want %q", result.Message, "No Gas Town sessions running")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_ListSessionsError(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
listErr: errors.New("tmux not running"),
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
// No tmux server is valid (Gas Town can be down)
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
if result.Message != "No tmux sessions running" {
|
||||
t.Errorf("Message = %q, want %q", result.Message, "No tmux sessions running")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_NonGasTownSessions(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"other-session", "my-dev"},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
if result.Message != "No Gas Town sessions running" {
|
||||
t.Errorf("Message = %q, want %q", result.Message, "No Gas Town sessions running")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_MayorCorrect(t *testing.T) {
|
||||
expected := config.RoleEnvVars("mayor", "", "")
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"hq-mayor"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"hq-mayor": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_MayorMissing(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"hq-mayor"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"hq-mayor": {}, // Missing all env vars
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_WitnessCorrect(t *testing.T) {
|
||||
expected := config.RoleEnvVars("witness", "myrig", "")
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-witness": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_WitnessMismatch(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-witness": {
|
||||
"GT_ROLE": "witness",
|
||||
"GT_RIG": "wrongrig", // Wrong rig
|
||||
},
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_RefineryCorrect(t *testing.T) {
|
||||
expected := config.RoleEnvVars("refinery", "myrig", "")
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-refinery"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-refinery": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_PolecatCorrect(t *testing.T) {
|
||||
expected := config.RoleEnvVars("polecat", "myrig", "Toast")
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-Toast"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-Toast": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_PolecatMissing(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-Toast"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-Toast": {
|
||||
"GT_ROLE": "polecat",
|
||||
// Missing GT_RIG, GT_POLECAT, BD_ACTOR, GIT_AUTHOR_NAME
|
||||
},
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_CrewCorrect(t *testing.T) {
|
||||
expected := config.RoleEnvVars("crew", "myrig", "worker1")
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-crew-worker1"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-myrig-crew-worker1": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_MultipleSessions(t *testing.T) {
|
||||
mayorEnv := config.RoleEnvVars("mayor", "", "")
|
||||
witnessEnv := config.RoleEnvVars("witness", "rig1", "")
|
||||
polecatEnv := config.RoleEnvVars("polecat", "rig1", "Toast")
|
||||
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"hq-mayor", "gt-rig1-witness", "gt-rig1-Toast"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"hq-mayor": mayorEnv,
|
||||
"gt-rig1-witness": witnessEnv,
|
||||
"gt-rig1-Toast": polecatEnv,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
if result.Message != "All 3 session(s) have correct environment variables" {
|
||||
t.Errorf("Message = %q", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_MixedCorrectAndMismatch(t *testing.T) {
|
||||
mayorEnv := config.RoleEnvVars("mayor", "", "")
|
||||
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"hq-mayor", "gt-rig1-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"hq-mayor": mayorEnv,
|
||||
"gt-rig1-witness": {
|
||||
"GT_ROLE": "witness",
|
||||
// Missing GT_RIG and other vars
|
||||
},
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_DeaconSkipped(t *testing.T) {
|
||||
// Deacon should be skipped - it doesn't use standard env vars
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"hq-deacon"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"hq-deacon": {}, // Empty - but should be skipped
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
// With only deacon (skipped), checkedCount=0, so should show "No Gas Town sessions"
|
||||
// Actually no - the session exists but is skipped. Let me check the logic...
|
||||
// The check filters to gt-* and hq-* first, then skips deacon. With deacon skipped,
|
||||
// checkedCount stays 0, but gtSessions has 1 entry.
|
||||
// Looking at the code, after skipping deacon, checkedCount=0 and mismatches is empty,
|
||||
// so it returns OK with "All 0 session(s) have correct environment variables"
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_GetEnvError(t *testing.T) {
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-myrig-witness"},
|
||||
envErrs: map[string]error{
|
||||
"gt-myrig-witness": errors.New("session not found"),
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("Status = %v, want StatusWarning", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvVarsCheck_HyphenatedRig(t *testing.T) {
|
||||
// Test rig name with hyphens: "foo-bar"
|
||||
expected := config.RoleEnvVars("witness", "foo-bar", "")
|
||||
reader := &mockEnvReader{
|
||||
sessions: []string{"gt-foo-bar-witness"},
|
||||
sessionEnvs: map[string]map[string]string{
|
||||
"gt-foo-bar-witness": expected,
|
||||
},
|
||||
}
|
||||
check := NewEnvVarsCheckWithReader(reader)
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("Status = %v, want StatusOK", result.Status)
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/tmux"
|
||||
)
|
||||
|
||||
// GTRootCheck verifies that tmux sessions have GT_ROOT set.
|
||||
// Sessions without GT_ROOT cannot find town-level formulas.
|
||||
type GTRootCheck struct {
|
||||
BaseCheck
|
||||
tmux TmuxEnvGetter // nil means use real tmux
|
||||
}
|
||||
|
||||
// TmuxEnvGetter abstracts tmux environment access for testing.
|
||||
type TmuxEnvGetter interface {
|
||||
ListSessions() ([]string, error)
|
||||
GetEnvironment(session, key string) (string, error)
|
||||
}
|
||||
|
||||
// realTmux wraps real tmux operations.
|
||||
type realTmux struct {
|
||||
t *tmux.Tmux
|
||||
}
|
||||
|
||||
func (r *realTmux) ListSessions() ([]string, error) {
|
||||
return r.t.ListSessions()
|
||||
}
|
||||
|
||||
func (r *realTmux) GetEnvironment(session, key string) (string, error) {
|
||||
return r.t.GetEnvironment(session, key)
|
||||
}
|
||||
|
||||
// NewGTRootCheck creates a new GT_ROOT check.
|
||||
func NewGTRootCheck() *GTRootCheck {
|
||||
return >RootCheck{
|
||||
BaseCheck: BaseCheck{
|
||||
CheckName: "gt-root-env",
|
||||
CheckDescription: "Verify sessions have GT_ROOT set for formula discovery",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// NewGTRootCheckWithTmux creates a check with a custom tmux interface (for testing).
|
||||
func NewGTRootCheckWithTmux(t TmuxEnvGetter) *GTRootCheck {
|
||||
c := NewGTRootCheck()
|
||||
c.tmux = t
|
||||
return c
|
||||
}
|
||||
|
||||
// Run checks GT_ROOT environment variable for all Gas Town sessions.
|
||||
func (c *GTRootCheck) Run(ctx *CheckContext) *CheckResult {
|
||||
t := c.tmux
|
||||
if t == nil {
|
||||
t = &realTmux{t: tmux.NewTmux()}
|
||||
}
|
||||
|
||||
sessions, err := t.ListSessions()
|
||||
if err != nil {
|
||||
// No tmux server - not an error, Gas Town might just be down
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No tmux sessions running",
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to Gas Town sessions (gt-* and hq-*)
|
||||
var gtSessions []string
|
||||
for _, sess := range sessions {
|
||||
if strings.HasPrefix(sess, "gt-") || strings.HasPrefix(sess, "hq-") {
|
||||
gtSessions = append(gtSessions, sess)
|
||||
}
|
||||
}
|
||||
|
||||
if len(gtSessions) == 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: "No Gas Town sessions running",
|
||||
}
|
||||
}
|
||||
|
||||
var missingSessions []string
|
||||
var okCount int
|
||||
|
||||
for _, sess := range gtSessions {
|
||||
gtRoot, err := t.GetEnvironment(sess, "GT_ROOT")
|
||||
if err != nil || gtRoot == "" {
|
||||
missingSessions = append(missingSessions, sess)
|
||||
} else {
|
||||
okCount++
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingSessions) == 0 {
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusOK,
|
||||
Message: fmt.Sprintf("All %d session(s) have GT_ROOT set", okCount),
|
||||
}
|
||||
}
|
||||
|
||||
details := make([]string, 0, len(missingSessions)+2)
|
||||
for _, sess := range missingSessions {
|
||||
details = append(details, fmt.Sprintf("Missing GT_ROOT: %s", sess))
|
||||
}
|
||||
details = append(details, "", "Sessions without GT_ROOT cannot find town-level formulas.")
|
||||
|
||||
return &CheckResult{
|
||||
Name: c.Name(),
|
||||
Status: StatusWarning,
|
||||
Message: fmt.Sprintf("%d session(s) missing GT_ROOT environment variable", len(missingSessions)),
|
||||
Details: details,
|
||||
FixHint: "Restart sessions to pick up GT_ROOT: gt shutdown && gt up",
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// mockTmuxEnv implements TmuxEnvGetter for testing.
|
||||
type mockTmuxEnv struct {
|
||||
sessions map[string]map[string]string // session -> env vars
|
||||
listErr error
|
||||
getErr error
|
||||
}
|
||||
|
||||
func (m *mockTmuxEnv) ListSessions() ([]string, error) {
|
||||
if m.listErr != nil {
|
||||
return nil, m.listErr
|
||||
}
|
||||
sessions := make([]string, 0, len(m.sessions))
|
||||
for s := range m.sessions {
|
||||
sessions = append(sessions, s)
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (m *mockTmuxEnv) GetEnvironment(session, key string) (string, error) {
|
||||
if m.getErr != nil {
|
||||
return "", m.getErr
|
||||
}
|
||||
if env, ok := m.sessions[session]; ok {
|
||||
return env[key], nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func TestGTRootCheck_NoSessions(t *testing.T) {
|
||||
mock := &mockTmuxEnv{sessions: map[string]map[string]string{}}
|
||||
check := NewGTRootCheckWithTmux(mock)
|
||||
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("expected StatusOK, got %v", result.Status)
|
||||
}
|
||||
if result.Message != "No Gas Town sessions running" {
|
||||
t.Errorf("unexpected message: %s", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGTRootCheck_NoGasTownSessions(t *testing.T) {
|
||||
mock := &mockTmuxEnv{
|
||||
sessions: map[string]map[string]string{
|
||||
"other-session": {"SOME_VAR": "value"},
|
||||
},
|
||||
}
|
||||
check := NewGTRootCheckWithTmux(mock)
|
||||
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("expected StatusOK, got %v", result.Status)
|
||||
}
|
||||
if result.Message != "No Gas Town sessions running" {
|
||||
t.Errorf("unexpected message: %s", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGTRootCheck_AllSessionsHaveGTRoot(t *testing.T) {
|
||||
mock := &mockTmuxEnv{
|
||||
sessions: map[string]map[string]string{
|
||||
"hq-mayor": {"GT_ROOT": "/home/user/gt", "GT_ROLE": "mayor"},
|
||||
"hq-deacon": {"GT_ROOT": "/home/user/gt", "GT_ROLE": "deacon"},
|
||||
"gt-myrig-witness": {"GT_ROOT": "/home/user/gt", "GT_ROLE": "witness"},
|
||||
"gt-myrig-refinery": {"GT_ROOT": "/home/user/gt", "GT_ROLE": "refinery"},
|
||||
},
|
||||
}
|
||||
check := NewGTRootCheckWithTmux(mock)
|
||||
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("expected StatusOK, got %v", result.Status)
|
||||
}
|
||||
if result.Message != "All 4 session(s) have GT_ROOT set" {
|
||||
t.Errorf("unexpected message: %s", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGTRootCheck_MissingGTRoot(t *testing.T) {
|
||||
mock := &mockTmuxEnv{
|
||||
sessions: map[string]map[string]string{
|
||||
"hq-mayor": {"GT_ROOT": "/home/user/gt"},
|
||||
"gt-myrig-witness": {"GT_ROLE": "witness"}, // Missing GT_ROOT
|
||||
"gt-myrig-refinery": {"GT_ROLE": "refinery"}, // Missing GT_ROOT
|
||||
},
|
||||
}
|
||||
check := NewGTRootCheckWithTmux(mock)
|
||||
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("expected StatusWarning, got %v", result.Status)
|
||||
}
|
||||
if result.Message != "2 session(s) missing GT_ROOT environment variable" {
|
||||
t.Errorf("unexpected message: %s", result.Message)
|
||||
}
|
||||
if result.FixHint != "Restart sessions to pick up GT_ROOT: gt shutdown && gt up" {
|
||||
t.Errorf("unexpected fix hint: %s", result.FixHint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGTRootCheck_EmptyGTRoot(t *testing.T) {
|
||||
mock := &mockTmuxEnv{
|
||||
sessions: map[string]map[string]string{
|
||||
"hq-mayor": {"GT_ROOT": ""}, // Empty GT_ROOT should be treated as missing
|
||||
},
|
||||
}
|
||||
check := NewGTRootCheckWithTmux(mock)
|
||||
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusWarning {
|
||||
t.Errorf("expected StatusWarning, got %v", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGTRootCheck_MixedPrefixes(t *testing.T) {
|
||||
// Test that both gt-* and hq-* sessions are checked
|
||||
mock := &mockTmuxEnv{
|
||||
sessions: map[string]map[string]string{
|
||||
"hq-mayor": {"GT_ROOT": "/home/user/gt"},
|
||||
"gt-rig-witness": {"GT_ROOT": "/home/user/gt"},
|
||||
"other-session": {}, // Should be ignored
|
||||
"random": {}, // Should be ignored
|
||||
},
|
||||
}
|
||||
check := NewGTRootCheckWithTmux(mock)
|
||||
|
||||
result := check.Run(&CheckContext{})
|
||||
|
||||
if result.Status != StatusOK {
|
||||
t.Errorf("expected StatusOK, got %v", result.Status)
|
||||
}
|
||||
// Should only count the 2 Gas Town sessions
|
||||
if result.Message != "All 2 session(s) have GT_ROOT set" {
|
||||
t.Errorf("unexpected message: %s", result.Message)
|
||||
}
|
||||
}
|
||||
@@ -497,6 +497,28 @@ func (t *Tmux) GetEnvironment(session, key string) (string, error) {
|
||||
return parts[1], nil
|
||||
}
|
||||
|
||||
// GetAllEnvironment returns all environment variables for a session.
|
||||
func (t *Tmux) GetAllEnvironment(session string) (map[string]string, error) {
|
||||
out, err := t.run("show-environment", "-t", session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
env := make(map[string]string)
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "-") {
|
||||
// Skip empty lines and unset markers (lines starting with -)
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
env[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// RenameSession renames a session.
|
||||
func (t *Tmux) RenameSession(oldName, newName string) error {
|
||||
_, err := t.run("rename-session", "-t", oldName, newName)
|
||||
|
||||
Reference in New Issue
Block a user