fix: create mayor/daemon.json during gt start and gt doctor --fix (#225)
* fix: create mayor/daemon.json during gt start and gt doctor --fix (#5) - Add DaemonPatrolConfig type with heartbeat and patrol settings - Add Load/Save/Ensure functions for daemon patrol config - Create daemon.json in gt start (non-fatal if fails) - Make PatrolHooksWiredCheck fixable with Fix() method - Add comprehensive tests for both config and doctor checks This fixes the issue where gt doctor expects mayor/daemon.json to exist but it was never created by gt start or any other command. * refactor: use constants.DirMayor instead of hardcoded string
This commit is contained in:
@@ -971,6 +971,214 @@ func TestLoadRuntimeConfigFallsBackToDefaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonPatrolConfigRoundTrip(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "mayor", "daemon.json")
|
||||
|
||||
original := NewDaemonPatrolConfig()
|
||||
original.Patrols["custom"] = PatrolConfig{
|
||||
Enabled: true,
|
||||
Interval: "10m",
|
||||
Agent: "custom-agent",
|
||||
}
|
||||
|
||||
if err := SaveDaemonPatrolConfig(path, original); err != nil {
|
||||
t.Fatalf("SaveDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadDaemonPatrolConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
|
||||
if loaded.Type != "daemon-patrol-config" {
|
||||
t.Errorf("Type = %q, want 'daemon-patrol-config'", loaded.Type)
|
||||
}
|
||||
if loaded.Version != CurrentDaemonPatrolConfigVersion {
|
||||
t.Errorf("Version = %d, want %d", loaded.Version, CurrentDaemonPatrolConfigVersion)
|
||||
}
|
||||
if loaded.Heartbeat == nil || !loaded.Heartbeat.Enabled {
|
||||
t.Error("Heartbeat not preserved")
|
||||
}
|
||||
if len(loaded.Patrols) != 4 {
|
||||
t.Errorf("Patrols count = %d, want 4", len(loaded.Patrols))
|
||||
}
|
||||
if custom, ok := loaded.Patrols["custom"]; !ok || custom.Agent != "custom-agent" {
|
||||
t.Error("custom patrol not preserved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonPatrolConfigValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *DaemonPatrolConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid default config",
|
||||
config: NewDaemonPatrolConfig(),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid minimal config",
|
||||
config: &DaemonPatrolConfig{
|
||||
Type: "daemon-patrol-config",
|
||||
Version: 1,
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "wrong type",
|
||||
config: &DaemonPatrolConfig{
|
||||
Type: "wrong",
|
||||
Version: 1,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "future version rejected",
|
||||
config: &DaemonPatrolConfig{
|
||||
Type: "daemon-patrol-config",
|
||||
Version: 999,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateDaemonPatrolConfig(tt.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateDaemonPatrolConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadDaemonPatrolConfigNotFound(t *testing.T) {
|
||||
_, err := LoadDaemonPatrolConfig("/nonexistent/path.json")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemonPatrolConfigPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
townRoot string
|
||||
expected string
|
||||
}{
|
||||
{"/home/user/gt", "/home/user/gt/mayor/daemon.json"},
|
||||
{"/var/lib/gastown", "/var/lib/gastown/mayor/daemon.json"},
|
||||
{"/tmp/test-workspace", "/tmp/test-workspace/mayor/daemon.json"},
|
||||
{"~/gt", "~/gt/mayor/daemon.json"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.townRoot, func(t *testing.T) {
|
||||
path := DaemonPatrolConfigPath(tt.townRoot)
|
||||
if path != tt.expected {
|
||||
t.Errorf("DaemonPatrolConfigPath(%q) = %q, want %q", tt.townRoot, path, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDaemonPatrolConfig(t *testing.T) {
|
||||
t.Run("creates config if missing", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(dir, "mayor"), 0755); err != nil {
|
||||
t.Fatalf("creating mayor dir: %v", err)
|
||||
}
|
||||
|
||||
err := EnsureDaemonPatrolConfig(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
|
||||
path := DaemonPatrolConfigPath(dir)
|
||||
loaded, err := LoadDaemonPatrolConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
if loaded.Type != "daemon-patrol-config" {
|
||||
t.Errorf("Type = %q, want 'daemon-patrol-config'", loaded.Type)
|
||||
}
|
||||
if len(loaded.Patrols) != 3 {
|
||||
t.Errorf("Patrols count = %d, want 3 (deacon, witness, refinery)", len(loaded.Patrols))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("preserves existing config", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "mayor", "daemon.json")
|
||||
|
||||
existing := &DaemonPatrolConfig{
|
||||
Type: "daemon-patrol-config",
|
||||
Version: 1,
|
||||
Patrols: map[string]PatrolConfig{
|
||||
"custom-only": {Enabled: true, Agent: "custom"},
|
||||
},
|
||||
}
|
||||
if err := SaveDaemonPatrolConfig(path, existing); err != nil {
|
||||
t.Fatalf("SaveDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
|
||||
err := EnsureDaemonPatrolConfig(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
|
||||
loaded, err := LoadDaemonPatrolConfig(path)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadDaemonPatrolConfig: %v", err)
|
||||
}
|
||||
if len(loaded.Patrols) != 1 {
|
||||
t.Errorf("Patrols count = %d, want 1 (should preserve existing)", len(loaded.Patrols))
|
||||
}
|
||||
if _, ok := loaded.Patrols["custom-only"]; !ok {
|
||||
t.Error("existing custom patrol was overwritten")
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestNewDaemonPatrolConfig(t *testing.T) {
|
||||
cfg := NewDaemonPatrolConfig()
|
||||
|
||||
if cfg.Type != "daemon-patrol-config" {
|
||||
t.Errorf("Type = %q, want 'daemon-patrol-config'", cfg.Type)
|
||||
}
|
||||
if cfg.Version != CurrentDaemonPatrolConfigVersion {
|
||||
t.Errorf("Version = %d, want %d", cfg.Version, CurrentDaemonPatrolConfigVersion)
|
||||
}
|
||||
if cfg.Heartbeat == nil {
|
||||
t.Fatal("Heartbeat is nil")
|
||||
}
|
||||
if !cfg.Heartbeat.Enabled {
|
||||
t.Error("Heartbeat.Enabled should be true by default")
|
||||
}
|
||||
if cfg.Heartbeat.Interval != "3m" {
|
||||
t.Errorf("Heartbeat.Interval = %q, want '3m'", cfg.Heartbeat.Interval)
|
||||
}
|
||||
if len(cfg.Patrols) != 3 {
|
||||
t.Errorf("Patrols count = %d, want 3", len(cfg.Patrols))
|
||||
}
|
||||
|
||||
for _, name := range []string{"deacon", "witness", "refinery"} {
|
||||
patrol, ok := cfg.Patrols[name]
|
||||
if !ok {
|
||||
t.Errorf("missing %s patrol", name)
|
||||
continue
|
||||
}
|
||||
if !patrol.Enabled {
|
||||
t.Errorf("%s patrol should be enabled by default", name)
|
||||
}
|
||||
if patrol.Agent != name {
|
||||
t.Errorf("%s patrol Agent = %q, want %q", name, patrol.Agent, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveTownSettings(t *testing.T) {
|
||||
t.Run("saves valid town settings", func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user