ajhahn.de
← eeco
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)
		}
	}
}