Go 313 lines
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
// cockpitSkillPath is where the emitted handover SKILL.md lands for a repo
// rooted at root: <root>/tester/.claude/skills/handover/SKILL.md (the
// EECO_USERNAME=tester pin scopes UserDir to <root>/tester).
func cockpitSkillPath(root string) string {
return filepath.Join(root, "tester", ".claude", "skills", "handover", "SKILL.md")
}
func setupInited(t *testing.T) string {
t.Helper()
root := newGitRepo(t)
chdir(t, root)
writeFile(t, root, "go.mod", "module sample\n\ngo 1.24\n")
if code := runInit(nil, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatalf("setup init exit=%d", code)
}
return root
}
func TestRunCockpit_GenerateVerifyOff(t *testing.T) {
root := setupInited(t)
skill := cockpitSkillPath(root)
ledger := wsPath(root, "state", "cockpit.json")
// generate
var out, errOut bytes.Buffer
if code := runCockpit([]string{"generate"}, &out, &errOut); code != 0 {
t.Fatalf("generate exit=%d stderr=%s", code, errOut.String())
}
b, err := os.ReadFile(skill)
if err != nil {
t.Fatalf("SKILL.md not written: %v", err)
}
body := string(b)
if !strings.Contains(body, "name: handover\n") || !strings.Contains(body, "allowed-tools: ") {
t.Errorf("emitted SKILL.md frontmatter off:\n%s", body)
}
if strings.Contains(body, "Bash(git commit") || strings.Contains(body, "Bash(git push") {
t.Error("emitted allowlist contains a write-git verb")
}
if _, err := os.Stat(ledger); err != nil {
t.Fatalf("ledger not written: %v", err)
}
// re-generate is byte-idempotent (no new backup)
if code := runCockpit([]string{"generate"}, &bytes.Buffer{}, &errOut); code != 0 {
t.Fatalf("re-generate exit=%d stderr=%s", code, errOut.String())
}
b2, _ := os.ReadFile(skill)
if string(b2) != body {
t.Error("SKILL.md changed on a no-op re-generate")
}
backups, _ := os.ReadDir(wsPath(root, "state", "backups"))
if len(backups) != 0 {
t.Errorf("re-generate created %d backup(s), want 0", len(backups))
}
// verify clean
if code := runCockpit([]string{"verify"}, &bytes.Buffer{}, &errOut); code != 0 {
t.Fatalf("verify exit=%d stderr=%s", code, errOut.String())
}
// hand-edit → verify drifts (exit 1)
if err := os.WriteFile(skill, []byte("edited\n"), 0o644); err != nil {
t.Fatal(err)
}
if code := runCockpit([]string{"verify"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 1 {
t.Errorf("verify on a drifted artifact exit=%d, want 1", code)
}
// off leaves the edited file (sha mismatch)
if code := runCockpit([]string{"off"}, &bytes.Buffer{}, &errOut); code != 0 {
t.Fatalf("off exit=%d stderr=%s", code, errOut.String())
}
if _, err := os.Stat(skill); err != nil {
t.Error("off removed a hand-edited artifact")
}
// re-generate clean, then off removes it
if err := os.Remove(skill); err != nil {
t.Fatal(err)
}
if code := runCockpit([]string{"generate"}, &bytes.Buffer{}, &errOut); code != 0 {
t.Fatalf("re-generate exit=%d stderr=%s", code, errOut.String())
}
if code := runCockpit([]string{"off"}, &bytes.Buffer{}, &errOut); code != 0 {
t.Fatalf("off exit=%d stderr=%s", code, errOut.String())
}
if _, err := os.Stat(skill); !os.IsNotExist(err) {
t.Error("clean off did not remove the artifact")
}
}
func TestRunCockpit_Status(t *testing.T) {
root := setupInited(t)
var out bytes.Buffer
if code := runCockpit([]string{"status"}, &out, &bytes.Buffer{}); code != 0 {
t.Fatalf("status exit=%d", code)
}
if !strings.Contains(out.String(), "claude/handover: not emitted") {
t.Errorf("status before generate = %q", out.String())
}
_ = root
}
func TestRunCockpit_Show(t *testing.T) {
root := setupInited(t)
var out bytes.Buffer
if code := runCockpit([]string{"show"}, &out, &bytes.Buffer{}); code != 0 {
t.Fatalf("show exit=%d", code)
}
if !strings.Contains(out.String(), "\"name\": \"handover\"") {
t.Errorf("show output missing handover JSON:\n%s", out.String())
}
_ = root
}
func TestRunCockpit_UsageErrors(t *testing.T) {
if code := runCockpit(nil, &bytes.Buffer{}, &bytes.Buffer{}); code != 2 {
t.Errorf("no-arg cockpit exit=%d, want 2", code)
}
if code := runCockpit([]string{"bogus"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 2 {
t.Errorf("unknown subcommand exit=%d, want 2", code)
}
}
func TestRunCockpit_NotInited(t *testing.T) {
dir := t.TempDir()
chdir(t, dir)
var errOut bytes.Buffer
if code := runCockpit([]string{"generate"}, &bytes.Buffer{}, &errOut); code != 1 {
t.Errorf("generate outside a repo exit=%d, want 1", code)
}
}
// TestRunCockpit_InitWritesSelection: a non-interactive init records the
// default active set (claude), and `target list` reflects it.
func TestRunCockpit_InitWritesSelection(t *testing.T) {
root := setupInited(t)
if _, err := os.Stat(wsPath(root, "cockpit.json")); err != nil {
t.Fatalf("init did not write the selection store: %v", err)
}
var out bytes.Buffer
if code := runCockpit([]string{"target", "list"}, &out, &bytes.Buffer{}); code != 0 {
t.Fatalf("target list exit=%d", code)
}
s := out.String()
if !strings.Contains(s, "active targets:") || !strings.Contains(s, "claude (enforced)") {
t.Errorf("target list missing active claude:\n%s", s)
}
if !strings.Contains(s, "agents (advisory)") {
t.Errorf("target list missing inactive advisory targets:\n%s", s)
}
}
// TestRunCockpit_TargetAddRm: add a target, see it active; rm it, gone.
// rm never deletes files (it only deselects).
func TestRunCockpit_TargetAddRm(t *testing.T) {
setupInited(t)
if code := runCockpit([]string{"target", "add", "cursor"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target add cursor failed")
}
var l1 bytes.Buffer
runCockpit([]string{"target", "list"}, &l1, &bytes.Buffer{})
if !strings.Contains(l1.String(), "cursor (advisory)") || !strings.Contains(l1.String(), "active targets:\n claude") {
t.Errorf("cursor not active after add:\n%s", l1.String())
}
if code := runCockpit([]string{"target", "rm", "cursor"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target rm cursor failed")
}
var l2 bytes.Buffer
runCockpit([]string{"target", "list"}, &l2, &bytes.Buffer{})
// cursor now appears only under the inactive list, not the active one.
if strings.Contains(l2.String(), "active targets:\n claude (enforced)\n cursor") {
t.Errorf("cursor still active after rm:\n%s", l2.String())
}
// Unknown target is rejected.
if code := runCockpit([]string{"target", "add", "bogus"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 1 {
t.Error("expected exit 1 for an unknown target add")
}
}
// TestRunCockpit_GenerateActiveSet_WithCursor: with claude+cursor active,
// generate emits the Claude SKILL.md and the Cursor .mdc for every playbook.
func TestRunCockpit_GenerateActiveSet_WithCursor(t *testing.T) {
root := setupInited(t)
if code := runCockpit([]string{"target", "add", "cursor"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target add cursor failed")
}
if code := runCockpit([]string{"generate"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("generate failed")
}
if _, err := os.Stat(cockpitSkillPath(root)); err != nil {
t.Errorf("claude SKILL.md missing: %v", err)
}
mdc := filepath.Join(root, "tester", ".cursor", "rules", "handover.mdc")
b, err := os.ReadFile(mdc)
if err != nil {
t.Fatalf("cursor .mdc missing: %v", err)
}
if !strings.Contains(string(b), "ADVISORY ONLY") {
t.Error("cursor .mdc missing the ADVISORY banner")
}
// Other playbooks emitted too (active-set generate is all-playbooks).
if _, err := os.Stat(filepath.Join(root, "tester", ".cursor", "rules", "commit.mdc")); err != nil {
t.Errorf("commit .mdc missing: %v", err)
}
}
// TestRunCockpit_AggregateAdvisory: an aggregate target emits one shared
// advisory file; off removes only it and the private tree survives.
func TestRunCockpit_AggregateAdvisory(t *testing.T) {
root := setupInited(t)
if code := runCockpit([]string{"target", "add", "agents"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target add agents failed")
}
if code := runCockpit([]string{"generate", "--target", "agents"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("generate --target agents failed")
}
agents := filepath.Join(root, "tester", "AGENTS.md")
b, err := os.ReadFile(agents)
if err != nil {
t.Fatalf("AGENTS.md missing: %v", err)
}
if !strings.Contains(string(b), "ADVISORY ONLY") || !strings.Contains(string(b), "## Fidelity report") {
t.Error("AGENTS.md missing advisory banner / fidelity report")
}
if code := runCockpit([]string{"verify", "--target", "agents"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Error("verify --target agents should be clean")
}
if code := runCockpit([]string{"off", "--target", "agents"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("off --target agents failed")
}
if _, err := os.Stat(agents); !os.IsNotExist(err) {
t.Error("AGENTS.md should be removed")
}
if _, err := os.Stat(filepath.Join(root, "tester")); err != nil {
t.Error("private tree must survive aggregate off")
}
}
// TestRunCockpit_UnknownTargetFlag: an unknown --target is exit 1.
func TestRunCockpit_UnknownTargetFlag(t *testing.T) {
setupInited(t)
if code := runCockpit([]string{"generate", "--target", "bogus"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 1 {
t.Error("expected exit 1 for an unknown --target")
}
}
// TestRunCockpit_VerifyNoFlagCleanUnused: no-flag verify on a cockpit that
// was never generated is a silent clean (empty-ledger gate), exit 0.
func TestRunCockpit_VerifyNoFlagCleanUnused(t *testing.T) {
setupInited(t)
var out bytes.Buffer
if code := runCockpit([]string{"verify"}, &out, &bytes.Buffer{}); code != 0 {
t.Errorf("no-flag verify on an unused cockpit = %d, want 0", code)
}
if !strings.Contains(out.String(), "clean") {
t.Errorf("verify clean line missing: %s", out.String())
}
}
// TestRunCockpit_VerifyNoFlagMissing: activating a target without generating
// it makes the no-flag verify report it missing (exit 1).
func TestRunCockpit_VerifyNoFlagMissing(t *testing.T) {
setupInited(t)
if code := runCockpit([]string{"generate"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("generate failed")
}
if code := runCockpit([]string{"target", "add", "cursor"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target add cursor failed")
}
var errOut bytes.Buffer
if code := runCockpit([]string{"verify"}, &bytes.Buffer{}, &errOut); code != 1 {
t.Errorf("no-flag verify after target add = %d, want 1", code)
}
if !strings.Contains(errOut.String(), "not emitted") {
t.Errorf("verify did not report missing: %s", errOut.String())
}
}
// TestRunCockpit_VerifyNoFlagOrphan: generating then deselecting a target
// leaves an orphan the no-flag verify reports exactly once (dedup by
// target), exit 1.
func TestRunCockpit_VerifyNoFlagOrphan(t *testing.T) {
setupInited(t)
if code := runCockpit([]string{"target", "add", "cursor"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target add cursor failed")
}
if code := runCockpit([]string{"generate"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("generate failed")
}
if code := runCockpit([]string{"target", "rm", "cursor"}, &bytes.Buffer{}, &bytes.Buffer{}); code != 0 {
t.Fatal("target rm cursor failed")
}
var errOut bytes.Buffer
if code := runCockpit([]string{"verify"}, &bytes.Buffer{}, &errOut); code != 1 {
t.Errorf("no-flag verify with an orphan = %d, want 1", code)
}
out := errOut.String()
if n := strings.Count(out, "deselected but artifact remains"); n != 1 {
t.Errorf("orphan reported %d time(s), want exactly 1 (dedup by target):\n%s", n, out)
}
}