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