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