An ask-ended the teller said before the assigner's consumer existed is not delivered to it (note 155). At each bind the assigner calls the seat verb operator-channel.asks once, a kept call, for the ended asks since its earliest held asking, and writes again each one that ended expired or cancelled, by the ask-ended rules and their compare-and-set. A seat verb: no module named. One invoke added. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
448 lines
23 KiB
Go
448 lines
23 KiB
Go
package assign
|
|
|
|
import (
|
|
"fmt"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents"
|
|
"mesh-delivery-assigner/internal/assignment"
|
|
"mesh-delivery-assigner/internal/puts"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
|
mwc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshwantsclient"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
|
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
|
|
|
mcc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
|
mcseat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
|
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
|
)
|
|
|
|
// 1297/B9 (the assigner's half): the ask about a held fingerprint is a valid ask of the router and of mesh-sdk's layout at
|
|
// its key, of a kind the view may answer.
|
|
func TestTheHoldAskIsValid(t *testing.T) {
|
|
t.Run("hold", theHoldAskIsValid)
|
|
t.Run("restore", theRestoreAskIsValid)
|
|
t.Run("ended at the channel", anAskEndedAtTheChannelIsWrittenAgain)
|
|
t.Run("ended at the channel, read at bind", anAskEndedAtTheChannelIsReadAtBind)
|
|
}
|
|
|
|
// Issue 1966, note 151: an ask the teller says ended at the operator channel without an answer (its event ask-ended) is
|
|
// written again under a new asking, an id of its own and a new expiry, one row per kind: the rules' asks are; the
|
|
// authorising ask of a proposal (ADR 0567 §5, §11) and a put's record ask (ADR 0546) are not, nor an asking the key no longer
|
|
// holds, an end asking again would not change, or a body that does not read.
|
|
func anAskEndedAtTheChannelIsWrittenAgain(t *testing.T) {
|
|
asked := time.Date(2026, 11, 9, 22, 0, 0, 0, time.UTC)
|
|
now := asked.Add(8 * 24 * time.Hour)
|
|
digest := "sha256:" + strings.Repeat("e", 64)
|
|
holdKey, _ := asks.HoldAskKey("nats", fNew)
|
|
restoreKey, _ := asks.RestoreAskKey("novox", "nats", fNew)
|
|
backupKey, _ := asks.BackupAskKey("novox", "nats", fNew)
|
|
recordKey, _ := asks.RecordAskKey("novox", "nats", digest)
|
|
assignmentsKey, _ := asks.AssignmentsAskKey("p-20261028t183000-1a2b3c")
|
|
need := RecordNeed{Node: "novox", Digest: digest, Judgement: JudgedMovesData}
|
|
record := RecordAsk(recordKey, "novox", "nats", fNew, need, 0, asked) // written before #195: no asking
|
|
put := puts.Put{FP: fNew, Why: "w", At: asked, Step: puts.Person, Ask: recordKey}
|
|
byPut := RecordAsk(recordKey, "novox", "nats", fNew, need, PutAsking(put), asked)
|
|
other := puts.Put{FP: fNew, Why: "w", At: asked.Add(time.Hour), Step: puts.Person, Ask: recordKey}
|
|
assignments := assignerAsk(AskingSeconds(asked), oc.Ask{ID: meshdeliveryclient.AskingID(assignmentsKey, AskingSeconds(asked)), Headline: "h",
|
|
Explanation: "e", Who: occ.Operator, Expires: occ.FormatTime(asked.Add(occ.ApproveLasts)), OnExpiry: "o",
|
|
Options: []oc.Option{{ID: OptionYes, Label: "Yes", Does: "d", Level: occ.Approve}}})
|
|
for _, c := range []struct {
|
|
name string
|
|
key string
|
|
a *asks.Ask // the ask the key holds; nil for none
|
|
ended string // the id of the asking that ended; "" for the one the key holds
|
|
outcome string
|
|
put *puts.Put
|
|
why string // "" when it is written again
|
|
}{
|
|
{name: "hold", key: holdKey, a: ptr(HoldAsk(holdKey, "nats", fNew, "novox", "w", AskingSeconds(asked), asked)), outcome: occ.OutcomeExpired},
|
|
{name: "restore", key: restoreKey, a: ptr(RestoreAsk(restoreKey, "novox", "nats", fNew, "w", AskingSeconds(asked), asked)), outcome: occ.OutcomeCancelled},
|
|
{name: "backup", key: backupKey, a: ptr(BackupAsk(backupKey, "novox", "nats", fNew, digest, "w", 0, asked)), outcome: occ.OutcomeCancelled},
|
|
{name: "record", key: recordKey, a: &record, outcome: occ.OutcomeCancelled},
|
|
{name: "record of another put", key: recordKey, a: &record, outcome: occ.OutcomeExpired, put: &other},
|
|
{name: "record a put asked", key: recordKey, a: &byPut, outcome: occ.OutcomeExpired, put: &put, why: "put"},
|
|
{name: "assignments", key: assignmentsKey, a: &assignments, outcome: occ.OutcomeExpired, why: "ADR 0567"},
|
|
{name: "another asking stands", key: holdKey, a: ptr(HoldAsk(holdKey, "nats", fNew, "novox", "w", AskingSeconds(now), now)),
|
|
ended: meshdeliveryclient.AskingID(holdKey, AskingSeconds(asked)), outcome: occ.OutcomeExpired, why: "holds the asking"},
|
|
{name: "taken away", key: holdKey, ended: "hold-x", outcome: occ.OutcomeExpired, why: "taken away"},
|
|
{name: "refused", key: holdKey, a: ptr(HoldAsk(holdKey, "nats", fNew, "novox", "w", 0, asked)), outcome: occ.OutcomeRefused, why: "refused"},
|
|
{name: "replaced", key: holdKey, a: ptr(HoldAsk(holdKey, "nats", fNew, "novox", "w", 0, asked)), outcome: occ.OutcomeReplaced, why: "replaced"},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
var current []byte
|
|
ended := c.ended
|
|
if c.a != nil {
|
|
var err error
|
|
if current, err = EncodeAsk(c.key, *c.a); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ended == "" {
|
|
ended = mustAsk(t, *c.a).ID
|
|
}
|
|
}
|
|
body, err := tellerevents.EncodeEnded(tellerevents.Ended{Key: c.key, Ask: ended, Outcome: c.outcome})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
e, err := tellerevents.DecodeEnded(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
value, why := AskedAgain(e, current, c.put, now)
|
|
if c.why != "" {
|
|
if value != nil || !strings.Contains(why, c.why) {
|
|
t.Fatalf("AskedAgain = %s, %q; want nothing written, %q", value, why, c.why)
|
|
}
|
|
return
|
|
}
|
|
if why != "" {
|
|
t.Fatalf("AskedAgain says %q, want the ask written again", why)
|
|
}
|
|
a, ask, err := DecodeAsk(c.key, value)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if a.Asking <= c.a.Asking || ask.ID != meshdeliveryclient.AskingID(c.key, a.Asking) || ask.ID == ended {
|
|
t.Errorf("asking %d under %s, want a new asking above %d under an id of its own, not %s", a.Asking, ask.ID, c.a.Asking, ended)
|
|
}
|
|
if err := occ.Check(ask, now); err != nil || !ExpiresAt(ask).Equal(now.Add(AskLasts).Truncate(time.Second)) {
|
|
t.Errorf("the router refuses the ask written again (%v), or it lapses at %s, want %s", err, ExpiresAt(ask), now.Add(AskLasts))
|
|
}
|
|
})
|
|
}
|
|
for _, raw := range []string{`{"v":1,"key":"k","ask":"a"}`, `{"v":1,"key":"k","ask":"a","outcome":"expired","more":1}`} {
|
|
if _, err := tellerevents.DecodeEnded([]byte(raw)); err == nil {
|
|
t.Errorf("DecodeEnded(%s) takes a body that is not an ended ask", raw)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Issue 1966 note 155, ADR 0668 §4: an ended ask the teller said before the assigner's consumer existed is not delivered to
|
|
// it, so at bind the assigner reads the operator-channel seat's asks: the query reaches back to the earliest held asking,
|
|
// and of the answer only the held askings that lapsed or were cancelled are taken, as the teller's event says them; an
|
|
// answered or another asker's ask, and one the key no longer holds, are not.
|
|
func anAskEndedAtTheChannelIsReadAtBind(t *testing.T) {
|
|
asked := time.Date(2026, 11, 9, 22, 0, 0, 0, time.UTC)
|
|
holdKey, _ := asks.HoldAskKey("nats", fNew)
|
|
restoreKey, _ := asks.RestoreAskKey("novox", "nats", fNew)
|
|
backupKey, _ := asks.BackupAskKey("novox", "nats", fNew)
|
|
hold := HoldAsk(holdKey, "nats", fNew, "novox", "w", AskingSeconds(asked), asked)
|
|
restore := RestoreAsk(restoreKey, "novox", "nats", fNew, "w", AskingSeconds(asked.Add(time.Hour)), asked.Add(time.Hour))
|
|
backup := BackupAsk(backupKey, "novox", "nats", fNew, "sha256:"+strings.Repeat("e", 64), "w", 0, asked.Add(-time.Hour)) // no asking
|
|
held := map[string][]byte{}
|
|
for key, a := range map[string]asks.Ask{holdKey: hold, restoreKey: restore, backupKey: backup} {
|
|
v, err := EncodeAsk(key, a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
held[key] = v
|
|
}
|
|
if _, ok := ChannelAsksQuery(nil); ok {
|
|
t.Error("with no ask held the channel is asked")
|
|
}
|
|
q, ok := ChannelAsksQuery(held)
|
|
if since, _ := q["since"].(string); !ok || q["state"] != "ended" || since != asked.Add(-time.Hour-time.Minute).Format(time.RFC3339) {
|
|
t.Errorf("the query is %v, want the ended asks since %s", q, asked.Add(-time.Hour-time.Minute).Format(time.RFC3339))
|
|
}
|
|
at := func(key string, a asks.Ask, state string) string {
|
|
return fmt.Sprintf(`{"key":"t.%s","state":%q}`, mustAsk(t, a).ID, state)
|
|
}
|
|
answer := `{"asks":[` + strings.Join([]string{at(holdKey, hold, occ.OutcomeExpired), at(restoreKey, restore, occ.OutcomeChosen),
|
|
at(backupKey, backup, occ.OutcomeCancelled), `{"key":"t.hold-gone","state":"expired"}`, `{"key":"open","state":"open"}`}, ",") + `],"cut":"c"}`
|
|
ended, cut, err := EndedAtChannel(held, []byte(answer))
|
|
want := []tellerevents.Ended{{Key: backupKey, Ask: mustAsk(t, backup).ID, Outcome: occ.OutcomeCancelled},
|
|
{Key: holdKey, Ask: mustAsk(t, hold).ID, Outcome: occ.OutcomeExpired}}
|
|
slices.SortFunc(want, func(a, b tellerevents.Ended) int { return strings.Compare(a.Key, b.Key) })
|
|
if err != nil || cut != "c" || !slices.Equal(ended, want) {
|
|
t.Errorf("EndedAtChannel = %+v, %q, %v; want %+v and the answer's cut", ended, cut, err, want)
|
|
}
|
|
if _, _, err := EndedAtChannel(held, []byte(`[1]`)); err == nil {
|
|
t.Error("an answer that does not read is taken")
|
|
}
|
|
}
|
|
|
|
func ptr(a asks.Ask) *asks.Ask { return &a }
|
|
|
|
func theHoldAskIsValid(t *testing.T) {
|
|
key, err := asks.HoldAskKey("mail", fNew)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now()
|
|
a := HoldAsk(key, "mail", fNew, "ace", "health failed, phase running: connection refused", 0, now)
|
|
if err := occ.Check(mustAsk(t, a), now); err != nil {
|
|
t.Errorf("the router refuses the ask: %v", err)
|
|
}
|
|
if _, err := EncodeAsk(key, a); err != nil {
|
|
t.Errorf("the ask cannot be written: %v", err)
|
|
}
|
|
// 1906/H3: the ask names the machine whose check failed and the hold's words.
|
|
if !strings.Contains(mustAsk(t, a).Explanation, "on ace") || !strings.Contains(mustAsk(t, a).Explanation, "connection refused") {
|
|
t.Errorf("the ask does not name the node and the hold's words: %q", mustAsk(t, a).Explanation)
|
|
}
|
|
// 1906/H2: an ask written again after a lift is another asking, under an id of its own.
|
|
again := HoldAsk(key, "mail", fNew, "ace", "x", 7, now)
|
|
if again.Asking != 7 || mustAsk(t, again).ID != meshdeliveryclient.AskingID(key, 7) || mustAsk(t, again).ID == mustAsk(t, a).ID {
|
|
t.Errorf("the asking after a lift = %d %s", again.Asking, mustAsk(t, again).ID)
|
|
}
|
|
if _, err := EncodeAsk(key, again); err != nil {
|
|
t.Errorf("the ask asked again cannot be written: %v", err)
|
|
}
|
|
if _, err := viewstate.ViewAnswerKey(key); err != nil {
|
|
t.Errorf("the view may not answer it: %v", err)
|
|
}
|
|
// The join (A1d ruling 1): mesh-sdk no longer refuses another id, no option or no ask in an assigner's ask; EncodeAsk does.
|
|
t.Run("EncodeAsk refuses what mesh-sdk no longer does", func(t *testing.T) {
|
|
good := HoldAsk(key, "mail", fNew, "ace", "x", 0, time.Unix(100, 0))
|
|
other := good
|
|
other.Asking = 3 // its id is the asking 0's
|
|
empty := good
|
|
ask := mustAsk(t, good)
|
|
ask.Options = nil
|
|
empty.Ask = assignerAsk(0, ask).Ask
|
|
for name, a := range map[string]asks.Ask{"another id": other, "no option": empty, "no ask": {}} {
|
|
if _, err := EncodeAsk(key, a); err == nil {
|
|
t.Errorf("%s: written", name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 1906/D3 (ADR 0539 §19): the ask about a data module held on a node is a valid ask of the router and of mesh-sdk's layout at
|
|
// its restore key, of a kind the view may answer; it offers restoring (losing the newer writes) and leaving it stopped.
|
|
func theRestoreAskIsValid(t *testing.T) {
|
|
key, err := asks.RestoreAskKey("ace", "mail", fNew)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now()
|
|
a := RestoreAsk(key, "ace", "mail", fNew, "connection refused", 2, now)
|
|
if err := occ.Check(mustAsk(t, a), now); err != nil {
|
|
t.Errorf("the router refuses the ask: %v", err)
|
|
}
|
|
if _, err := EncodeAsk(key, a); err != nil {
|
|
t.Errorf("the ask cannot be written: %v", err)
|
|
}
|
|
if _, err := viewstate.ViewAnswerKey(key); err != nil {
|
|
t.Errorf("the view may not answer it: %v", err)
|
|
}
|
|
var ids []string
|
|
for _, o := range mustAsk(t, a).Options {
|
|
ids = append(ids, o.ID)
|
|
}
|
|
if len(ids) != 2 || ids[0] != OptionRestore || ids[1] != OptionLeaveStopped {
|
|
t.Errorf("options = %v", ids)
|
|
}
|
|
if !strings.Contains(mustAsk(t, a).Explanation, "connection refused") || !strings.Contains(mustAsk(t, a).Explanation, "ace") {
|
|
t.Errorf("the ask does not name the node and the check's words: %q", mustAsk(t, a).Explanation)
|
|
}
|
|
}
|
|
|
|
// 1906/D3: the condition for a held fingerprint names the module, the node and the check's words, under the assigner's own
|
|
// name (ADR 0437 clause 9), and is cleared under the same key.
|
|
func TestTheAssignersConditions(t *testing.T) {
|
|
t.Run("the held condition names module, node and words", heldConditionNamesModuleNodeAndWords)
|
|
t.Run("a lifted hold clears its condition saying why", liftedHoldClearsItsConditionSayingWhy)
|
|
t.Run("1952/WM6: each said condition is a finding, its clear deletes it, said straight only with no findings bucket", saidConditionsAreFindings)
|
|
}
|
|
|
|
func heldConditionNamesModuleNodeAndWords(t *testing.T) {
|
|
c := HeldCondition("mail", fNew, "ace", "connection refused", true, true)
|
|
if _, err := occ.EncodeCondition(c); err != nil {
|
|
t.Fatalf("the condition is refused: %v", err)
|
|
}
|
|
if c.State != occ.StateRaised || c.Subject.ID != "mail" || c.Subject.Machine != "ace" || !strings.Contains(c.Explanation, "connection refused") || !strings.Contains(c.Key, "mail") {
|
|
t.Errorf("condition = %+v", c)
|
|
}
|
|
if got := HeldCondition("mail", fNew, "", "", false, false); got.State != occ.StateCleared || got.Key != c.Key {
|
|
t.Errorf("cleared = %+v", got)
|
|
}
|
|
}
|
|
|
|
// 1906/H2: a hold lifted by itself clears the held condition under its key, saying why.
|
|
func liftedHoldClearsItsConditionSayingWhy(t *testing.T) {
|
|
c := LiftedCondition("mail", fNew)
|
|
if _, err := occ.EncodeCondition(c); err != nil {
|
|
t.Fatalf("the condition is refused: %v", err)
|
|
}
|
|
if c.State != occ.StateCleared || c.Key != HeldCondition("mail", fNew, "ace", "x", true, false).Key || !strings.Contains(c.Resolved, "lifted its hold by itself") {
|
|
t.Errorf("condition = %+v", c)
|
|
}
|
|
}
|
|
|
|
// 1952/WM6 (ADR 0564 §2, issue 1952 note 16): every condition the assigner says is a finding in its own bucket, under
|
|
// `<scope>.<id>.<kind>`, written at the raise and deleted at the clear, and only as a finding: the keeper is the one
|
|
// source since the controller's end (novox/hq ADR 0590 §3, issue 1126 B8). Said straight to the router only where the
|
|
// findings bucket cannot be written to at all.
|
|
func saidConditionsAreFindings(t *testing.T) {
|
|
at := time.Date(2026, 10, 29, 21, 0, 0, 0, time.UTC)
|
|
for _, c := range []struct {
|
|
raised, cleared oc.Condition
|
|
key string
|
|
}{
|
|
{HeldCondition("mail", fNew, "ace", "refused", true, true), LiftedCondition("mail", fNew), "module.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".held"},
|
|
{NodeFailedCondition("ace", "mail", fNew, NodeFailure{Words: "x", Others: []string{"g14"}}, true), NodeFailedCondition("ace", "mail", fNew, NodeFailure{}, false), "module.ace.mail.failed-alone"},
|
|
{NeitherCondition("ace", "mail", Holding{Rule: 3, Words: "w"}, true), NeitherCondition("ace", "mail", Holding{}, false), "module.ace.mail.neither"},
|
|
{SeatWaitsCondition([]string{"assignments"}), SeatWaitsCondition(nil), "module.mesh-delivery-assigner.seat-states"},
|
|
{BackupCondition("ace", "mail", fNew, "disk full", true), BackupCondition("ace", "mail", fNew, "", false), "module.ace.mail." + strings.TrimPrefix(fNew, meshcatalogclient.SourcePrefix2) + ".backup"},
|
|
} {
|
|
puts, deletes, said := map[string]mcseat.FindingsByKeyValue{}, []string{}, 0
|
|
f := Findings{Put: func(k string, v []byte) error {
|
|
got, err := mcc.DecodeFinding(v)
|
|
puts[k] = got
|
|
return err
|
|
}, Delete: func(k string) error { deletes = append(deletes, k); return nil }, Now: func() time.Time { return at }}
|
|
say := f.Say(func(oc.Condition) error { said++; return nil })
|
|
if err := say(c.raised); err != nil {
|
|
t.Fatalf("%s: %v", c.key, err)
|
|
}
|
|
got, ok := puts[c.key]
|
|
if !ok || got.Severity != c.raised.Severity || got.Headline != c.raised.Headline || got.Observer != assignment.Module ||
|
|
got.Subject.Machine != c.raised.Subject.Machine || said != 0 {
|
|
t.Fatalf("%s: finding %+v (written %v), said %d", c.key, got, puts, said)
|
|
}
|
|
if err := say(c.cleared); err != nil || !slices.Equal(deletes, []string{c.key}) || said != 0 {
|
|
t.Fatalf("%s: deleted %v, said %d (%v)", c.key, deletes, said, err)
|
|
}
|
|
if err := (Findings{}).Say(func(oc.Condition) error { said++; return nil })(c.raised); err != nil || said != 1 {
|
|
t.Fatalf("%s: with no findings bucket said %d (%v), want straight once", c.key, said, err)
|
|
}
|
|
}
|
|
if _, _, _, err := FindingOf(oc.Condition{Key: "another.x", State: occ.StateRaised}, at); err == nil {
|
|
t.Error("a condition not the assigner's is a finding")
|
|
}
|
|
}
|
|
|
|
// 1906/D4: the record ask and the backup ask are valid asks of the router and of mesh-sdk's layout at their keys, answerable in
|
|
// the view, and name the move and the node.
|
|
func TestTheRecordAndBackupAsksAreValid(t *testing.T) {
|
|
now := time.Now()
|
|
rkey, err := asks.RecordAskKey("ace", "mail", "sha256:"+strings.Repeat("b", 64))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := RecordAsk(rkey, "ace", "mail", fNew, RecordNeed{Node: "ace", Digest: "sha256:" + strings.Repeat("b", 64), Judgement: JudgedCannot, Words: "the composition it runs is gone"}, 2, now)
|
|
bkey, err := asks.BackupAskKey("ace", "mail", fNew)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dgst := "sha256:" + strings.Repeat("b", 64)
|
|
b := BackupAsk(bkey, "ace", "mail", fNew, dgst, "no space left", 0, now)
|
|
for key, a := range map[string]asks.Ask{rkey: r, bkey: b} {
|
|
if err := occ.Check(mustAsk(t, a), now); err != nil {
|
|
t.Errorf("%s: the router refuses the ask: %v", key, err)
|
|
}
|
|
if _, err := EncodeAsk(key, a); err != nil {
|
|
t.Errorf("%s: cannot be written: %v", key, err)
|
|
}
|
|
if _, err := viewstate.ViewAnswerKey(key); err != nil {
|
|
t.Errorf("%s: the view may not answer it: %v", key, err)
|
|
}
|
|
}
|
|
if !strings.Contains(mustAsk(t, r).Explanation, "gone") || mustAsk(t, r).Options[0].ID != OptionYes {
|
|
t.Errorf("the record ask = %+v", mustAsk(t, r))
|
|
}
|
|
if !strings.Contains(mustAsk(t, b).Explanation, "no space left") || mustAsk(t, b).Options[0].ID != mwc.BackupAgain || mustAsk(t, b).Options[1].ID != mwc.BackupSkip {
|
|
t.Errorf("the backup ask = %+v", mustAsk(t, b))
|
|
}
|
|
// the id binds the ask to the composition and to its number (review round 1, items 3 and 4)
|
|
if again := BackupAsk(bkey, "ace", "mail", fNew, dgst, "x", NextBackupAsking(0), now); mustAsk(t, again).ID == mustAsk(t, b).ID {
|
|
t.Errorf("asked again under the same id")
|
|
}
|
|
if NextBackupAsking(-1) != 0 || NextBackupAsking(0) != 2 || NextBackupAsking(2) != 3 {
|
|
t.Errorf("the numbers of the askings: %d %d %d", NextBackupAsking(-1), NextBackupAsking(0), NextBackupAsking(2))
|
|
}
|
|
if BackupAskDigest(b) != dgst {
|
|
t.Errorf("the ask names composition %q, want %q", BackupAskDigest(b), dgst)
|
|
}
|
|
if BackupAnswerWords("again", 0) == BackupAnswerWords("again", 1) {
|
|
t.Errorf("two answers of the same option are one want value")
|
|
}
|
|
c := BackupCondition("ace", "mail", fNew, "no space left", true)
|
|
if _, err := occ.EncodeCondition(c); err != nil {
|
|
t.Errorf("the backup condition is refused: %v", err)
|
|
}
|
|
// ADR 0546 clauses 7 and 8: a put's ask is valid at the record ask's key; for the bus its yes approves and binds the act,
|
|
// so the view cannot answer it, and it lasts no longer than an approving ask may.
|
|
put := puts.Put{FP: fNew, Why: "the bus's user list", Hold: true}
|
|
d := ComposedDigest{Digest: dgst, Judgement: JudgedCannot, Words: "the have names no composition"}
|
|
for _, bus := range []bool{false, true} {
|
|
a, err := PutAsk(rkey, "ace", "mail", put, d, bus, now)
|
|
if err != nil {
|
|
t.Fatalf("bus %v: %v", bus, err)
|
|
}
|
|
if err := occ.Check(mustAsk(t, a), now); err != nil {
|
|
t.Errorf("bus %v: the router refuses the put's ask: %v", bus, err)
|
|
}
|
|
if _, err := EncodeAsk(rkey, a); err != nil {
|
|
t.Errorf("bus %v: the put's ask cannot be written: %v", bus, err)
|
|
}
|
|
viewed := viewstate.ViewAnswerFits(viewstate.ViewAnswer{Asking: mustAsk(t, a).ID, Option: OptionYes}, rkey, a)
|
|
yes := mustAsk(t, a).Options[0]
|
|
// mesh-sdk gives no refusal by level any more: the teller refuses an option a view cannot answer (its level is not
|
|
// acknowledge), as it did through the sdk before.
|
|
if viewed == nil {
|
|
if o, ok := occ.OptionOf(mustAsk(t, a), OptionYes); ok && o.Level != "" && o.Level != occ.Acknowledge {
|
|
viewed = fmt.Errorf("only a channel that proves who answered takes the option %s", o.ID)
|
|
}
|
|
}
|
|
switch {
|
|
case !strings.Contains(mustAsk(t, a).Explanation, "the bus's user list"):
|
|
t.Errorf("bus %v: the put's ask does not say why: %s", bus, mustAsk(t, a).Explanation)
|
|
case bus && (yes.Level != occ.Approve || yes.Binds == "" || viewed == nil || expiresAt(t, a).After(now.Add(occ.ApproveLasts))):
|
|
t.Errorf("bus: the yes is %+v, the view's answer %v, expiring %s", yes, viewed, expiresAt(t, a))
|
|
case !bus && (yes.Level != occ.Acknowledge || viewed != nil):
|
|
t.Errorf("a recorded module: the yes is %+v, the view's answer %v", yes, viewed)
|
|
}
|
|
later := put
|
|
later.At = now.Add(time.Minute)
|
|
b, _ := PutAsk(rkey, "ace", "mail", later, d, bus, now)
|
|
if mustAsk(t, b).ID == mustAsk(t, a).ID || b.Asking != PutAsking(later) {
|
|
t.Errorf("bus %v: a later put's ask is asking %d under id %q, the earlier's %q", bus, b.Asking, mustAsk(t, b).ID, mustAsk(t, a).ID)
|
|
}
|
|
if err := occ.Performs(yes, PutAct("ace", "mail", fNew, dgst)); err != nil {
|
|
t.Errorf("bus %v: the yes does not perform the act it bound: %v", bus, err)
|
|
}
|
|
if err := occ.Performs(yes, PutAct("ace", "mail", fNew, dig(fOld))); bus && err == nil {
|
|
t.Errorf("bus: the yes performs another composition's act")
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1945/AC21: the condition of a module on neither list is urgent, names the module, the node and the rule holding its want back,
|
|
// under the assigner's own name keyed by node and module, and is cleared under the same key.
|
|
func TestTheNeitherConditionNamesModuleNodeAndRule(t *testing.T) {
|
|
c := NeitherCondition("g14", "i3status-rust", Holding{Rule: mwc.RuleProviders, Words: "no provider of x11-display runs healthy on g14 or at its canary"}, true)
|
|
if _, err := occ.EncodeCondition(c); err != nil {
|
|
t.Fatalf("the condition is refused: %v", err)
|
|
}
|
|
if c.State != occ.StateRaised || c.Severity != occ.SeverityUrgent || c.Subject.ID != "i3status-rust" || c.Subject.Machine != "g14" ||
|
|
!strings.Contains(c.Explanation, "x11-display") || !strings.Contains(c.Explanation, "rule 5") || c.Key != "mesh-delivery-assigner.neither.g14.i3status-rust" {
|
|
t.Errorf("condition = %+v", c)
|
|
}
|
|
if got := NeitherCondition("g14", "i3status-rust", Holding{}, false); got.State != occ.StateCleared || got.Key != c.Key {
|
|
t.Errorf("cleared = %+v", got)
|
|
}
|
|
}
|
|
|
|
// mustAsk is the router's ask an asks-state value carries.
|
|
func mustAsk(t *testing.T, a asks.Ask) oc.Ask {
|
|
t.Helper()
|
|
ask, err := AskOf(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return ask
|
|
}
|
|
|
|
func expiresAt(t *testing.T, a asks.Ask) time.Time {
|
|
t.Helper()
|
|
return ExpiresAt(mustAsk(t, a))
|
|
}
|