Merge pull request #268 from julianknutsen/fix/gt-root-env
fix: agents cannot find town-level formulas
This commit is contained in:
@@ -120,6 +120,7 @@ func runDoctor(cmd *cobra.Command, args []string) error {
|
|||||||
d.Register(doctor.NewRoutesCheck())
|
d.Register(doctor.NewRoutesCheck())
|
||||||
d.Register(doctor.NewOrphanSessionCheck())
|
d.Register(doctor.NewOrphanSessionCheck())
|
||||||
d.Register(doctor.NewOrphanProcessCheck())
|
d.Register(doctor.NewOrphanProcessCheck())
|
||||||
|
d.Register(doctor.NewGTRootCheck())
|
||||||
d.Register(doctor.NewWispGCCheck())
|
d.Register(doctor.NewWispGCCheck())
|
||||||
d.Register(doctor.NewBranchCheck())
|
d.Register(doctor.NewBranchCheck())
|
||||||
d.Register(doctor.NewBeadsSyncOrphanCheck())
|
d.Register(doctor.NewBeadsSyncOrphanCheck())
|
||||||
|
|||||||
@@ -1064,13 +1064,15 @@ func findTownRootFromCwd() (string, error) {
|
|||||||
// prompt is optional - if provided, appended as the initial prompt.
|
// prompt is optional - if provided, appended as the initial prompt.
|
||||||
func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) string {
|
func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) string {
|
||||||
var rc *RuntimeConfig
|
var rc *RuntimeConfig
|
||||||
|
var townRoot string
|
||||||
if rigPath != "" {
|
if rigPath != "" {
|
||||||
// Derive town root from rig path
|
// Derive town root from rig path
|
||||||
townRoot := filepath.Dir(rigPath)
|
townRoot = filepath.Dir(rigPath)
|
||||||
rc = ResolveAgentConfig(townRoot, rigPath)
|
rc = ResolveAgentConfig(townRoot, rigPath)
|
||||||
} else {
|
} else {
|
||||||
// Try to detect town root from cwd for town-level agents (mayor, deacon)
|
// Try to detect town root from cwd for town-level agents (mayor, deacon)
|
||||||
townRoot, err := findTownRootFromCwd()
|
var err error
|
||||||
|
townRoot, err = findTownRootFromCwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rc = DefaultRuntimeConfig()
|
rc = DefaultRuntimeConfig()
|
||||||
} else {
|
} else {
|
||||||
@@ -1078,6 +1080,11 @@ func BuildStartupCommand(envVars map[string]string, rigPath, prompt string) stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add GT_ROOT so agents can find town-level resources (formulas, etc.)
|
||||||
|
if townRoot != "" {
|
||||||
|
envVars["GT_ROOT"] = townRoot
|
||||||
|
}
|
||||||
|
|
||||||
// Build environment export prefix
|
// Build environment export prefix
|
||||||
var exports []string
|
var exports []string
|
||||||
for k, v := range envVars {
|
for k, v := range envVars {
|
||||||
|
|||||||
119
internal/doctor/gtroot_check.go
Normal file
119
internal/doctor/gtroot_check.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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",
|
||||||
|
}
|
||||||
|
}
|
||||||
147
internal/doctor/gtroot_check_test.go
Normal file
147
internal/doctor/gtroot_check_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user