Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/close_test.go
T
jschoubben 46505d42d8
mesh/delivery merged: source.main moved to novox/mesh-catalog@8acad77d33f9
mesh/check being carried to 46505d42d8fb on 8acad77d33f9 | from d2f326dff40e
A graduated closing is judged on every became link the effort carries, carried ones too, each with its record's title and status (issue 784, ADR 0630 §2)
Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
2026-11-04 16:00:13 +01:00

232 lines
10 KiB
Go

package main
import (
"context"
"fmt"
"strings"
"testing"
"time"
)
func closeNote(to string) string {
return "It became the knowledge base's own record; " + to + " carries it now."
}
// ADR 0630 §1, §2: a close writes one version, the served text unchanged and one section added; a retired word in
// the old text is a note, never a refusal, and the review judges the closing alone.
func TestACloseAddsOneSectionAndJudgesOnlyTheClosing(t *testing.T) {
w := imported(t, newMemStore())
w.clock = time.Date(2026, 10, 1, 9, 0, 0, 0, time.UTC)
r, _ := w.k.ReserveNumber("author@g14", KindResearch, "an old effort")
old := effortAsk(r.Number, StatusActive, Section{Name: "00-overview.md", Text: "The control plane wrote them."})
old.Date = "2026-10-01"
w.accepted(t, "author@g14", old)
w.clock = time.Date(2026, 11, 4, 9, 0, 0, 0, time.UTC)
key := Key{KindResearch, r.Number}
before := mustGet(t, w.k.Store, key).Current
out, err := w.k.Close("lead@shanks", CloseAsk{Number: r.Number, To: StatusGraduated, Note: closeNote("0290"),
Links: []LinkAsk{{Kind: LinkBecame, To: "0290"}}})
if err != nil {
t.Fatalf("a close was refused for its old text: %v", err)
}
if len(out.Notes) == 0 || !strings.Contains(strings.Join(out.Notes, "\n"), "retired-word") {
t.Errorf("the old text's finding is not listed as a note: %v", out.Notes)
}
e := mustGet(t, w.k.Store, key)
if e.Pending == nil || e.Current.Status != StatusActive {
t.Fatalf("the served version must stay until the review: pending %v, status %s", e.Pending != nil, e.Current.Status)
}
q := ""
w.judge.reviews = nil
w.reviewAll(t)
if len(w.judge.reviews) != 1 {
t.Fatalf("reviews: %d", len(w.judge.reviews))
}
q = w.judge.reviews[0]
if !strings.Contains(q, "review the closing of") || strings.Contains(q, "control plane") {
t.Errorf("the review did not judge the closing alone:\n%.500s", q)
}
after := mustGet(t, w.k.Store, key)
if after.Pending != nil || after.Current.Status != StatusGraduated || len(after.Current.Sections) != len(before.Sections)+1 {
t.Fatalf("after the review: pending %v, %s, %d sections", after.Pending != nil, after.Current.Status, len(after.Current.Sections))
}
for i, s := range before.Sections {
if after.Current.Sections[i] != s {
t.Errorf("section %d changed", i)
}
}
if last := after.Current.Sections[len(before.Sections)]; last.Name != "Graduated" || last.Text != closeNote("0290") {
t.Errorf("the added section: %+v", last)
}
}
// ADR 0630 §4: the moves it allows, a refused move naming the allowed ones.
func TestCloseAllowsOnlyItsMoves(t *testing.T) {
w := imported(t, newMemStore())
becameTo := []LinkAsk{{Kind: LinkBecame, To: "0290"}}
citesTo := []LinkAsk{{Kind: LinkCites, To: "0290"}}
for _, c := range []struct {
name string
from string
to string
links []LinkAsk
note string
wantErr string
}{
{"an idea to abandoned", StatusIdea, StatusAbandoned, citesTo, closeNote("0290"), ""},
{"an idea to graduated", StatusIdea, StatusGraduated, becameTo, closeNote("0290"), "from idea it allows abandoned"},
{"an idea to an idea", StatusIdea, StatusIdea, nil, closeNote("0290"), "from idea it allows abandoned"},
{"an active to an idea", StatusActive, StatusIdea, nil, closeNote("0290"), ""},
{"graduated needs a became", StatusActive, StatusGraduated, nil, closeNote("0290"), "became"},
{"a wrong link kind", StatusActive, StatusAbandoned, becameTo, closeNote("0290"), "cites"},
{"no note", StatusActive, StatusIdea, nil, " ", "note"},
} {
n := w.effort(t, StatusIdea)
if c.from == StatusActive {
w.accepted(t, "author@g14", effortAsk(n, StatusActive, Section{Name: "00-overview.md", Text: "Taken up."}))
}
_, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: c.to, Links: c.links, Note: c.note})
if (err == nil) != (c.wantErr == "") || (err != nil && !strings.Contains(err.Error(), c.wantErr)) {
t.Errorf("%s: %v, want %q", c.name, err, c.wantErr)
}
}
if _, err := w.k.Close("lead@shanks", CloseAsk{Number: 999999, To: StatusIdea, Note: "x"}); err == nil {
t.Error("a record it holds none of was closed")
}
}
// ADR 0630 §3: a version still waiting and the operator's ask for it end with the closing; the ask is withdrawn.
func TestACloseEndsTheWaitingVersionAndItsAsk(t *testing.T) {
w := imported(t, newMemStore())
n := w.effort(t, StatusActive)
key := Key{KindResearch, n}
next := effortAsk(n, StatusActive, Section{Name: "00-overview.md", Text: "What a roadmap entry would be."}, Section{Name: "01-findings.md", Text: "More."})
next.NeedsOperator = true
next.Brief = &Brief{Headline: "More findings", Summary: "A new finding is added. It changes nothing for the operator."}
if _, err := w.k.Propose("author@g14", next); err != nil {
t.Fatal(err)
}
w.reviewAll(t)
e := mustGet(t, w.k.Store, key)
if e.Pending == nil || e.Pending.Ask == nil || e.Pending.Ask.Said != AskWaiting {
t.Fatalf("no ask is waiting: %+v", e.Pending)
}
id := e.Pending.Ask.ID
out, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: StatusIdea, Note: "Parked: nothing carries it this month."})
if err != nil {
t.Fatal(err)
}
if len(out.Ended) != 2 || !strings.Contains(strings.Join(out.Ended, " "), id) {
t.Errorf("the answer does not name the ended version and ask: %v", out.Ended)
}
if !contains(w.router.cancelled, id) {
t.Errorf("the ask %s was not withdrawn on its channel: %v", id, w.router.cancelled)
}
e = mustGet(t, w.k.Store, key)
if e.Pending == nil || e.Pending.Ask != nil || e.Pending.Pending == nil || e.Pending.Pending.Closing == nil {
t.Errorf("the pending version is not the closing alone: %+v", e.Pending)
}
w.reviewAll(t)
if got := mustGet(t, w.k.Store, key).Current; got.Status != StatusIdea || got.Section("01-findings.md") != "" {
t.Errorf("after the review: %s, the ended version's finding served: %q", got.Status, got.Section("01-findings.md"))
}
}
// ADR 0630 §2: a graduated close counts the became links the effort already carries, and holds every one of them
// to naming an accepted record.
func TestAGraduatingCloseReadsTheBecameLinksTheEffortCarries(t *testing.T) {
w := imported(t, newMemStore())
dec := w.reserved(t, "author@test")
for _, c := range []struct {
name, became, wantErr string
}{
{"an old became to an accepted decision", "0290", ""},
{"an old became to a decision not accepted", Key{KindDecision, dec}.String(), "accepted"},
} {
r, _ := w.k.ReserveNumber("author@g14", KindResearch, "an effort")
a := effortAsk(r.Number, StatusActive, Section{Name: "00-overview.md", Text: "What it was."})
a.Links = append(a.Links, LinkAsk{Kind: LinkBecame, To: "0290"})
w.accepted(t, "author@g14", a)
if c.became != "0290" {
if _, err := w.k.Propose("author@test", valid(dec)); err != nil { // proposed, never reviewed: not accepted
t.Fatal(err)
}
// the old link goes to a record not accepted: written into the held record, as an import would
e := mustGet(t, w.k.Store, Key{KindResearch, r.Number})
cur := e.Current.Clone()
for i := range cur.Links {
if cur.Links[i].Kind == LinkBecame {
cur.Links[i].To = Ref{Kind: KindDecision, Number: dec}
}
}
if err := w.k.Store.Save(context.Background(), Version{By: "import@test", At: time.Now(), Why: "seeded", Record: cur}, true, false, e.Revision); err != nil {
t.Fatal(err)
}
}
_, err := w.k.Close("lead@shanks", CloseAsk{Number: r.Number, To: StatusGraduated, Note: closeNote("0290")})
if (err == nil) != (c.wantErr == "") || (err != nil && !strings.Contains(err.Error(), c.wantErr)) {
t.Errorf("%s: %v, want %q", c.name, err, c.wantErr)
}
}
}
// ADR 0630 §2: the review of a closing sees the closing alone, so a record over the judgement's limit is reviewable;
// an idea's closing takes no links and the question says so (§1); a graduated closing is judged on every became link
// the effort carries, the carried ones too, each with its record's title and status.
func TestAClosingOfARecordOverTheJudgementLimitIsReviewed(t *testing.T) {
w := imported(t, newMemStore())
// An old link to a decision with long clauses: the old question carried every clause of every linked record.
var long []Clause
for i := 1; i <= 20; i++ {
long = append(long, Clause{N: i, Text: "**Long.** " + strings.Repeat("A long clause. ", 300), Status: ClauseInForce, Source: SourceWritten})
}
seedRecord(t, w.k.Store, &Record{Kind: KindDecision, Number: 9000, Title: "a long decision", Status: StatusAccepted, Topic: "the mesh",
Clauses: long}, time.Now())
adr := mustGet(t, w.k.Store, Key{KindDecision, 290}).Current
becameADR := Link{Kind: LinkBecame, To: Ref{Kind: KindDecision, Number: 290}, Source: SourceWritten}
for _, c := range []struct {
name, to string
carried []Link
want []string
}{
{"to an idea", StatusIdea, nil, []string{"takes no links", "The links the closing adds:\n- none"}},
{"to graduated, its became carried", StatusGraduated, []Link{becameADR, becameADR},
[]string{"- became ADR 0290\n", fmt.Sprintf("- ADR 0290. %s (%s)\n", clip(adr.Title, 200), StatusAccepted)}},
} {
n := w.effort(t, StatusActive)
key := Key{KindResearch, n}
e := mustGet(t, w.k.Store, key)
big := e.Current.Clone()
big.Sections = append(big.Sections, Section{Name: "01-findings.md", Text: "OLD-FINDING " + strings.Repeat("The effort measured one more thing. ", 2200)})
big.Links = append(big.Links, Link{Kind: LinkTouches, To: Ref{Kind: KindDecision, Number: 9000}, Source: SourceWritten})
big.Links = append(big.Links, c.carried...)
if len(ReviewQuestion(big, nil)) <= MostReviewQuestion {
t.Fatalf("%s: the seeded record is not over the judgement's limit", c.name)
}
if err := w.k.Store.Save(context.Background(), Version{By: "import@test", At: time.Now(), Why: "seeded", Record: big}, true, false, e.Revision); err != nil {
t.Fatal(err)
}
if _, err := w.k.Close("lead@shanks", CloseAsk{Number: n, To: c.to, Note: closeNote("0290")}); err != nil {
t.Fatalf("%s: %v", c.name, err)
}
w.judge.reviews = nil
said := w.reviewAll(t)
if len(w.judge.reviews) != 1 {
t.Fatalf("%s: not judged: %v", c.name, said)
}
q := w.judge.reviews[0]
bad := strings.Contains(q, "OLD-FINDING") || strings.Contains(q, "A long clause") || strings.Contains(q, "a long decision") ||
len(q) > MostReviewQuestion || !strings.Contains(q, "served as active") || strings.Count(q, "ADR 0290. ") > 1
for _, s := range c.want {
bad = bad || !strings.Contains(q, s)
}
if bad {
t.Errorf("%s: the question is not the closing alone (%d bytes):\n%.1500s", c.name, len(q), q)
}
if got := mustGet(t, w.k.Store, key).Current.Status; got != c.to {
t.Errorf("%s: after the review: %s", c.name, got)
}
}
}