Go 216 lines
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 <UserDir>/.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)
}
}