package main import ( "context" "fmt" "strings" "testing" ) // to-be 55 §10: "A proposal that breaks a per-record rule is refused, naming every reason — the module's table // test, one case per rule of §4." Each case takes a proposal every rule takes and breaks one rule; the proposal is // refused, the refusal names that rule, and nothing is written. A rule with no case fails the test. func TestEveryPerRecordRuleRefusesAProposal(t *testing.T) { cases := []struct { rule string break_ func(p *ProposeAsk, w *world, t *testing.T) }{ {"kind", func(p *ProposeAsk, w *world, t *testing.T) { // A kind the verb refuses before the rules: judged by the rules when a record of another kind is held. p.Kind = "design" }}, {"reserved", func(p *ProposeAsk, w *world, t *testing.T) { p.Number = 9999 }}, {"title", func(p *ProposeAsk, w *world, t *testing.T) { p.Title = "two\nlines" }}, {"topic", func(p *ProposeAsk, w *world, t *testing.T) { p.Topic = "gardening" }}, {"date", func(p *ProposeAsk, w *world, t *testing.T) { p.Date = "11 October" }}, {"deciders", func(p *ProposeAsk, w *world, t *testing.T) { p.Deciders = nil }}, {"research-status", func(p *ProposeAsk, w *world, t *testing.T) { p.Kind, p.Number = KindResearch, w.reservedResearch(t) p.Status, p.Links = "wondering", nil p.Sections = []Section{{Name: "00-overview.md", Text: "An effort."}} p.Clauses = nil }}, {"has-clause", func(p *ProposeAsk, w *world, t *testing.T) { p.Clauses = nil; p.Links = p.Links[:1] }}, {"clause-numbering", func(p *ProposeAsk, w *world, t *testing.T) { p.Clauses = []string{"1. **One.** A.", "3. **Three.** C."} }}, {"clause-lead", func(p *ProposeAsk, w *world, t *testing.T) { p.Clauses[1] = "A batch is delivered by one walk." }}, {"clause-size", func(p *ProposeAsk, w *world, t *testing.T) { p.Clauses[1] = "**Long.** " + strings.Repeat("word ", 500) }}, {"context", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections = p.Sections[1:] }}, {"consequences", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections = append(p.Sections[:2], p.Sections[3]) }}, {"checked", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections = p.Sections[:3] }}, {"overview", func(p *ProposeAsk, w *world, t *testing.T) { p.Kind, p.Number, p.Status, p.Clauses, p.Links = KindResearch, w.reservedResearch(t), StatusActive, nil, nil p.Sections = []Section{{Name: "01-findings.md", Text: "Found."}} }}, {"link-kind", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: "admires", To: "0276"}) }}, {"link-target", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkCites, To: "0007"}) }}, {"link-fits", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkBecame, To: "0276"}) }}, {"link-fits", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkBuiltIn, To: "0276"}) // built-in names a milestone, not a record }}, {"self-link", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkCites, To: itoa4(p.Number)}) }}, {"rests-on", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkExtends, To: "0127"}) }}, {"supersedes-accepted", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkSupersedes, To: "0127"}) }}, {"changes-in-force", func(p *ProposeAsk, w *world, t *testing.T) { // 0131 superseded 0127, so 0127's clauses are superseded; a changes link to one names a clause no longer in force. w.supersededClause(t) p.Links = append(p.Links, LinkAsk{Kind: LinkChanges, To: "0011 §1", From: 2}) }}, {"live-citation", func(p *ProposeAsk, w *world, t *testing.T) { p.Links = append(p.Links, LinkAsk{Kind: LinkCites, To: "0127"}) }}, {"became", func(p *ProposeAsk, w *world, t *testing.T) { p.Kind, p.Number, p.Status, p.Clauses, p.Links = KindResearch, w.reservedResearch(t), StatusGraduated, nil, nil p.Sections = []Section{{Name: "00-overview.md", Text: "An effort that graduated."}} }}, {"text-links", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections[0].Text += " See [ADR 0007](0007-a-record-nobody-holds.md)." }}, {"retired-word", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections[2].Text = "The control plane is quicker." }}, {"daemon-line", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections[3].Text = "Run `mesh-controller doctor` after the merge." }}, {"deferral", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections[2].Text = "The bound itself is left for later." }}, {"insight-form", func(p *ProposeAsk, w *world, t *testing.T) { p.Sections[2].Text = "A progressive insight corrected the count." }}, {"brief", func(p *ProposeAsk, w *world, t *testing.T) { p.Date, p.NeedsOperator = "2026-10-13", true }}, {"section-form", func(p *ProposeAsk, w *world, t *testing.T) { p.Date, p.Sections[0].Name = "2026-11-04", "00-context.md" }}, } covered := map[string]bool{} for _, c := range cases { covered[c.rule] = true } for _, r := range PerRecordRules { if !covered[r.ID] { t.Errorf("the rule %s has no case", r.ID) } } if len(PerRecordRules) != 31 { t.Errorf("%d per-record rules; ADR 0290 counts 29, issue 639 adds the brief and to-be 55 §2.2 the form of a section", len(PerRecordRules)) } for _, c := range cases { t.Run(c.rule, func(t *testing.T) { w := imported(t, newMemStore()) n := w.reserved(t, "author@test") p := valid(n) c.break_(&p, w, t) if c.rule == "kind" { // The verb refuses a kind it does not hold before the rules; the rule itself is held by JudgeRecord. ref, _ := w.k.reference() r := &Record{Kind: "design", Number: n, Title: "x", Date: "2026-10-11"} if !names(JudgeRecord(r, mustGet(t, w.k.Store, Key{KindDecision, n}), w.k.lookup(context.Background()), ref), "kind") { t.Fatal("a record of an unknown kind was not refused by the kind rule") } if _, err := w.k.Propose("author@test", p); err == nil { t.Fatal("propose took a record of an unknown kind") } return } before := versionsOf(t, w, Key{p.Kind, p.Number}) _, err := w.k.Propose("author@test", p) if err == nil { t.Fatalf("a proposal breaking %s was taken", c.rule) } if !strings.Contains(err.Error(), "- "+c.rule+":") { t.Fatalf("the refusal does not name %s: %v", c.rule, err) } if after := versionsOf(t, w, Key{p.Kind, p.Number}); after != before { t.Fatalf("a refused proposal wrote a version (%d, was %d)", after, before) } }) } } // One proposal breaking three rules is refused naming all three, at once. func TestARefusalNamesEveryReasonAtOnce(t *testing.T) { w := imported(t, newMemStore()) p := valid(w.reserved(t, "author@test")) p.Topic = "gardening" p.Clauses[1] = "no lead here." p.Sections[2].Text = "The control plane is quicker." _, err := w.k.Propose("author@test", p) if err == nil { t.Fatal("taken") } for _, rule := range []string{"topic", "clause-lead", "retired-word"} { if !strings.Contains(err.Error(), "- "+rule+":") { t.Errorf("the refusal does not name %s: %v", rule, err) } } } // The proposal every case breaks is taken as it is: each case fails for its own rule, not for the base's. func TestTheValidProposalIsTaken(t *testing.T) { eachStore(t, func(t *testing.T, s Store) { w := imported(t, s) n := w.reserved(t, "author@test") p := valid(n) p.Links = append(p.Links, LinkAsk{Kind: LinkBuiltIn, To: "milestone 47/G"}) // a decision links its milestone got, err := w.k.Propose("author@test", p) if err != nil { t.Fatal(err) } e := mustGet(t, s, Key{KindDecision, n}) if l := e.Current.Links[len(e.Current.Links)-1]; l.Kind != LinkBuiltIn || l.To != (Ref{Kind: TargetMilestone, Path: "47/G"}) { t.Fatalf("the milestone link was kept as %+v", l) } if got.Status != StatusProposed || e.Current.Status != StatusProposed || len(e.Current.Clauses) != 2 || e.Current.Clauses[0].Status != ClauseProposed || e.Current.Author != "author@test" { t.Fatalf("%+v / %+v", got, e.Current) } if len(w.said.of(EventProposed)) != 1 || len(w.tracker.asked) != 1 || w.tracker.asked[0]["kind"] != KindDecision { t.Fatalf("events %v, tracker %v", w.said.events, w.tracker.asked) } }) } func names(refusals []Refusal, rule string) bool { for _, r := range refusals { if r.Rule == rule { return true } } return false } func versionsOf(t *testing.T, w *world, k Key) int { t.Helper() e, err := w.k.Store.Get(context.Background(), k) if err != nil { t.Fatal(err) } if e == nil { return 0 } return e.Versions } func itoa4(n int) string { return fmt.Sprintf("%04d", n) } func (w *world) reservedResearch(t *testing.T) int { t.Helper() r, err := w.k.ReserveNumber("author@test", KindResearch, "a test effort") if err != nil { t.Fatal(err) } return r.Number } // supersededClause gives 0011, accepted, one clause a later record superseded, so a change to that clause names // one no longer in force. func (w *world) supersededClause(t *testing.T) { t.Helper() ctx := context.Background() e := mustGet(t, w.k.Store, Key{KindDecision, 11}) r := e.Current.Clone() r.Clauses = []Clause{{N: 1, Text: "**Generated.** Managed files are generated.", Status: ClauseSuperseded, Source: SourceWritten, ChangedBy: []string{"ADR 0012"}}} if err := w.k.Store.Save(ctx, Version{By: "test", Why: "superseded for the test", Record: r}, true, false, e.Revision); err != nil { t.Fatal(err) } }