Go 127 lines
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)
}
}
}