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