package cockpit import ( "bytes" "os" "path/filepath" "regexp" "strings" "testing" ) // syncSet is a small, stable playbook set for Sync tests: the real handover // source plus a synthetic one, so multi-playbook behavior is observable. func syncSet(t *testing.T) []Playbook { t.Helper() return []Playbook{loadHandover(t), synthPlaybook("zeta")} } func findKind(rep SyncReport, kind string) *SyncFinding { for i := range rep.Findings { if rep.Findings[i].Kind == kind { return &rep.Findings[i] } } return nil } // TestSync_EmptyLedgerClean: a cockpit that was never generated (no ledger, // only a default selection) returns a silent clean — the load-bearing // empty-ledger gate that keeps the post-merge builtin a no-op. func TestSync_EmptyLedgerClean(t *testing.T) { cfg := testConfig(t) rep, err := Sync(cfg, syncSet(t)) if err != nil { t.Fatal(err) } if !rep.Clean || len(rep.Findings) != 0 { t.Fatalf("empty-ledger Sync = %+v, want clean with no findings", rep) } } // TestSync_DriftedAfterHandEdit: a hand-edit to an emitted artifact is // reported as a drifted finding. func TestSync_DriftedAfterHandEdit(t *testing.T) { cfg := testConfig(t) set := syncSet(t) if err := SaveSelection(cfg, Selection{Targets: []string{"claude"}}); err != nil { t.Fatal(err) } for _, pb := range set { if _, err := Generate(cfg, pb, "claude"); err != nil { t.Fatalf("generate %s: %v", pb.Name, err) } } dst := filepath.Join(cfg.UserDir, claudeRenderer{}.RelPath(set[0])) if err := os.WriteFile(dst, []byte("edited\n"), 0o644); err != nil { t.Fatal(err) } rep, err := Sync(cfg, set) if err != nil { t.Fatal(err) } if rep.Clean { t.Fatal("Sync reported clean over a hand-edited artifact") } if findKind(rep, "drifted") == nil { t.Fatalf("no drifted finding: %+v", rep.Findings) } } // TestSync_MissingAfterTargetAdd: activating a target without generating it // surfaces a missing finding for that target. func TestSync_MissingAfterTargetAdd(t *testing.T) { cfg := testConfig(t) set := syncSet(t) for _, pb := range set { if _, err := Generate(cfg, pb, "claude"); err != nil { t.Fatal(err) } } if err := SaveSelection(cfg, Selection{Targets: []string{"claude", "cursor"}}); err != nil { t.Fatal(err) } rep, err := Sync(cfg, set) if err != nil { t.Fatal(err) } if rep.Clean { t.Fatal("Sync clean despite an un-emitted active target") } missing := false for _, f := range rep.Findings { if f.Target == "cursor" && f.Kind == "missing" { missing = true } } if !missing { t.Fatalf("no cursor missing finding: %+v", rep.Findings) } } // TestSync_OrphanDedupByTarget: a deselected per-playbook target with K // ledger records collapses to exactly one orphan finding. func TestSync_OrphanDedupByTarget(t *testing.T) { cfg := testConfig(t) set := syncSet(t) if err := SaveSelection(cfg, Selection{Targets: []string{"cursor"}}); err != nil { t.Fatal(err) } for _, pb := range set { if _, err := Generate(cfg, pb, "cursor"); err != nil { t.Fatalf("generate cursor/%s: %v", pb.Name, err) } } // Deselect cursor; its K artifacts are now orphaned. if err := SaveSelection(cfg, Selection{Targets: []string{"claude"}}); err != nil { t.Fatal(err) } rep, err := Sync(cfg, set) if err != nil { t.Fatal(err) } orphans := 0 for _, f := range rep.Findings { if f.Kind == "orphan" { orphans++ if f.Target != "cursor" { t.Errorf("orphan finding target = %q, want cursor", f.Target) } } } if orphans != 1 { t.Fatalf("orphan findings = %d, want exactly 1 (dedup by target); findings = %+v", orphans, rep.Findings) } } var rfc3339Date = regexp.MustCompile(`\d{4}-\d{2}-\d{2}T\d{2}:`) // TestRenderersDeterministic_NoHostStrings is the derive-at-fire guard: // every renderer produces byte-identical output on a repeat render and bakes // no host-specific or volatile value into the artifact (which would make // verify drift forever). func TestRenderersDeterministic_NoHostStrings(t *testing.T) { set := []Playbook{synthPlaybook("alpha"), synthPlaybook("beta")} for _, target := range Targets() { r, ok := rendererFor(target) if !ok { t.Fatalf("no renderer for %q", target) } var out []byte if agg, isAgg := isAggregate(r); isAgg { b1, err := agg.RenderAll(set) if err != nil { t.Fatalf("%s RenderAll: %v", target, err) } b2, err := agg.RenderAll(set) if err != nil { t.Fatalf("%s RenderAll: %v", target, err) } if !bytes.Equal(b1, b2) { t.Errorf("%s RenderAll is not deterministic", target) } out = b1 } else { b1, err := r.Render(set[0]) if err != nil { t.Fatalf("%s Render: %v", target, err) } b2, err := r.Render(set[0]) if err != nil { t.Fatalf("%s Render: %v", target, err) } if !bytes.Equal(b1, b2) { t.Errorf("%s Render is not deterministic", target) } out = b1 } s := string(out) for _, bad := range []string{"/Users/", "/home/", "$USER"} { if strings.Contains(s, bad) { t.Errorf("%s output contains host string %q (derive-at-fire violated)", target, bad) } } if rfc3339Date.MatchString(s) { t.Errorf("%s output contains a baked timestamp (derive-at-fire violated)", target) } } }