package client import "testing" const ( hashV1 = "sha256:aaa" hashV2 = "sha256:bbb" hashMod = "sha256:localmod" ) func testPathFor(typ, name string) string { return "/home/u/target/" + typ + "/" + name } func manifestWith(entries map[string]*ManifestItem) *Manifest { m := NewManifest() for k, v := range entries { m.Items[k] = v } return m } // TestPlanMatrix walks the complete three-state matrix (disk × upstream) // plus the missing-disk restore cases, asserting the single planned action. func TestPlanMatrix(t *testing.T) { type disk string const ( diskNone disk = "missing" diskClean disk = "unmodified" diskMod disk = "user-modified" diskForeign disk = "never-managed" ) type upstream string const ( upAdded upstream = "added" upChanged upstream = "changed" upSame upstream = "unchanged" upGone upstream = "removed" upAbsent upstream = "not-in-repo" ) cases := []struct { disk disk up upstream want Action }{ {diskNone, upAdded, ActDeploy}, {diskClean, upAdded, ActSkip}, {diskMod, upAdded, ActSkip}, {diskForeign, upAdded, ActSkip}, {diskNone, upChanged, ActUpdate}, {diskClean, upChanged, ActUpdate}, {diskMod, upChanged, ActSkip}, {diskForeign, upChanged, ActSkip}, {diskNone, upSame, ActUpdate}, {diskClean, upSame, ActKeep}, {diskMod, upSame, ActSkip}, {diskForeign, upSame, ActSkip}, {diskNone, upGone, ActRemove}, {diskClean, upGone, ActRemove}, {diskMod, upGone, ActSkip}, {diskForeign, upGone, ActSkip}, {diskClean, upAbsent, ActLeave}, {diskNone, upAbsent, ActKeep}, // marker for "no plan entry at all" } for _, tc := range cases { name := string(tc.disk) + "/" + string(tc.up) t.Run(name, func(t *testing.T) { key := manifestKey("skills", "alpha") var items []PlannedItem manifest := NewManifest() diskFacts := map[string]DiskFact{} switch tc.up { case upAdded: items = append(items, PlannedItem{Type: "skills", Name: "alpha", RelPath: "skills/alpha", Hash: hashV1}) case upChanged, upSame: hash := hashV2 if tc.up == upSame { hash = hashV1 } items = append(items, PlannedItem{Type: "skills", Name: "alpha", RelPath: "skills/alpha", Hash: hash}) manifest.Items[key] = &ManifestItem{Name: "alpha", Type: "skills", Hash: hashV1} case upGone: manifest.Items[key] = &ManifestItem{Name: "alpha", Type: "skills", Hash: hashV1} case upAbsent: // neither inventory nor manifest } switch tc.disk { case diskNone: case diskClean: diskFacts[key] = DiskFact{Exists: true, Hash: hashV1} case diskMod, diskForeign: diskFacts[key] = DiskFact{Exists: true, Hash: hashMod} } plan := Plan(items, manifest, diskFacts, testPathFor) if tc.disk == diskNone && tc.up == upAbsent { if len(plan) != 0 { t.Fatalf("a key present nowhere must produce no entry, got %+v", plan) } return } if len(plan) != 1 { t.Fatalf("want exactly 1 entry, got %d: %+v", len(plan), plan) } if plan[0].Action != tc.want { t.Errorf("action = %s, want %s (reason: %s)", plan[0].Action, tc.want, plan[0].Reason) } if plan[0].DstPath != testPathFor("skills", "alpha") { t.Errorf("DstPath not mapped: %q", plan[0].DstPath) } if tc.want == ActSkip || tc.want == ActLeave { if plan[0].Reason == "" { t.Error("skip/leave entries must carry a reason") } } }) } } // TestPlanMixedFixture combines several states in one plan and asserts the // full deterministic output. func TestPlanMixedFixture(t *testing.T) { items := []PlannedItem{ {Type: "commands", Name: "deploy", RelPath: "commands/deploy.md", Hash: hashV1}, {Type: "skills", Name: "fresh", RelPath: "skills/fresh", Hash: hashV1}, {Type: "skills", Name: "tweaked", RelPath: "skills/tweaked", Hash: hashV2}, {Type: "skills", Name: "keeper", RelPath: "skills/keeper", Hash: hashV1}, {Type: "agents", Name: "helper", RelPath: "agents/helper.md", Hash: hashV1}, } manifest := manifestWith(map[string]*ManifestItem{ "skills/tweaked": {Name: "tweaked", Type: "skills", Hash: hashV1}, "skills/goner": {Name: "goner", Type: "skills", Hash: hashV1}, "skills/keeper": {Name: "keeper", Type: "skills", Hash: hashV1}, "skills/blocked": {Name: "blocked", Type: "skills", Hash: hashV1}, "agents/helper": {Name: "helper", Type: "agents", Hash: hashV1}, }) disk := map[string]DiskFact{ "skills/fresh": {Exists: true, Hash: hashMod}, // never-managed × added "skills/tweaked": {Exists: true, Hash: hashMod}, // user-modified × changed "skills/goner": {Exists: true, Hash: hashV1}, // unmodified × removed "skills/keeper": {Exists: true, Hash: hashV1}, // unmodified × unchanged "skills/blocked": {Exists: true, Hash: hashMod}, // user-modified × removed "skills/mine": {Exists: true}, // never-managed × absent "commands/deploy": {}, // missing × added "agents/helper": {Exists: true, Hash: hashV1}, // unmodified × unchanged } plan := Plan(items, manifest, disk, testPathFor) got := map[string]Action{} for _, e := range plan { got[e.Type+"/"+e.Name] = e.Action } want := map[string]Action{ "commands/deploy": ActDeploy, "skills/fresh": ActSkip, "skills/tweaked": ActSkip, "agents/helper": ActKeep, "skills/goner": ActRemove, "skills/keeper": ActKeep, "skills/blocked": ActSkip, "skills/mine": ActLeave, } if len(got) != len(want) { t.Fatalf("plan has %d entries, want %d: %+v", len(got), len(want), plan) } for key, action := range want { if got[key] != action { t.Errorf("%s: action = %s, want %s", key, got[key], action) } } sorted := true for i := 1; i < len(plan); i++ { prev := plan[i-1].Type + "/" + plan[i-1].Name curr := plan[i].Type + "/" + plan[i].Name if prev > curr { sorted = false } } if !sorted { t.Error("plan must be deterministically sorted by type/name") } } // TestPlanPureGuarantee compiles the purity contract: planning identical // inputs twice yields identical plans, and no input is mutated. func TestPlanPureGuarantee(t *testing.T) { items := []PlannedItem{{Type: "skills", Name: "a", RelPath: "skills/a", Hash: hashV1}} manifest := manifestWith(map[string]*ManifestItem{"skills/b": {Name: "b", Type: "skills", Hash: hashV1}}) disk := map[string]DiskFact{"skills/b": {Exists: true, Hash: hashV1}} p1 := Plan(items, manifest, disk, testPathFor) p2 := Plan(items, manifest, disk, testPathFor) if len(p1) != len(p2) { t.Fatal("same inputs must yield same plan length") } for i := range p1 { if p1[i] != p2[i] { t.Fatalf("plan not deterministic at %d: %+v vs %+v", i, p1[i], p2[i]) } } if manifest.Items["skills/b"] == nil || len(items) != 1 { t.Error("planner must not mutate its inputs") } }