package hooks import ( "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" "github.com/ajhahnde/eeco/internal/config" ) // gitRepoCfg builds a config rooted at a real git repo with one commit, plus a // workspace for the throttle stamp. Skips when git is unavailable. func gitRepoCfg(t *testing.T) *config.Config { t.Helper() if _, err := exec.LookPath("git"); err != nil { t.Skip("git not available") } root := t.TempDir() for _, args := range [][]string{ {"init", "-q"}, {"config", "user.email", "t@x"}, {"config", "user.name", "t"}, } { c := exec.Command("git", args...) c.Dir = root if out, err := c.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } if err := os.WriteFile(filepath.Join(root, "f.txt"), []byte("x"), 0o644); err != nil { t.Fatal(err) } for _, args := range [][]string{{"add", "-A"}, {"commit", "-qm", "seed"}} { c := exec.Command("git", args...) c.Dir = root if out, err := c.CombinedOutput(); err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } } 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 TestStopNudge_FiresOnDirtyThenThrottled(t *testing.T) { cfg := gitRepoCfg(t) if err := os.WriteFile(filepath.Join(cfg.RepoRoot, "wip.txt"), []byte("z"), 0o644); err != nil { t.Fatal(err) } now := time.Now() reason, fire := StopNudge(cfg, now) if !fire { t.Fatal("expected a nudge on a dirty tree with no handover note") } if !strings.Contains(reason, "handover") || !strings.Contains(reason, "dirty working tree") { t.Errorf("nudge reason off: %q", reason) } // Stamp written → a second call within the throttle window is silent. if _, fire := StopNudge(cfg, now.Add(time.Minute)); fire { t.Error("second nudge within the 6h throttle should be silent") } // Past the throttle, it can fire again. if _, fire := StopNudge(cfg, now.Add(7*time.Hour)); !fire { t.Error("nudge should fire again past the 6h throttle") } } func TestStopNudge_RecentStampSilences(t *testing.T) { cfg := gitRepoCfg(t) if err := os.WriteFile(filepath.Join(cfg.RepoRoot, "wip.txt"), []byte("z"), 0o644); err != nil { t.Fatal(err) } now := time.Now() stamp := filepath.Join(cfg.Workspace, "state", stopNudgeStampName) if err := os.WriteFile(stamp, []byte(strconv.FormatInt(now.Unix(), 10)), 0o644); err != nil { t.Fatal(err) } if _, fire := StopNudge(cfg, now.Add(time.Hour)); fire { t.Error("a fresh throttle stamp must silence the nudge") } } func TestThrottleElapsed(t *testing.T) { stamp := filepath.Join(t.TempDir(), "x.last") now := time.Now() if !throttleElapsed(stamp, now, time.Hour) { t.Error("a missing stamp should count as elapsed") } writeStamp(stamp, now) if throttleElapsed(stamp, now.Add(time.Minute), time.Hour) { t.Error("a fresh stamp should not be elapsed") } if !throttleElapsed(stamp, now.Add(2*time.Hour), time.Hour) { t.Error("a stamp older than min should be elapsed") } } func TestJoinReasons(t *testing.T) { cases := []struct { in []string want string }{ {nil, ""}, {[]string{"a"}, "a"}, {[]string{"a", "b"}, "a and b"}, {[]string{"a", "b", "c"}, "a, b and c"}, } for _, c := range cases { if got := joinReasons(c.in); got != c.want { t.Errorf("joinReasons(%v) = %q, want %q", c.in, got, c.want) } } }