Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/writing_test.go
T
jschoubben 7bf0e991d8 CAT-K7: every operation's event is sent once, after a restart too
An event of a change carries the record, the version it follows and the event as its message id; the last version sent per
record is a key of the state sent; at start the events of the operations after it are sent again under the same ids, so a
stop between an operation and its event loses none and a second start sends none.

Issue 1966.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:09:26 +01:00

176 lines
7.1 KiB
Go

package main
import (
"strings"
"testing"
)
// ledgerWorld is a knowledge base over the ledger played here, holding the fixture's records.
func ledgerWorld(t *testing.T) (*world, *fakeLedger) {
t.Helper()
f := newFakeLedger(t)
w := newWorld(t, newLedgerStore(f, newFakeStates()), fixture())
w.k.States = newFakeStates()
if _, err := w.k.Import("import@test", ImportAsk{}, fixture()); err != nil {
t.Fatal(err)
}
return w, f
}
func (f *fakeLedger) count(op string) int {
n := 0
for _, a := range f.applied {
if a.Operation == op {
n++
}
}
return n
}
// A record is written in a draft one section per call and submitted as a version (to-be 55 §4, AC48): each section's text
// is in the history once, at the write; submit writes none again; only the draft's author writes it; take-over makes
// another the author, with the reason in the next version's message; a refusal at submit names every reason and keeps the draft.
func TestADraftIsWrittenOneSectionAtATimeAndSubmittedAsAVersion(t *testing.T) {
w, f := ledgerWorld(t)
r, _ := w.k.ReserveNumber("author@g14", KindDecision, "a test record")
p := valid(r.Number)
secs := p.Sections
secs[0].Text = "The merge window of ADR 0276 §1 waits too long." // a reference of the one form; a link to a file is refused at the write
p.Sections = nil
key := Key{KindDecision, r.Number}
if _, err := w.k.Open("", p); err == nil || !strings.Contains(err.Error(), "proved") {
t.Fatalf("an unproven caller opened a draft: %v", err)
}
if _, err := w.k.Open("other@g14", p); err == nil || !strings.Contains(err.Error(), "reserved by author@g14") {
t.Fatalf("another caller opened under a reserved number: %v", err)
}
if _, err := w.k.Write("author@g14", key, "Context", "x"); err == nil || !strings.Contains(err.Error(), "no draft") {
t.Fatalf("a write with no draft: %v", err)
}
if _, err := w.k.Open("author@g14", p); err != nil {
t.Fatal(err)
}
before := f.count(OpWriteSection)
for _, s := range secs[:3] {
if _, err := w.k.Write("author@g14", key, s.Name, s.Text); err != nil {
t.Fatal(err)
}
}
if f.count(OpWriteSection) != before+3 {
t.Fatalf("the text was not in the history at the write")
}
// Another session cannot write, remove, order, submit or drop it; the refusal names the author.
for name, do := range map[string]func() error{
"write": func() error { _, err := w.k.Write("other@g14", key, "Context", "y"); return err },
"remove": func() error { _, err := w.k.Remove("other@g14", key, "Context"); return err },
"order": func() error { _, err := w.k.Order("other@g14", key, []string{"Context"}); return err },
"submit": func() error { _, err := w.k.SubmitDraft("other@g14", key, ""); return err },
"drop": func() error { _, err := w.k.DropDraft("other@g14", key, ""); return err },
} {
if err := do(); err == nil || !strings.Contains(err.Error(), "author@g14") {
t.Errorf("%s by another session: %v", name, err)
}
}
// The fourth section, then the order is set and one section is replaced.
if _, err := w.k.Write("author@g14", key, secs[3].Name, secs[3].Text); err != nil {
t.Fatal(err)
}
if _, err := w.k.Order("author@g14", key, []string{"Context", "Considered Options"}); err == nil {
t.Fatal("an order that leaves sections out was taken")
}
if _, err := w.k.Remove("author@g14", key, "No such"); err == nil {
t.Fatal("a section the draft lacks was removed")
}
if _, err := w.k.Write("author@g14", key, "Consequences", "Merges land sooner, and later too."); err != nil {
t.Fatal(err)
}
// Take-over: the reason is carried into the next version.
if _, err := w.k.TakeOver("other@g14", key, ""); err == nil {
t.Fatal("a take-over with no reason")
}
if to, err := w.k.TakeOver("other@g14", key, "the author's session is gone"); err != nil || to.From != "author@g14" {
t.Fatalf("%v %+v", err, to)
}
if _, err := w.k.Write("author@g14", key, "Context", "z"); err == nil {
t.Fatal("the previous author still writes")
}
writes := f.count(OpWriteSection)
out, err := w.k.SubmitDraft("other@g14", key, "")
if err != nil {
t.Fatal(err)
}
if f.count(OpWriteSection) != writes {
t.Errorf("submit wrote %d sections again", f.count(OpWriteSection)-writes)
}
// Version 3: the reservation, the take-over, the proposal.
if out.Status != StatusProposed || out.Version != 3 {
t.Fatalf("%+v", out)
}
e := mustGet(t, w.k.Store, key)
if len(e.Current.Sections) != 4 || e.Current.Section("Consequences") != "Merges land sooner, and later too." || !strings.Contains(e.Current.Author, "other@g14") {
t.Fatalf("%+v", e.Current.Sections)
}
if len(w.said.of(EventSubmitted)) != 1 || len(w.said.of(EventTakenOver)) != 1 || len(w.said.of(EventWritten)) < 5 || len(w.said.of(EventOpened)) != 1 {
t.Fatalf("events %v", w.said.events)
}
if d, _, _ := w.k.readDraft(key); d != nil {
t.Fatal("the draft stands after its submit")
}
}
func TestASubmitRefusedKeepsTheDraftAndANewVersionOfAnEffortStartsFromItsSections(t *testing.T) {
w, f := ledgerWorld(t)
r, _ := w.k.ReserveNumber("author@g14", KindResearch, "an effort")
first := effortAsk(r.Number, StatusActive, Section{Name: "Overview", Text: "What it would be."}, Section{Name: "Findings", Text: "One."})
w.accepted(t, "author@g14", first)
key := Key{KindResearch, r.Number}
// Closed with no section written: the draft starts from the served sections, so no text is written at all.
closing := ProposeAsk{Kind: KindResearch, Number: r.Number, Status: StatusGraduated, Links: append(first.Links, LinkAsk{Kind: LinkBecame, To: "0290"})}
d, err := w.k.Open("author@g14", closing)
if err != nil || strings.Join(d.Sections, ",") != "Overview,Findings" {
t.Fatalf("%v %+v", err, d)
}
writes := f.count(OpWriteSection)
if _, err := w.k.SubmitDraft("author@g14", key, "graduated"); err != nil {
t.Fatal(err)
}
if f.count(OpWriteSection) != writes {
t.Fatal("a version that changed no section wrote a section")
}
w.reviewAll(t)
if e := mustGet(t, w.k.Store, key); e.Current.Status != StatusGraduated || len(e.Current.Sections) != 2 {
t.Fatalf("%s %v", e.Current.Status, e.Current.Sections)
}
// A decision that breaks a rule: every reason at once, the draft kept as it was.
n, _ := w.k.ReserveNumber("author@g14", KindDecision, "a test record")
p := valid(n.Number)
p.Sections, p.Topic = nil, "no such topic"
k := Key{KindDecision, n.Number}
_, _ = w.k.Open("author@g14", p)
_, _ = w.k.Write("author@g14", k, "Context", "short")
if _, err := w.k.SubmitDraft("author@g14", k, ""); err == nil || !strings.Contains(err.Error(), "refused by") {
t.Fatalf("%v", err)
}
if d, _, _ := w.k.readDraft(k); d == nil || len(d.Sections) != 1 {
t.Fatal("a refused submit lost the draft")
}
if _, err := w.k.DropDraft("author@g14", k, "not now"); err != nil {
t.Fatal(err)
}
if d, _, _ := w.k.readDraft(k); d != nil {
t.Fatal("a dropped draft stands")
}
}
// Until the records move to the ledger, the database takes a record whole: a draft is refused saying so.
func TestADraftIsRefusedWhileTheRecordsAreInTheDatabase(t *testing.T) {
w := imported(t, newMemStore())
w.k.States = newFakeStates()
if _, err := w.k.Open("author@g14", ProposeAsk{Kind: KindDecision, Number: 1}); err == nil || !strings.Contains(err.Error(), "propose it") {
t.Fatalf("%v", err)
}
}