The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold, no summary. A settings proposal was the same wall of text, its values masked so the approval was blind. - messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each answer does, the expiry, where else it is answered and the asker's Details travel as the message's note (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram · <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone (from the earlier work on issue 383, #178). - telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message shown before is still compared by the digest it was kept under. - desk-channel: the note follows the body. - mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is composed again as it was asked (form), so its warrant still holds. - claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the module's), its text in the Details; a question's context is at most 300 characters and its recommendation one point. Older proposals and questions keep their form. The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and asks.Glance.
225 lines
8.8 KiB
Go
225 lines
8.8 KiB
Go
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"})
|
|
}},
|
|
{"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
|
|
}},
|
|
}
|
|
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) != 30 {
|
|
t.Errorf("%d per-record rules; ADR 0290 counts 29, and issue 639 adds the brief", 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")
|
|
got, err := w.k.Propose("author@test", valid(n))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e := mustGet(t, s, Key{KindDecision, n})
|
|
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)
|
|
}
|
|
}
|