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) } }