mesh/delivery merged: source.main moved to novox/mesh-catalog@65fad0ae1c31
mesh/check being carried to 5d50b952d4cb on 65fad0ae1c31 | from a68b39f7b512
A record proposed before a retraction and accepted after it marked the retracted clause changed or the retracted record superseded, serving it again; and returning a drafted record numbered its clauses anew, bringing retracted ones back in force or making a waiting Approve mark another clause. accept now leaves what is retracted alone, Return refuses a record with a retraction, and retract waits until a record's drafting is done.
365 lines
14 KiB
Go
365 lines
14 KiB
Go
package main
|
|
|
|
// Withdraw (novox/hq issue 1081): the operator's decision that a reserved or proposed record is dropped, so its
|
|
// number stays spent and the record stops asking anything of anyone.
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
func TestAReservedRecordIsWithdrawnAtOnce(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
key := Key{KindDecision, n}
|
|
out, err := w.k.WithdrawRecord("operator@shanks", key, "never proposed")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Status != StatusWithdrawn || out.Record != key.String() {
|
|
t.Fatalf("%+v", out)
|
|
}
|
|
e := mustGet(t, w.k.Store, key)
|
|
if e.Current.Status != StatusWithdrawn {
|
|
t.Fatalf("status %q", e.Current.Status)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAProposedRecordNeedingTheOperatorIsWithdrawnWithItsAskEndedAndItsReviewCleared(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
key := Key{KindDecision, n}
|
|
p := valid(n)
|
|
p.NeedsOperator = true
|
|
p.Brief = &Brief{Headline: "a test record that needs the operator", Summary: "the operator is asked, then the proposal is withdrawn."}
|
|
if _, err := w.k.Propose("author@test", p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.reviewAll(t)
|
|
before := mustGet(t, w.k.Store, key)
|
|
if before.Current.Status != StatusProposed || before.Current.Ask == nil || before.Current.Ask.Said != AskWaiting {
|
|
t.Fatalf("not waiting for the operator: %+v", before.Current)
|
|
}
|
|
why := "replaced by research on moving designs into the knowledge base"
|
|
out, err := w.k.WithdrawRecord("operator@shanks", key, why)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Status != StatusWithdrawn {
|
|
t.Fatalf("status %q", out.Status)
|
|
}
|
|
after := mustGet(t, w.k.Store, key)
|
|
if after.Current.Status != StatusWithdrawn {
|
|
t.Fatalf("status %q", after.Current.Status)
|
|
}
|
|
if after.Versions != before.Versions+1 {
|
|
t.Fatalf("versions %d, want %d", after.Versions, before.Versions+1)
|
|
}
|
|
if after.Current.Ask == nil || after.Current.Ask.Said == AskWaiting {
|
|
t.Fatalf("the waiting ask was not ended: %+v", after.Current.Ask)
|
|
}
|
|
if !contains(w.router.cancelled, before.Current.Ask.ID) {
|
|
t.Fatalf("the router was not told to take back the ask %s: cancelled %v", before.Current.Ask.ID, w.router.cancelled)
|
|
}
|
|
if after.Current.Judging != nil {
|
|
t.Fatalf("a pending review was not cleared: %+v", after.Current.Judging)
|
|
}
|
|
vs, err := w.k.Store.Versions(context.Background(), key)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
last := vs[len(vs)-1]
|
|
if last.By != "operator@shanks" || !strings.Contains(last.Why, why) {
|
|
t.Fatalf("the new version is not by the caller with the why: %+v", last)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAnAcceptedRecordRefusesWithdrawNamingWhy(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
key := Key{KindDecision, n}
|
|
w.accepted(t, "author@test", valid(n))
|
|
if _, err := w.k.WithdrawRecord("operator@shanks", key, "no longer wanted"); err == nil || !strings.Contains(err.Error(), "accepted") {
|
|
t.Fatalf("err %v", err)
|
|
}
|
|
e := mustGet(t, w.k.Store, key)
|
|
if e.Current.Status != StatusAccepted {
|
|
t.Fatalf("an accepted record was changed: %q", e.Current.Status)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAWithdrawnRecordRefusesWithdrawAgain(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
key := Key{KindDecision, n}
|
|
if _, err := w.k.WithdrawRecord("operator@shanks", key, "first"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := w.k.WithdrawRecord("operator@shanks", key, "again"); err == nil || !strings.Contains(err.Error(), "withdrawn") {
|
|
t.Fatalf("err %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWithdrawNeedsWhyAndRefusesARecordItHoldsNoneOf(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
if _, err := w.k.WithdrawRecord("operator@shanks", Key{KindDecision, n}, " "); err == nil {
|
|
t.Fatal("an empty why was accepted")
|
|
}
|
|
if _, err := w.k.WithdrawRecord("operator@shanks", Key{KindDecision, 999999}, "why"); err == nil {
|
|
t.Fatal("a record it holds none of was accepted")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Retract (the operator, 2026-10-31: "I'm more a fan of removing incorrect decisions than creating new records"): an
|
|
// accepted record, or listed clauses of it, stops holding on the operator's Approve and on nothing else; its versions
|
|
// stay, every read says it is retracted, and the whole-set rules do not fail for it.
|
|
func TestARetractionTakesEffectOnlyOnTheOperatorsApprove(t *testing.T) {
|
|
const why = "never verified by the operator"
|
|
type tc struct {
|
|
name string
|
|
clauses []int
|
|
answer string // approve, decline, or withdraw (the pending retraction withdrawn, then approved too late)
|
|
// then is a record proposed before the retraction and accepted after it, changing §1 or superseding the record:
|
|
// its acceptance never brings back what was retracted.
|
|
then string
|
|
}
|
|
for _, c := range []tc{
|
|
{"the record whole, approved", nil, OptionApprove, ""},
|
|
{"one clause, approved", []int{1}, OptionApprove, ""},
|
|
{"the record whole, declined", nil, OptionDecline, ""},
|
|
{"one clause, withdrawn before the answer", []int{1}, "withdraw", ""},
|
|
{"one clause, then a change of it proposed before and accepted after", []int{1}, OptionApprove, LinkChanges},
|
|
{"the record whole, then a supersession proposed before and accepted after", nil, OptionApprove, LinkSupersedes},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
ctx := context.Background()
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
key, changed := Key{KindDecision, n}, Key{KindDecision, 276}
|
|
w.accepted(t, "author@test", valid(n))
|
|
w.k.judgeSet(ctx)
|
|
findings, _ := w.k.Store.Findings(ctx)
|
|
later := 0
|
|
if c.then != "" {
|
|
later = w.reserved(t, "author@test")
|
|
p := valid(later)
|
|
p.Links = []LinkAsk{{Kind: LinkExtends, To: itoa4(n)}, {Kind: LinkChanges, To: itoa4(n) + " §1", From: 1}}
|
|
if c.then == LinkSupersedes {
|
|
p.Links = []LinkAsk{{Kind: LinkSupersedes, To: itoa4(n)}}
|
|
}
|
|
if _, err := w.k.Propose("author@test", p); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
before := mustGet(t, w.k.Store, key)
|
|
asked := len(w.router.asked)
|
|
|
|
out, err := w.k.Retract("lead@shanks", key, c.clauses, why)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if out.Status != RetractAsked || out.Record != key.String() || out.Ask == "" {
|
|
t.Fatalf("%+v", out)
|
|
}
|
|
// Asked once, at a glance, under the router's bounds; nothing changed yet.
|
|
if len(w.router.asked) != asked+1 {
|
|
t.Fatalf("asked %d times", len(w.router.asked)-asked)
|
|
}
|
|
a := w.router.asked[len(w.router.asked)-1]
|
|
if err := a.Check(w.clock); err != nil || asks.Glance(a.Explanation) != nil || len([]rune(a.Headline)) > 60 ||
|
|
!strings.Contains(a.Headline, key.String()) || !strings.Contains(a.Explanation+a.Details, why) {
|
|
t.Fatalf("the ask %+v: %v", a, err)
|
|
}
|
|
g, err := w.k.Get(GetAsk{Kind: KindDecision, Number: n})
|
|
if err != nil || g.Status != StatusAccepted || !strings.Contains(g.Retracting, "lead@shanks") || len(g.Retracted) != 0 {
|
|
t.Fatalf("while asked: %+v %v", g, err)
|
|
}
|
|
if h, _ := w.k.Holds("", &key, 0); len(h.Clauses) != 2 {
|
|
t.Fatalf("while asked, holds %+v", h)
|
|
}
|
|
if _, err := w.k.Retract("lead@shanks", key, nil, "again"); err == nil {
|
|
t.Fatal("a second retraction was asked while one waits")
|
|
}
|
|
if _, err := w.k.Propose("author@test", ProposeAsk{Kind: KindDecision, Number: n, Title: "x", Insight: "y"}); err == nil ||
|
|
!strings.Contains(err.Error(), "retraction") {
|
|
t.Fatalf("an insight over a waiting retraction: %v", err)
|
|
}
|
|
|
|
switch c.answer {
|
|
case "withdraw":
|
|
if _, err := w.k.WithdrawRetraction("lead@shanks", key, "asked by mistake"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !contains(w.router.cancelled, out.Ask) {
|
|
t.Fatalf("the router was not told: %v", w.router.cancelled)
|
|
}
|
|
w.router.answer(out.Ask, OptionApprove, w.clock)
|
|
default:
|
|
w.router.answer(out.Ask, c.answer, w.clock)
|
|
}
|
|
if _, err := w.k.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.then != "" {
|
|
w.reviewAll(t)
|
|
if l := mustGet(t, w.k.Store, Key{KindDecision, later}).Current; !l.Accepted() {
|
|
t.Fatalf("the later record was not accepted: %s", l.Status)
|
|
}
|
|
r := mustGet(t, w.k.Store, key).Current
|
|
h, _ := w.k.Holds("", &key, 0)
|
|
// Whole: retracted, nothing held. One clause: §1 retracted, and §2 the only one held.
|
|
whole := c.clauses == nil
|
|
if (whole && (r.Status != StatusRetracted || len(h.Clauses) != 0)) ||
|
|
(!whole && (r.Clause(1).Status != ClauseRetracted || len(h.Clauses) != 1)) {
|
|
t.Fatalf("an acceptance brought back what was retracted: %s, %+v, holds %+v", r.Status, r.Clauses, h.Clauses)
|
|
}
|
|
return
|
|
}
|
|
e := mustGet(t, w.k.Store, key)
|
|
vs, _ := w.k.Store.Versions(ctx, key)
|
|
if len(vs) <= before.Versions || e.Current.Retracting != nil {
|
|
t.Fatalf("versions %d (were %d), still retracting %+v", len(vs), before.Versions, e.Current.Retracting)
|
|
}
|
|
g, _ = w.k.Get(GetAsk{Kind: KindDecision, Number: n})
|
|
h, _ := w.k.Holds("", &key, 0)
|
|
l, _ := w.k.Links(key)
|
|
lc, _ := w.k.Links(changed)
|
|
target := mustGet(t, w.k.Store, changed).Current.Clause(1)
|
|
if c.answer != OptionApprove {
|
|
// Nothing changed: it holds as it did.
|
|
if e.Current.Status != StatusAccepted || len(h.Clauses) != 2 || len(g.Retracted) != 0 || g.Retracting != "" ||
|
|
strings.Contains(strings.Join(target.ChangedBy, ","), "retracted") {
|
|
t.Fatalf("a retraction not approved changed it: %+v, holds %d", g, len(h.Clauses))
|
|
}
|
|
return
|
|
}
|
|
// Approved: it no longer holds, says by whom, when, why and on what, and history keeps it.
|
|
if len(g.Retracted) != 1 || g.Retracted[0].By != "lead@shanks" || g.Retracted[0].Why != why ||
|
|
!strings.Contains(g.Retracted[0].Approval, "telegram") || g.Retracting != "" {
|
|
t.Fatalf("get: %+v", g.Retracted)
|
|
}
|
|
if !strings.Contains(vs[len(vs)-1].Why, "retracted") || vs[len(vs)-1].By != "the operator" {
|
|
t.Fatalf("the version: %+v", vs[len(vs)-1])
|
|
}
|
|
hit := false
|
|
for _, ev := range w.said.of(EventChanged) {
|
|
hit = hit || (ev["record"] == key.String() && strings.Contains(ev["what"].(string), "retracted"))
|
|
}
|
|
if !hit {
|
|
t.Fatalf("no changed event says the retraction: %v", w.said.of(EventChanged))
|
|
}
|
|
// What it changed stays changed; its mark says the change is retracted; the link says so both ways.
|
|
if target.Status != ClauseChanged || !strings.Contains(strings.Join(target.ChangedBy, ","), "retracted") {
|
|
t.Fatalf("0276 §1: %+v", target)
|
|
}
|
|
marked := false
|
|
for _, r := range lc.In {
|
|
marked = marked || (strings.HasPrefix(r.From, key.String()) && r.Retracted != "")
|
|
}
|
|
for _, r := range l.Out {
|
|
marked = marked && (r.Retracted != "" || (c.clauses != nil && r.From == key.String()))
|
|
}
|
|
if !marked {
|
|
t.Fatalf("links: %+v / %+v", l.Out, lc.In)
|
|
}
|
|
asked2, _ := w.k.Ask("merge window ninety seconds quiet", MostBound)
|
|
if c.clauses == nil {
|
|
if e.Current.Status != StatusRetracted || g.Status != StatusRetracted || len(h.Clauses) != 0 ||
|
|
!strings.HasPrefix(g.Clauses[0].Holds, "retracted") {
|
|
t.Fatalf("whole: %s, holds %+v, get %+v", e.Current.Status, h.Clauses, g.Clauses)
|
|
}
|
|
for _, r := range asked2.Records {
|
|
if r.Record == key.String() {
|
|
t.Fatalf("ask serves a retracted record: %+v", r)
|
|
}
|
|
}
|
|
} else {
|
|
if e.Current.Status != StatusAccepted || len(h.Clauses) != 1 || h.Clauses[0].N != 2 ||
|
|
!strings.HasPrefix(g.Clauses[0].Holds, "retracted") || g.Clauses[1].Holds != "in force" {
|
|
t.Fatalf("clause: %s, holds %+v, get %+v", e.Current.Status, h.Clauses, g.Clauses)
|
|
}
|
|
for _, r := range asked2.Records {
|
|
for _, ac := range r.Clauses {
|
|
if r.Record == key.String() && ac.N == 1 {
|
|
t.Fatalf("ask serves a retracted clause: %+v", r)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The whole-set rules say no more than they did.
|
|
w.k.judgeSet(ctx)
|
|
if after, _ := w.k.Store.Findings(ctx); len(after) != len(findings) {
|
|
t.Fatalf("findings were %v, are %v", findings, after)
|
|
}
|
|
if _, err := w.k.Retract("lead@shanks", key, c.clauses, why); err == nil {
|
|
t.Fatal("a retracted record or clause was retracted again")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
// Refused, with nothing asked: no why, a record not accepted, a clause it has none of.
|
|
eachStore(t, func(t *testing.T, s Store) {
|
|
w := imported(t, s)
|
|
n := w.reserved(t, "author@test")
|
|
key := Key{KindDecision, n}
|
|
if _, err := w.k.Retract("lead@shanks", key, nil, why); err == nil || !strings.Contains(err.Error(), "withdraw") {
|
|
t.Fatalf("a reserved record: %v", err)
|
|
}
|
|
w.accepted(t, "author@test", valid(n))
|
|
for _, bad := range []struct {
|
|
clauses []int
|
|
why string
|
|
}{{nil, " "}, {[]int{9}, why}, {[]int{1, 1}, why}} {
|
|
if _, err := w.k.Retract("lead@shanks", key, bad.clauses, bad.why); err == nil {
|
|
t.Fatalf("retract %v %q was asked", bad.clauses, bad.why)
|
|
}
|
|
}
|
|
if len(w.router.asked) != 0 {
|
|
t.Fatalf("asked %d", len(w.router.asked))
|
|
}
|
|
if _, err := w.k.WithdrawRetraction("lead@shanks", key, "nothing waits"); err == nil {
|
|
t.Fatal("a retraction was withdrawn where none waits")
|
|
}
|
|
// A drafted record: retracted only once its drafting is done, and never returned to be drafted again while a
|
|
// retraction waits or after one, which would number its clauses anew and bring them back in force.
|
|
drafted := Key{KindDecision, 240}
|
|
draftUntil(t, w, drafted, DraftDrafted)
|
|
if _, err := w.k.Retract("lead@shanks", drafted, []int{1}, why); err == nil || !strings.Contains(err.Error(), "draft") {
|
|
t.Fatalf("a record still being drafted: %v", err)
|
|
}
|
|
draftUntil(t, w, drafted, DraftDone)
|
|
out, err := w.k.Retract("lead@shanks", drafted, []int{1}, why)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := w.k.Return("operator@shanks", drafted, "redraft"); err == nil || !strings.Contains(err.Error(), "retract") {
|
|
t.Fatalf("returned while a retraction waits: %v", err)
|
|
}
|
|
w.router.answer(out.Ask, OptionApprove, w.clock)
|
|
if _, err := w.k.Reconcile(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := w.k.Return("operator@shanks", drafted, "redraft"); err == nil || !strings.Contains(err.Error(), "retract") {
|
|
t.Fatalf("returned after a retraction: %v", err)
|
|
}
|
|
if c := mustGet(t, w.k.Store, drafted).Current.Clause(1); c == nil || c.Status != ClauseRetracted {
|
|
t.Fatalf("§1: %+v", c)
|
|
}
|
|
})
|
|
}
|