package hooks import ( "encoding/json" "os" "path/filepath" "strings" "testing" "github.com/ajhahnde/eeco/internal/config" ) // newMachineryCfg builds a config with a per-user dir (so the machinery has // a /.claude/settings.json to write) and a workspace for the // ledger. The .claude dir is intentionally NOT pre-created, exercising the // MkdirAll-on-enable path. func newMachineryCfg(t *testing.T) *config.Config { t.Helper() root := t.TempDir() userDir := filepath.Join(root, "tester") ws := filepath.Join(userDir, ".eeco") if err := os.MkdirAll(filepath.Join(ws, "state"), 0o755); err != nil { t.Fatal(err) } return &config.Config{ RepoRoot: root, UserDir: userDir, WorkspaceName: ".eeco", Workspace: ws, } } func TestCockpitMachinery_EnableInstallsGuardGroup(t *testing.T) { cfg := newMachineryCfg(t) if _, err := EnableCockpitMachinery(cfg); err != nil { t.Fatalf("EnableCockpitMachinery: %v", err) } path := cockpitSettingsPath(cfg) b, err := os.ReadFile(path) if err != nil { t.Fatalf("settings.json not written: %v", err) } root := map[string]any{} if err := json.Unmarshal(b, &root); err != nil { t.Fatalf("settings.json not valid JSON: %v", err) } if !machineryFullyInstalled(root) { t.Errorf("not all machinery groups present after enable:\n%s", b) } // All four event groups land (PreToolUse / SessionStart / Stop / PostToolUse). for _, ev := range []string{"PreToolUse", "SessionStart", "Stop", "PostToolUse"} { if len(eventGroups(root, ev)) == 0 { t.Errorf("event %s group missing after enable", ev) } } // Ledger records the install. l, _ := loadLedger(cfg) if !l.CockpitMachinery.Installed { t.Error("ledger CockpitMachinery.Installed = false after enable") } } func TestCockpitMachinery_EnableIdempotent(t *testing.T) { cfg := newMachineryCfg(t) if _, err := EnableCockpitMachinery(cfg); err != nil { t.Fatal(err) } path := cockpitSettingsPath(cfg) first, _ := os.ReadFile(path) msg, err := EnableCockpitMachinery(cfg) if err != nil { t.Fatal(err) } second, _ := os.ReadFile(path) if string(first) != string(second) { t.Error("settings.json changed on a no-op re-enable") } if msg == "" { t.Error("expected an already-enabled message") } } func TestCockpitMachinery_OffRestoresAndPreservesForeign(t *testing.T) { cfg := newMachineryCfg(t) path := cockpitSettingsPath(cfg) // A pre-existing settings file with a foreign PreToolUse group + an // unknown key; `off` must restore it byte-for-byte after on/off. if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } original := `{ "model": "opus", "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "some-other-tool guard" } ] } ] } } ` if err := os.WriteFile(path, []byte(original), 0o644); err != nil { t.Fatal(err) } if _, err := EnableCockpitMachinery(cfg); err != nil { t.Fatalf("enable: %v", err) } if _, err := DisableCockpitMachinery(cfg); err != nil { t.Fatalf("disable: %v", err) } b, _ := os.ReadFile(path) root := map[string]any{} if err := json.Unmarshal(b, &root); err != nil { t.Fatalf("settings.json not valid JSON after off: %v", err) } if machineryInstalled(root) { t.Error("machinery group survived disable") } // The foreign group + unknown key must remain. groups := eventGroups(root, "PreToolUse") if len(groups) != 1 { t.Fatalf("foreign PreToolUse group count = %d, want 1", len(groups)) } if root["model"] != "opus" { t.Errorf("unknown key not preserved: model=%v", root["model"]) } } func TestCockpitMachinery_OffRemovesFileItCreated(t *testing.T) { cfg := newMachineryCfg(t) path := cockpitSettingsPath(cfg) if _, err := os.Stat(path); !os.IsNotExist(err) { t.Fatalf("settings file should be absent before enable, stat err=%v", err) } if _, err := EnableCockpitMachinery(cfg); err != nil { t.Fatalf("enable: %v", err) } if _, err := DisableCockpitMachinery(cfg); err != nil { t.Fatalf("disable: %v", err) } // eeco created the file and our group was its only content → absent // again (byte-for-byte restore), not a leftover {} shell. if _, err := os.Stat(path); !os.IsNotExist(err) { t.Errorf("settings file eeco created should be removed on off, stat err=%v", err) } } func TestCockpitMachinery_DisableNotEnabled(t *testing.T) { cfg := newMachineryCfg(t) msg, err := DisableCockpitMachinery(cfg) if err != nil { t.Fatal(err) } if msg != "cockpit machinery not enabled" { t.Errorf("disable-not-enabled msg = %q", msg) } } func TestCockpitMachinery_StatusReflectsDisk(t *testing.T) { cfg := newMachineryCfg(t) lines := CockpitMachineryStatus(cfg) if len(lines) == 0 || !strings.Contains(lines[0], "off") { t.Errorf("status before enable = %v, want off", lines) } if _, err := EnableCockpitMachinery(cfg); err != nil { t.Fatal(err) } lines = CockpitMachineryStatus(cfg) if !strings.Contains(lines[0], "on") { t.Errorf("status after enable = %v, want on", lines) } // One header line + one line per managed event, every event reading "on". if len(lines) != 1+len(machineryHookSet()) { t.Fatalf("status line count = %d, want %d", len(lines), 1+len(machineryHookSet())) } for _, ev := range []string{"PreToolUse", "SessionStart", "Stop", "PostToolUse"} { found := false for _, ln := range lines[1:] { if strings.Contains(ln, ev) && strings.Contains(ln, "on") { found = true } } if !found { t.Errorf("status missing an on-line for event %s:\n%v", ev, lines) } } } func TestCockpitMachinery_Refresh(t *testing.T) { cfg := newMachineryCfg(t) // Not enabled → a clean no-op message, nothing touched. if msg, err := RefreshCockpitMachinery(cfg); err != nil || !strings.Contains(msg, "not enabled") { t.Errorf("refresh before enable = (%q, %v), want a not-enabled no-op", msg, err) } if _, err := EnableCockpitMachinery(cfg); err != nil { t.Fatal(err) } // Freshly enabled → commands already embed selfPath(), so refresh is a // no-op "already current". msg, err := RefreshCockpitMachinery(cfg) if err != nil { t.Fatal(err) } if !strings.Contains(msg, "already current") { t.Errorf("refresh of a freshly-enabled machinery = %q, want already-current", msg) } }