ajhahn.de
← eeco
Go 163 lines
package cockpit

import (
	"os"
	"path/filepath"
	"testing"
)

func TestGenerateOff_Reversible(t *testing.T) {
	cfg := testConfig(t)
	pb := loadHandover(t)
	dst := filepath.Join(cfg.UserDir, ".claude", "skills", "handover", "SKILL.md")
	leaf := filepath.Dir(dst)

	res, err := Generate(cfg, pb, "claude")
	if err != nil {
		t.Fatalf("Generate: %v", err)
	}
	if res.Action != "generated" {
		t.Errorf("first generate action = %q, want generated", res.Action)
	}
	if _, err := os.Stat(dst); err != nil {
		t.Fatalf("SKILL.md not written: %v", err)
	}
	if _, err := os.Stat(ledgerPath(cfg)); err != nil {
		t.Fatalf("ledger not written: %v", err)
	}

	off, err := Off(cfg, pb, "claude")
	if err != nil {
		t.Fatalf("Off: %v", err)
	}
	if !off.Changed {
		t.Error("Off reported no change for an installed artifact")
	}
	if _, err := os.Stat(dst); !os.IsNotExist(err) {
		t.Error("SKILL.md still present after off")
	}
	if _, err := os.Stat(leaf); !os.IsNotExist(err) {
		t.Error("leaf skill dir not pruned after off")
	}
	// Record cleared.
	l, _ := loadLedger(cfg)
	if l.find("claude", "handover") >= 0 {
		t.Error("ledger record not cleared after off")
	}
}

func TestVerify_DriftAndOffLeavesEdited(t *testing.T) {
	cfg := testConfig(t)
	pb := loadHandover(t)
	dst := filepath.Join(cfg.UserDir, ".claude", "skills", "handover", "SKILL.md")

	if _, err := Generate(cfg, pb, "claude"); err != nil {
		t.Fatalf("Generate: %v", err)
	}

	// Clean verify.
	vr, err := Verify(cfg, pb, "claude", "")
	if err != nil {
		t.Fatalf("Verify: %v", err)
	}
	if !vr.Clean {
		t.Errorf("verify not clean on a fresh emit: %s", vr.Detail)
	}

	// Hand-edit → drift.
	if err := os.WriteFile(dst, []byte("hand edited\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	vr, err = Verify(cfg, pb, "claude", "")
	if err != nil {
		t.Fatalf("Verify after edit: %v", err)
	}
	if vr.Clean {
		t.Error("verify reported clean on a hand-edited artifact")
	}

	// Off leaves the edited file untouched.
	off, err := Off(cfg, pb, "claude")
	if err != nil {
		t.Fatalf("Off after edit: %v", err)
	}
	if off.Changed {
		t.Error("Off removed a hand-edited artifact")
	}
	b, err := os.ReadFile(dst)
	if err != nil || string(b) != "hand edited\n" {
		t.Errorf("hand-edited file not preserved by off: %q (%v)", string(b), err)
	}
}

func TestGenerate_Idempotent(t *testing.T) {
	cfg := testConfig(t)
	pb := loadHandover(t)
	dst := filepath.Join(cfg.UserDir, ".claude", "skills", "handover", "SKILL.md")

	if _, err := Generate(cfg, pb, "claude"); err != nil {
		t.Fatalf("first Generate: %v", err)
	}
	file1, _ := os.ReadFile(dst)
	ledger1, _ := os.ReadFile(ledgerPath(cfg))

	res, err := Generate(cfg, pb, "claude")
	if err != nil {
		t.Fatalf("second Generate: %v", err)
	}
	if res.Action != "already current" {
		t.Errorf("second generate action = %q, want already current", res.Action)
	}
	file2, _ := os.ReadFile(dst)
	ledger2, _ := os.ReadFile(ledgerPath(cfg))
	if string(file1) != string(file2) {
		t.Error("SKILL.md changed on a no-op re-generate")
	}
	if string(ledger1) != string(ledger2) {
		t.Error("ledger changed on a no-op re-generate")
	}
	// No backup churn: state/backups should be empty/absent.
	backups, _ := os.ReadDir(filepath.Join(cfg.Workspace, "state", "backups"))
	if len(backups) != 0 {
		t.Errorf("re-generate created %d backup(s), want 0", len(backups))
	}
}

func TestLedger_RoundTrip(t *testing.T) {
	cfg := testConfig(t)
	l := ledger{Records: []record{{
		Installed: true, Target: "claude", Playbook: "handover",
		Path: "/x/SKILL.md", SHA256: "abc", Created: true, At: "2026-06-05T00:00:00Z",
	}}}
	if err := saveLedger(cfg, l); err != nil {
		t.Fatal(err)
	}
	b1, _ := os.ReadFile(ledgerPath(cfg))
	got, err := loadLedger(cfg)
	if err != nil {
		t.Fatal(err)
	}
	if err := saveLedger(cfg, got); err != nil {
		t.Fatal(err)
	}
	b2, _ := os.ReadFile(ledgerPath(cfg))
	if string(b1) != string(b2) {
		t.Error("ledger save→load→save is not byte-identical")
	}
}

func TestStatus_Transitions(t *testing.T) {
	cfg := testConfig(t)
	pb := loadHandover(t)

	if got := Status(cfg)[0]; got != "claude/handover: not emitted" {
		t.Errorf("status before generate = %q", got)
	}
	if _, err := Generate(cfg, pb, "claude"); err != nil {
		t.Fatal(err)
	}
	if got := Status(cfg)[0]; got != "claude/handover: on" {
		t.Errorf("status after generate = %q", got)
	}
}