Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/fields_test.go
T
jschoubben 156fda4b33
mesh/merge-gate pass: builds mesh-knowledge → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-catalog@1b7f79f76177 (merged as 73367192 into main, walk plan-17917161183…
mesh-knowledge: refresh repairs records the knowledge base drafted, keeping their clauses while their source is unchanged; ADR 0114 and 0148 as fixtures (review of #213)
2026-10-11 12:44:08 +02:00

410 lines
17 KiB
Go

package main
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// hq issue 487: the import held every record of hq at f08920199d25 and lost some of what they say — `reconstructed`,
// `amended`, a research effort's `touches`, `rests-on` as its own link — took clauses from a numbered list inside a
// `###` subsection of the Decision section (ADR 0259, 0272, 0273, 0278, 0288, and 0040, 0114, 0148 the same way),
// and left the 11 records with no Decision section clause-less with no drafting queued. The fixture is those real
// records, copied from hq at that commit (testdata/hq-f0892019/COMMIT).
func at487() dirRecords { return dirRecords{dir: filepath.Join("testdata", "hq-f0892019")} }
func imported487(t *testing.T, s Store) *world {
t.Helper()
w := newWorld(t, s, at487())
out, err := w.k.Import("import@test", ImportAsk{}, at487())
if err != nil {
t.Fatal(err)
}
if out.Refused != 0 || out.Created != 15 {
t.Fatalf("%s %+v", out.Said, out.Rows)
}
return w
}
func linkTo(r *Record, kind, to string) *Link {
for i, l := range r.Links {
if l.Kind == kind && l.To.String() == to {
return &r.Links[i]
}
}
return nil
}
func TestTheImportHoldsEveryFieldOfARecord(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported487(t, s)
get := func(k Key) *Record { return mustGet(t, s, k).Current }
// reconstructed: true and false are both held, as a field of their own.
if r := get(Key{KindDecision, 2}); r.Reconstructed == nil || !*r.Reconstructed || r.Extra["reconstructed"] != "" {
t.Fatalf("0002 reconstructed: %v, extra %v", r.Reconstructed, r.Extra)
}
if r := get(Key{KindDecision, 4}); r.Reconstructed == nil || *r.Reconstructed {
t.Fatalf("0004 reconstructed: %v", r.Reconstructed)
}
// amended: ADR 0085's day.
if r := get(Key{KindDecision, 85}); len(r.Amended) != 1 || r.Amended[0] != "2026-09-20" || r.Extra["amended"] != "" {
t.Fatalf("0085 amended: %v, extra %v", r.Amended, r.Extra)
}
// rests-on: its own typed link, not a citation.
for _, n := range []int{37, 38} {
r := get(Key{KindDecision, n})
if linkTo(r, LinkRestsOn, "ADR 0009") == nil || linkTo(r, LinkCites, "ADR 0009") != nil || r.Extra["rests-on"] != "" {
t.Fatalf("%04d's links: %v, extra %v", n, r.Links, r.Extra)
}
}
// touches: a link where it names a record, an issue or a design; text otherwise, in order.
r21 := get(Key{KindResearch, 21})
if linkTo(r21, LinkTouches, "design 03-DESIGN/01-to-be/34-the-console.md") == nil ||
strings.Join(r21.Touches, "|") != "the console|the tool runtime|seats|assignments" || r21.Extra["touches"] != "" {
t.Fatalf("research 021 touches: links %v, text %v, extra %v", r21.Links, r21.Touches, r21.Extra)
}
r30 := get(Key{KindResearch, 30})
for _, to := range []string{"issue 242", "ADR 0030", "ADR 0053"} {
if linkTo(r30, LinkTouches, to) == nil {
t.Errorf("research 030 does not touch %s: %v", to, r30.Links)
}
}
// get serves them, and links reads them from both ends.
g, err := w.k.Get(GetAsk{Kind: KindResearch, Number: 21, Bound: 16 << 10})
if err != nil {
t.Fatal(err)
}
if strings.Join(g.Touches, "|") != "the console|the tool runtime|seats|assignments" || !contains(g.Links, "touches design 03-DESIGN/01-to-be/34-the-console.md") {
t.Fatalf("get of research 021: %+v", g)
}
g2, _ := w.k.Get(GetAsk{Number: 2})
g85, _ := w.k.Get(GetAsk{Number: 85})
if g2.Reconstructed == nil || !*g2.Reconstructed || len(g85.Amended) != 1 {
t.Fatalf("get: 0002 reconstructed %v, 0085 amended %v", g2.Reconstructed, g85.Amended)
}
g37, _ := w.k.Get(GetAsk{Number: 37})
if !contains(g37.Links, "rests-on ADR 0009") {
t.Fatalf("get of 0037: %v", g37.Links)
}
in, _ := s.Inbound(context.Background(), Key{KindDecision, 30})
found := false
for _, l := range in {
found = found || (l.From == Key{KindResearch, 30} && l.Link.Kind == LinkTouches)
}
if !found {
t.Fatalf("0030 does not read research 030's touch from its end: %+v", in)
}
})
}
// Clauses are taken only from the Decision section's own top-level numbered list with bold leads, never from a list
// inside a `###` subsection of it.
func TestClausesComeOnlyFromTheDecisionSectionsOwnList(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
imported487(t, s)
for _, n := range []int{259, 272, 273, 278, 288, 40, 114, 148} {
r := mustGet(t, s, Key{KindDecision, n}).Current
if len(r.Clauses) != 0 || r.Drafting == nil || r.Drafting.State != DraftWaiting {
t.Errorf("ADR %04d: %d clause(s) %v, drafting %+v", n, len(r.Clauses), r.Clauses, r.Drafting)
}
}
})
// A Decision section with its own list, then a subsection with a list of its own: only the first is clauses.
section := "Some words.\n\n1. **The first.** It holds.\n And goes on.\n2. **The second.** It holds too.\n\n" +
"### A subsection\n\n1. **Not a clause.** A step of the subsection.\n2. **Nor this.** Another.\n"
got := WrittenClauses(section)
if len(got) != 2 || !strings.HasPrefix(got[0].Text, "**The first.**") || !strings.HasPrefix(got[1].Text, "**The second.**") {
t.Fatalf("%+v", got)
}
// A list inside a fenced block is not the section's either.
if got := WrittenClauses("```\n### not a heading\n```\n1. **One.** x\n"); len(got) != 1 {
t.Fatalf("a fence: %+v", got)
}
}
// A record with no Decision section (11 in hq) is queued for drafting from what it decides, never silently
// clause-less; one too large to draft from in one judgement is marked as having none.
func TestARecordWithNoDecisionSectionIsDraftedFromWhatItDecides(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
w := imported487(t, s)
for _, n := range []int{4, 37, 38} {
r := mustGet(t, s, Key{KindDecision, n}).Current
if r.Drafting == nil || r.Drafting.State != DraftWaiting || !strings.Contains(r.Drafting.Reason, "no Decision section") {
t.Fatalf("ADR %04d: %+v", n, r.Drafting)
}
}
k := Key{KindDecision, 4}
r := draftUntil(t, w, k, DraftDone)
if len(r.Clauses) == 0 || r.Clauses[0].Source != SourceConfirmed {
t.Fatalf("0004's clauses: %+v", r.Clauses)
}
var drafting string
for _, q := range w.judge.questions {
if strings.Contains(q, "ADR 0004") && strings.Contains(q, "no Decision section") {
drafting = q
break
}
}
if drafting == "" || !strings.Contains(drafting, "## The link is the security boundary") || strings.Contains(drafting, "## Consequences") {
t.Fatalf("0004 was drafted from: %.400s", drafting)
}
})
// Too large to draft from in one judgement: marked, said, never clause-less in silence.
w := newWorld(t, newMemStore(), at487())
r := &Record{Kind: KindDecision, Number: 9, Title: "huge", Status: StatusAccepted, Topic: "the mesh",
Sections: []Section{{Name: "What it is", Text: strings.Repeat("A node is a machine the mesh adopted. ", 2000)}},
Drafting: &Drafting{State: DraftWaiting, Reason: NoDecisionSection}}
if err := w.k.Store.Create(context.Background(), Version{By: "test", Why: "made", Record: r}); err != nil {
t.Fatal(err)
}
if _, err := w.k.DraftStep(); err != nil {
t.Fatal(err)
}
got := mustGet(t, w.k.Store, Key{KindDecision, 9}).Current
if got.Drafting.State != DraftDone || !strings.Contains(got.Drafting.Reason, "no Decision section") || len(w.judge.questions) != 0 {
t.Fatalf("%+v", got.Drafting)
}
}
// legacy is a record as the import before hq issue 487 wrote it: clauses from any numbered list of the Decision
// section, reconstructed, amended and touches only as unserved text, rests-on as a citation, and no drafting for a
// record with no Decision section.
func legacy(t *testing.T, path string, commit string) *Record {
t.Helper()
var p *ParsedRecord
var err error
if strings.HasPrefix(path, DecisionsFolder) {
p, err = ParseDecision(path, readFile(t, filepath.Join("testdata", "hq-f0892019", filepath.FromSlash(path))))
} else {
docs := map[string]string{}
entries, _ := os.ReadDir(filepath.Join("testdata", "hq-f0892019", filepath.FromSlash(path)))
for _, e := range entries {
docs[e.Name()] = readFile(t, filepath.Join("testdata", "hq-f0892019", filepath.FromSlash(path), e.Name()))
}
p, err = ParseResearch(filepath.Base(path), docs)
}
if err != nil {
t.Fatal(err)
}
r := p.Record
if r.Extra == nil {
r.Extra = map[string]string{}
}
if r.Reconstructed != nil {
r.Extra["reconstructed"] = map[bool]string{true: "true", false: "false"}[*r.Reconstructed]
}
if len(r.Amended) > 0 {
r.Extra["amended"] = strings.Join(r.Amended, ", ")
}
r.Reconstructed, r.Amended = nil, nil
var links []Link
var touched []string
for _, l := range r.Links {
switch l.Kind {
case LinkRestsOn:
l.Kind = LinkCites
case LinkTouches:
touched = append(touched, l.To.String())
continue
}
links = append(links, l)
}
r.Links = links
if len(touched)+len(r.Touches) > 0 {
r.Extra["touches"] = strings.Join(append(touched, r.Touches...), ", ")
}
r.Touches = nil
if r.Kind == KindDecision {
r.Drafting = nil
if s, ok := decisionSection(r); ok {
r.Clauses = writtenClauses(s)
for i := range r.Clauses {
r.Clauses[i].Source, r.Clauses[i].Status = SourceWritten, clauseStatusOf(r.Status)
}
if len(r.Clauses) == 0 {
r.Drafting = &Drafting{State: DraftWaiting}
}
}
}
r.Source = "hq@" + commit + ":" + path
return r
}
func TestARefreshRepairsOnlyRecordsNobodyChangedSinceTheImportAndChangesNothingTwice(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
ctx := context.Background()
w := newWorld(t, s, at487())
commit, _ := at487().Standing()
paths := []string{}
for _, dir := range []string{DecisionsFolder, ResearchFolder} {
entries, _ := os.ReadDir(filepath.Join("testdata", "hq-f0892019", dir))
for _, e := range entries {
paths = append(paths, dir+"/"+e.Name())
}
}
at := time.Date(2026, 10, 11, 9, 35, 7, 0, time.UTC)
for _, p := range paths {
r := legacy(t, p, commit)
if err := s.Create(ctx, Version{By: Unproven, At: at, Why: "imported from hq at " + short(commit) + ": " + p, Record: r}); err != nil {
t.Fatal(err)
}
}
// ADR 0085 was corrected by someone since its import, and the refresh never touches it.
e85 := mustGet(t, s, Key{KindDecision, 85})
d := e85.Current.Clone()
d.Title = "A secret is a provision, corrected"
if err := s.Save(ctx, Version{By: "someone@g14", At: at, Why: "a correction by hand", Record: d}, true, false, e85.Revision); err != nil {
t.Fatal(err)
}
before := countVersions(t, s)
// The dry run says what it would change, and writes nothing.
dry, err := w.k.Import("import@test", ImportAsk{DryRun: true, Refresh: true}, at487())
if err != nil {
t.Fatal(err)
}
if countVersions(t, s) != before || dry.Refreshed != 14 || dry.ChangedSince != 1 || dry.Created != 0 {
t.Fatalf("dry run: %s", dry.Said)
}
says := map[string]ImportRow{}
for _, row := range dry.Rows {
says[row.Record] = row
}
if row := says["ADR 0259"]; row.Action != ImportWouldRefresh || !strings.Contains(row.Why, "clauses") {
t.Fatalf("0259 in the dry run: %+v", row)
}
if row := says["ADR 0002"]; !strings.Contains(row.Why, "reconstructed") {
t.Fatalf("0002 in the dry run: %+v", row)
}
if row := says["ADR 0085"]; row.Action != ImportChangedSince {
t.Fatalf("0085 in the dry run: %+v", row)
}
out, err := w.k.Import("import@test", ImportAsk{Refresh: true, Commit: commit[:12]}, at487())
if err != nil {
t.Fatal(err)
}
if out.Refreshed != 14 || out.ChangedSince != 1 || countVersions(t, s) != before+14 {
t.Fatalf("refresh: %s", out.Said)
}
e := mustGet(t, s, Key{KindDecision, 259})
vs, _ := s.Versions(ctx, Key{KindDecision, 259})
if e.Versions != 2 || len(e.Current.Clauses) != 0 || e.Current.Drafting == nil || e.Current.Drafting.State != DraftWaiting ||
!strings.HasPrefix(vs[1].Why, "refreshed from hq at "+short(commit)) || !strings.Contains(vs[1].Why, "clauses") {
t.Fatalf("0259 after the refresh: %+v, %q", e.Current.Clauses, vs[1].Why)
}
if r := mustGet(t, s, Key{KindDecision, 2}).Current; r.Reconstructed == nil || !*r.Reconstructed {
t.Fatal("0002 not repaired")
}
if r := mustGet(t, s, Key{KindDecision, 37}).Current; linkTo(r, LinkRestsOn, "ADR 0009") == nil || r.Drafting == nil {
t.Fatalf("0037 not repaired: %v %+v", r.Links, r.Drafting)
}
if r := mustGet(t, s, Key{KindResearch, 21}).Current; len(r.Touches) != 4 {
t.Fatalf("research 021 not repaired: %v", r.Touches)
}
if e := mustGet(t, s, Key{KindDecision, 85}); e.Versions != 2 || e.Current.Title != "A secret is a provision, corrected" {
t.Fatalf("0085, changed since its import, was touched: %d versions", e.Versions)
}
// Run again: nothing changes, and it says so.
after := countVersions(t, s)
again, err := w.k.Import("import@test", ImportAsk{Refresh: true}, at487())
if err != nil {
t.Fatal(err)
}
if again.Refreshed != 0 || again.Unchanged != 14 || countVersions(t, s) != after || !strings.Contains(again.Said, "Nothing changed") {
t.Fatalf("a second refresh: %s", again.Said)
}
// Drafted and confirmed since, by the knowledge base itself: still the import's side. A later refresh keeps
// the drafted clauses, since the Decision section they come from is unchanged, and changes nothing.
r259 := draftUntil(t, w, Key{KindDecision, 259}, DraftDone)
third, _ := w.k.Import("import@test", ImportAsk{Refresh: true}, at487())
for _, row := range third.Rows {
if row.Record == "ADR 0259" {
t.Fatalf("0259 in a third refresh: %+v", row)
}
}
if got := mustGet(t, s, Key{KindDecision, 259}).Current; third.Refreshed != 0 || renderJSON(got.Clauses) != renderJSON(r259.Clauses) {
t.Fatalf("a third refresh: %s", third.Said)
}
})
}
// Live, ADR 0002 has versions 2 and 3 drafted and confirmed by the knowledge base after its import. Those are the
// import's side: a refresh repairs it, keeping its clauses while the Decision section they were drafted from is
// what hq says, and queuing it to be drafted again when it is not.
func TestARefreshRepairsARecordTheKnowledgeBaseDraftedAndKeepsItsClauses(t *testing.T) {
eachStore(t, func(t *testing.T, s Store) {
ctx := context.Background()
w := newWorld(t, s, at487())
commit, _ := at487().Standing()
p2 := DecisionsFolder + "/0002-nodes-communicate-over-a-broker.md"
p4 := DecisionsFolder + "/0004-a-node-and-how-it-joins.md"
r2 := legacy(t, p2, commit)
// 0004 has no Decision section; the import before #487 queued nothing, so a hand-made drafting stands in for
// one made from a text that is no longer what hq says.
r4 := legacy(t, p4, commit)
r4.Drafting = &Drafting{State: DraftWaiting}
r4.Sections = append(r4.Sections, Section{Name: "Decision", Text: "Nodes join by a token. That is decided here."})
for _, c := range []struct {
path string
r *Record
}{{p2, r2}, {p4, r4}} {
if err := s.Create(ctx, Version{By: Unproven, Why: "imported from hq at " + short(commit) + ": " + c.path, Record: c.r}); err != nil {
t.Fatal(err)
}
}
drafted2 := draftUntil(t, w, Key{KindDecision, 2}, DraftDone)
draftUntil(t, w, Key{KindDecision, 4}, DraftDone)
if mustGet(t, s, Key{KindDecision, 2}).Versions != 3 {
t.Fatal("0002 was not drafted and confirmed as live")
}
out, err := w.k.Import("import@test", ImportAsk{Refresh: true}, at487())
if err != nil {
t.Fatal(err)
}
if out.ChangedSince != 0 {
t.Fatalf("drafting counted as a change: %s %+v", out.Said, out.Rows)
}
e2 := mustGet(t, s, Key{KindDecision, 2})
if e2.Versions != 4 || e2.Current.Reconstructed == nil || !*e2.Current.Reconstructed ||
renderJSON(e2.Current.Clauses) != renderJSON(drafted2.Clauses) || e2.Current.Drafting == nil || e2.Current.Drafting.State != DraftDone {
t.Fatalf("0002 after the refresh: %d versions, %+v, drafting %+v", e2.Versions, e2.Current.Clauses, e2.Current.Drafting)
}
// 0004's clauses were drafted from a text hq does not hold: taken back, and drafted again from its sections.
e4 := mustGet(t, s, Key{KindDecision, 4}).Current
if len(e4.Clauses) != 0 || e4.Drafting == nil || e4.Drafting.State != DraftWaiting || !strings.Contains(e4.Drafting.Reason, "no Decision section") {
t.Fatalf("0004 after the refresh: %+v %+v", e4.Clauses, e4.Drafting)
}
// A record someone returned is someone's change: left alone.
if _, err := w.k.Return("operator@shanks", Key{KindDecision, 2}, "clause 2 says more than the record"); err != nil {
t.Fatal(err)
}
again, _ := w.k.Import("import@test", ImportAsk{Refresh: true}, at487())
if again.ChangedSince != 1 {
t.Fatalf("a returned record refreshed: %s", again.Said)
}
})
}
// A refresh without a commit's records refuses, and an import without refresh never rewrites what it holds.
func TestAnImportWithoutRefreshLeavesAHeldRecordAsItIs(t *testing.T) {
w := newWorld(t, newMemStore(), at487())
commit, _ := at487().Standing()
p := DecisionsFolder + "/0259-an-ask-is-a-message-on-the-bus-any-channel-answers-it-and-the-asker-acts-on-a-warrant.md"
if err := w.k.Store.Create(context.Background(), Version{By: Unproven, Why: "imported from hq at " + short(commit) + ": " + p, Record: legacy(t, p, commit)}); err != nil {
t.Fatal(err)
}
out, err := w.k.Import("import@test", ImportAsk{}, at487())
if err != nil {
t.Fatal(err)
}
if out.Held != 1 || out.Refreshed != 0 || len(mustGet(t, w.k.Store, Key{KindDecision, 259}).Current.Clauses) != 7 {
t.Fatalf("%s", out.Said)
}
}