package main import ( "os" "path/filepath" "regexp" "strings" "sync" "testing" "github.com/novox/mesh-controller/internal/inventory" ) // **Every plan is written through SavePlansMoving, which says it** (issue 974, novox/hq ADR 0406 §2: every move of a plan // is said as plan-moved, so no move is found only by comparing). The one hook, inventory.PlanSaved, is called there after the // write; so no code but chainSavePlans (which SavePlansMoving calls, and no other function) writes a plan to the rollouts // bucket by anything but a change of the merges it holds. Checked by reading the sources, no store needed. func TestNoPlanIsWrittenButThroughSavePlansMoving(t *testing.T) { write := regexp.MustCompile(`(?is)(insert\s+into|update|delete\s+from|truncate)\s+(table\s+)?release_plan\b`) var found []string for _, root := range []string{"../../cmd", "../../internal"} { err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { return nil } raw, err := os.ReadFile(path) if err != nil { return err } text := string(raw) if write.MatchString(text) { found = append(found, path+" writes the plan table") } // A plan record is written by the bucket's raw create and update, which only the plans' own file calls. if filepath.ToSlash(path) != "../../internal/inventory/chain_rollouts.go" && filepath.ToSlash(path) != "../../internal/chain/chain.go" && strings.Contains(text, "chain.RolloutsOf") { found = append(found, path+" reaches the rollouts bucket") } return nil }) if err != nil { t.Fatal(err) } } if len(found) > 0 { t.Fatalf("a plan is written without plan-moved being said:\n%s", strings.Join(found, "\n")) } raw, err := os.ReadFile("../../internal/inventory/plans.go") if err != nil { t.Fatal(err) } text := string(raw) save := strings.Index(text, "func (i *Inventory) SavePlansMoving") next := strings.Index(text[save:], "\nfunc ") if next < 0 { next = len(text) - save } if !strings.Contains(text[save:save+next], "chainSavePlans(") { t.Error("SavePlansMoving does not write through chainSavePlans") } chainRaw, err := os.ReadFile("../../internal/inventory/chain_rollouts.go") if err != nil { t.Fatal(err) } chainText := string(chainRaw) at := strings.Index(chainText, "func (i *Inventory) chainSavePlans") end := strings.Index(chainText[at:], "\nfunc ") if !strings.Contains(chainText[at:at+end], "PlanSaved(i, *p)") { t.Error("chainSavePlans does not say each plan it wrote") } if n := strings.Count(chainText, "createRaw(ctx, c, p.ID"); n != 1 { t.Errorf("a plan is made from %d places of chain_rollouts.go, want the one in chainSavePlans", n) } } // movesSaid records what the hook hears: for each plan, the revisions it was said at. type movesSaid struct { mu sync.Mutex revs map[string][]int64 } func (s *movesSaid) hear(_ *inventory.Inventory, p inventory.Plan) { s.mu.Lock() defer s.mu.Unlock() s.revs[p.ID] = append(s.revs[p.ID], p.Revision) } // sayingEverySave installs a PlanSaved that records, as the serving controller's says plan-moved. func sayingEverySave(t *testing.T) *movesSaid { t.Helper() s := &movesSaid{revs: map[string][]int64{}} was := inventory.PlanSaved inventory.PlanSaved = s.hear t.Cleanup(func() { inventory.PlanSaved = was }) return s } // noUnsaid fails the test for a plan whose stored revision is not one the hook heard: a move nobody said. func (s *movesSaid) noUnsaid(t *testing.T, open *stores) { t.Helper() recent, err := open.inventory.RecentPlans(t.Context(), 50) if err != nil { t.Fatal(err) } s.mu.Lock() defer s.mu.Unlock() if len(recent) == 0 { t.Fatal("no plan was kept") } for _, p := range recent { revs := s.revs[p.ID] if len(revs) == 0 || revs[len(revs)-1] != p.Revision { t.Errorf("%s is kept at revision %d and was said at %v: a move nobody said", p.ID, p.Revision, revs) } } } // Live, with the store: the moves a merge makes of a plan — the batch it opens, the walk it is cut into, the tier that // is asked — are each said, and a look that changes nothing says nothing (novox/hq ADR 0406 §2).