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)
}
}