ajhahn.de
← eeco
Go 184 lines
package hooks

import (
	"encoding/json"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestCommitGuard_NotConfigured(t *testing.T) {
	cfg := newCfg(t, "")
	if _, err := EnableCommitGuard(cfg); err != ErrCommitGuardNotConfigured {
		t.Errorf("enable err = %v, want ErrCommitGuardNotConfigured", err)
	}
	if _, err := DisableCommitGuard(cfg); err != ErrCommitGuardNotConfigured {
		t.Errorf("disable err = %v, want ErrCommitGuardNotConfigured", err)
	}
	if _, err := RefreshCommitGuard(cfg); err != ErrCommitGuardNotConfigured {
		t.Errorf("refresh err = %v, want ErrCommitGuardNotConfigured", err)
	}
}

func TestCommitGuard_EnableWritesPreToolUseGroup(t *testing.T) {
	dir := t.TempDir()
	sp := filepath.Join(dir, "settings.json")
	cfg := newCfg(t, sp)

	if _, err := EnableCommitGuard(cfg); err != nil {
		t.Fatalf("EnableCommitGuard: %v", err)
	}
	b, err := os.ReadFile(sp)
	if err != nil {
		t.Fatal(err)
	}
	var root map[string]any
	if err := json.Unmarshal(b, &root); err != nil {
		t.Fatalf("settings not valid JSON: %v", err)
	}
	if !commitGuardInstalled(root) {
		t.Errorf("commit-guard group not present:\n%s", b)
	}
	// The group carries the Bash matcher and the hidden runner command.
	groups := preToolGroups(root)
	if len(groups) != 1 {
		t.Fatalf("want 1 PreToolUse group, got %d", len(groups))
	}
	gm := groups[0].(map[string]any)
	if gm["matcher"] != "Bash" {
		t.Errorf("matcher = %v, want Bash", gm["matcher"])
	}
	cmd := gm["hooks"].([]any)[0].(map[string]any)["command"].(string)
	if !strings.Contains(cmd, commitGuardToken) {
		t.Errorf("command missing token: %q", cmd)
	}
	// Idempotent.
	msg, err := EnableCommitGuard(cfg)
	if err != nil || !strings.Contains(msg, "already enabled") {
		t.Errorf("re-enable: msg=%q err=%v", msg, err)
	}
}

func TestCommitGuard_DisablePreservesForeignGroupsAndKeys(t *testing.T) {
	dir := t.TempDir()
	sp := filepath.Join(dir, "settings.json")
	cfg := newCfg(t, sp)
	original := `{
  "model": "x",
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [ { "type": "command", "command": "other-tool guard" } ] }
    ],
    "SessionStart": [
      { "hooks": [ { "type": "command", "command": "keep-session" } ] }
    ]
  }
}`
	if err := os.WriteFile(sp, []byte(original), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := EnableCommitGuard(cfg); err != nil {
		t.Fatalf("EnableCommitGuard: %v", err)
	}
	// Two PreToolUse groups now (foreign + eeco).
	var root map[string]any
	b, _ := os.ReadFile(sp)
	if err := json.Unmarshal(b, &root); err != nil {
		t.Fatal(err)
	}
	if len(preToolGroups(root)) != 2 {
		t.Fatalf("want 2 PreToolUse groups after enable, got %d", len(preToolGroups(root)))
	}

	if _, err := DisableCommitGuard(cfg); err != nil {
		t.Fatalf("DisableCommitGuard: %v", err)
	}
	b, _ = os.ReadFile(sp)
	if err := json.Unmarshal(b, &root); err != nil {
		t.Fatal(err)
	}
	if commitGuardInstalled(root) {
		t.Error("eeco group still present after disable")
	}
	if root["model"] != "x" {
		t.Errorf("foreign top-level key lost: %v", root["model"])
	}
	groups := preToolGroups(root)
	if len(groups) != 1 {
		t.Fatalf("foreign PreToolUse group not preserved, groups=%d", len(groups))
	}
	gm := groups[0].(map[string]any)
	cmd := gm["hooks"].([]any)[0].(map[string]any)["command"].(string)
	if cmd != "other-tool guard" {
		t.Errorf("wrong PreToolUse group survived: %q", cmd)
	}
	// The SessionStart channel is untouched.
	if len(sessionGroups(root)) != 1 {
		t.Errorf("SessionStart group disturbed by commit-guard disable")
	}
}

func TestCommitGuard_EnableRefusesMalformedJSON(t *testing.T) {
	dir := t.TempDir()
	sp := filepath.Join(dir, "settings.json")
	cfg := newCfg(t, sp)
	bad := "{ not valid json"
	if err := os.WriteFile(sp, []byte(bad), 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := EnableCommitGuard(cfg); err == nil {
		t.Fatal("expected refusal on malformed settings")
	}
	if b, _ := os.ReadFile(sp); string(b) != bad {
		t.Errorf("malformed settings file was modified:\n%s", b)
	}
}

func TestCommitGuard_DisableNoOpWhenAbsent(t *testing.T) {
	dir := t.TempDir()
	sp := filepath.Join(dir, "settings.json")
	cfg := newCfg(t, sp)
	original := `{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"keep-me"}]}]}}`
	if err := os.WriteFile(sp, []byte(original), 0o644); err != nil {
		t.Fatal(err)
	}
	msg, err := DisableCommitGuard(cfg)
	if err != nil {
		t.Fatalf("DisableCommitGuard: %v", err)
	}
	if !strings.Contains(msg, "not enabled") {
		t.Errorf("msg = %q, want 'not enabled'", msg)
	}
	if b, _ := os.ReadFile(sp); string(b) != original {
		t.Errorf("settings modified despite no eeco group:\n%s", b)
	}
}

func TestCommitGuard_RefreshRewritesStalePath(t *testing.T) {
	dir := t.TempDir()
	sp := filepath.Join(dir, "settings.json")
	cfg := newCfg(t, sp)
	// Install a group carrying the token but a stale binary path.
	stale := `{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":"\"/old/path/eeco\" ` + commitGuardToken + `"}]}]}}`
	if err := os.WriteFile(sp, []byte(stale), 0o644); err != nil {
		t.Fatal(err)
	}
	msg, err := RefreshCommitGuard(cfg)
	if err != nil {
		t.Fatalf("RefreshCommitGuard: %v", err)
	}
	if !strings.Contains(msg, "refreshed") {
		t.Errorf("msg = %q, want 'refreshed'", msg)
	}
	b, _ := os.ReadFile(sp)
	if strings.Contains(string(b), "/old/path/eeco") {
		t.Errorf("stale path not rewritten:\n%s", b)
	}
	// Second refresh is a no-op.
	msg, err = RefreshCommitGuard(cfg)
	if err != nil || !strings.Contains(msg, "already current") {
		t.Errorf("second refresh: msg=%q err=%v, want 'already current'", msg, err)
	}
}