package hooks import ( "os" "path/filepath" "strings" "testing" "time" "github.com/ajhahnde/eeco/internal/cockpit" "github.com/ajhahnde/eeco/internal/config" "github.com/ajhahnde/eeco/internal/playbooks" ) // watchCfg builds a config with a workspace for the flags/stamps (no git). func watchCfg(t *testing.T) *config.Config { t.Helper() root := t.TempDir() ws := filepath.Join(root, "tester", ".eeco") if err := os.MkdirAll(filepath.Join(ws, "state"), 0o755); err != nil { t.Fatal(err) } return &config.Config{ RepoRoot: root, UserDir: filepath.Join(root, "tester"), WorkspaceName: ".eeco", Workspace: ws, } } func TestContractWatch_FlagsWatchedInput(t *testing.T) { cfg := watchCfg(t) if !ContractWatch(cfg, cockpit.SelectionPath(cfg)) { t.Fatal("editing the selection store should drop a flag") } for _, name := range []string{contractChangedFlag, cockpitDirtyFlag} { if _, err := os.Stat(filepath.Join(cfg.Workspace, "state", name)); err != nil { t.Errorf("flag %s not written: %v", name, err) } } if !ContractWatch(cfg, filepath.Join(cfg.Workspace, "config.local")) { t.Error("editing config.local should drop a flag") } } func TestContractWatch_IgnoresUnrelated(t *testing.T) { cfg := watchCfg(t) if ContractWatch(cfg, filepath.Join(cfg.RepoRoot, "README.md")) { t.Error("an unrelated edit must not drop a flag") } if ContractWatch(cfg, "") { t.Error("a blank path must be a no-op") } if _, err := os.Stat(filepath.Join(cfg.Workspace, "state", contractChangedFlag)); !os.IsNotExist(err) { t.Errorf("no flag should exist after unrelated edits, stat err=%v", err) } } func TestDocDriftNudge_FlagFiresAndClears(t *testing.T) { cfg := watchCfg(t) flag := filepath.Join(cfg.Workspace, "state", contractChangedFlag) if err := os.WriteFile(flag, nil, 0o644); err != nil { t.Fatal(err) } line, fire := DocDriftNudge(cfg, time.Now()) if !fire { t.Fatal("a contract-changed flag should fire the nudge") } if !strings.Contains(line, "changed") { t.Errorf("flag-driven nudge text off: %q", line) } if _, err := os.Stat(flag); !os.IsNotExist(err) { t.Error("the nudge should clear the contract-changed flag (one-shot)") } // Right after firing (flag cleared, stamp fresh) it is silent. if _, fire := DocDriftNudge(cfg, time.Now()); fire { t.Error("the nudge should be silent right after firing") } } func TestDocDriftNudge_BackstopSilentWhenCockpitUnused(t *testing.T) { cfg := watchCfg(t) // No flag, no stamp (backstop elapsed), but the cockpit was never generated // here → must stay silent (the empty-ledger gate). if _, fire := DocDriftNudge(cfg, time.Now()); fire { t.Error("the backstop must not fire where the cockpit was never generated") } } func TestDocDriftNudge_BackstopFiresWhenGenerated(t *testing.T) { cfg := watchCfg(t) if err := cockpit.SaveSelection(cfg, cockpit.Selection{Targets: []string{"claude"}, Playbooks: []string{"handover"}}); err != nil { t.Fatal(err) } pb, err := playbooks.Get("handover") if err != nil { t.Fatal(err) } if _, err := cockpit.Generate(cfg, pb, "claude"); err != nil { t.Fatal(err) } // No flag, no stamp (backstop elapsed) and the cockpit IS generated → fire. line, fire := DocDriftNudge(cfg, time.Now()) if !fire { t.Fatal("the backstop should fire once the cockpit is generated") } if !strings.Contains(line, "backstop") { t.Errorf("backstop nudge text off: %q", line) } } func TestClearGitWriteSentinels(t *testing.T) { cfg := watchCfg(t) stateDir := filepath.Join(cfg.Workspace, "state") for _, k := range []string{"commit", "tag"} { if err := os.WriteFile(filepath.Join(stateDir, "git-"+k+"-authorized"), nil, 0o600); err != nil { t.Fatal(err) } } ClearGitWriteSentinels(cfg) for _, k := range []string{"commit", "tag"} { if _, err := os.Stat(filepath.Join(stateDir, "git-"+k+"-authorized")); !os.IsNotExist(err) { t.Errorf("sentinel git-%s-authorized not cleared, stat err=%v", k, err) } } }