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