package cockpit import ( "os" "path/filepath" "testing" "github.com/ajhahnde/eeco/internal/config" ) func TestScanAllowlistForWriteGitVerbs(t *testing.T) { base := composeAllowedTools(loadHandover(t)) cases := []struct { name string allowlist []string want []string }{ {"real handover allowlist holds", base, nil}, {"git commit is forbidden", append(append([]string{}, base...), "Bash(git commit:*)"), []string{"commit"}}, {"git push is forbidden", append(append([]string{}, base...), "Bash(git push:*)"), []string{"push"}}, {"git stash list passes", []string{"Bash(git stash list:*)"}, nil}, {"bare git stash fails", []string{"Bash(git stash:*)"}, []string{"stash"}}, {"git branch --show-current passes", []string{"Bash(git branch --show-current:*)"}, nil}, {"bare git branch fails", []string{"Bash(git branch:*)"}, []string{"branch"}}, {"git branch -D fails", []string{"Bash(git branch -D:*)"}, []string{"branch"}}, {"non-bash tools ignored", []string{"Read", "Write", "Agent"}, nil}, {"non-git bash ignored", []string{"Bash(rm -rf:*)"}, nil}, // rm is not a git subverb here } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got := ScanAllowlistForWriteGitVerbs(tc.allowlist, defaultForbiddenGitVerbs) if len(got) != len(tc.want) { t.Fatalf("got %v, want %v", got, tc.want) } for i := range tc.want { if got[i] != tc.want[i] { t.Errorf("hit %d = %q, want %q", i, got[i], tc.want[i]) } } }) } } func TestGenerate_RefusesForbiddenVerb(t *testing.T) { cfg := testConfig(t) pb := loadHandover(t) // Poison the playbook: add a write-git capability. pb.Capabilities = append(pb.Capabilities, Capability{Kind: "bash", Verb: "git commit", Scope: "*"}) if _, err := Generate(cfg, pb, "claude"); err == nil { t.Fatal("expected Generate to refuse a poisoned playbook") } // Nothing written, no ledger. if _, err := os.Stat(filepath.Join(cfg.UserDir, ".claude", "skills", "handover", "SKILL.md")); err == nil { t.Error("a SKILL.md was written despite the safety refusal") } if _, err := os.Stat(ledgerPath(cfg)); err == nil { t.Error("a ledger was written despite the safety refusal") } } // testConfig builds a minimal Config whose UserDir/Workspace point inside a // throwaway temp dir, the only fields the cockpit emit path touches. func testConfig(t *testing.T) *config.Config { t.Helper() root := t.TempDir() return &config.Config{ UserDir: filepath.Join(root, "tester"), Workspace: filepath.Join(root, "tester", ".eeco"), } }