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…
410 lines
17 KiB
Go
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)
|
|
}
|
|
}
|