Ask under each asking's own router id, spell the heads in lower case as the gate does, show a removed have plainly; pin mesh-sdk ba684e5 (review round 1: G2, T1, issue 1370)
This commit is contained in:
@@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9 h1:XL1dbtSl
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 h1:kvtV/SPsdRVUL/BZ3xxGS6U8YrXy/t7TRBjVrD6VLw8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b h1:U7++vBnHNWRYpwdbO+Tx03HUcEzDFLBciNDe0fNiXYc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-indexer
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019223929-58373d65b47a h1:Vjk2Kq+g
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019223929-58373d65b47a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 h1:kvtV/SPsdRVUL/BZ3xxGS6U8YrXy/t7TRBjVrD6VLw8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b h1:U7++vBnHNWRYpwdbO+Tx03HUcEzDFLBciNDe0fNiXYc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -69,29 +69,44 @@ func (t *Teller) logf(format string, args ...any) {
|
||||
}
|
||||
}
|
||||
|
||||
// seen is what the teller keeps beside an ask it made: the gate key it was made for and the revision of that key it was
|
||||
// made at, so a replay of the revision asks nothing again.
|
||||
// seen is what the teller keeps beside an ask it made. Under `key.<router id>` it names the gate key the asking was made for;
|
||||
// under `gate.<gate key>` it names the router id of the latest asking of that key and the revision of the key it was made
|
||||
// at, so a replay of the revision asks nothing again and an overwrite with a new id is known for a new asking.
|
||||
type seen struct {
|
||||
Key string `json:"key"`
|
||||
Key string `json:"key,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Rev uint64 `json:"rev"`
|
||||
}
|
||||
|
||||
func recordKey(id string) string { return "key." + id }
|
||||
func keptKey(id string) string { return "ask." + id }
|
||||
func condKey(id string) string { return "cond." + id }
|
||||
func gateRecordKey(key string) string { return "gate." + key }
|
||||
func recordKey(id string) string { return "key." + id }
|
||||
func keptKey(id string) string { return "ask." + id }
|
||||
func condKey(id string) string { return "cond." + id }
|
||||
|
||||
func (t *Teller) seenOf(id string) (seen, *stdio.StateEntry, error) {
|
||||
e, err := t.Store.Get(recordKey(id))
|
||||
// condID is what a condition of an ask is kept and named by: the gate key it was asked for, so the asking that follows a bad
|
||||
// answer clears the condition of the one before it. An id the teller holds no key for is its own.
|
||||
func (t *Teller) condID(id string) string {
|
||||
if s, e, _ := t.seenOf(id); e != nil && s.Key != "" {
|
||||
return s.Key
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (t *Teller) seenAt(rk string) (seen, *stdio.StateEntry, error) {
|
||||
e, err := t.Store.Get(rk)
|
||||
if err != nil || e == nil {
|
||||
return seen{}, nil, err
|
||||
}
|
||||
var s seen
|
||||
if err := json.Unmarshal(e.Value, &s); err != nil {
|
||||
return seen{}, nil, fmt.Errorf("the record %s cannot be read: %w", recordKey(id), err)
|
||||
return seen{}, nil, fmt.Errorf("the record %s cannot be read: %w", rk, err)
|
||||
}
|
||||
return s, e, nil
|
||||
}
|
||||
|
||||
// seenOf is the record of a router id: the gate key it was asked for.
|
||||
func (t *Teller) seenOf(id string) (seen, *stdio.StateEntry, error) { return t.seenAt(recordKey(id)) }
|
||||
|
||||
func (t *Teller) keptOf(id string) (*asker.Kept, uint64, error) {
|
||||
e, err := t.Store.Get(keptKey(id))
|
||||
if err != nil || e == nil {
|
||||
@@ -107,21 +122,27 @@ func (t *Teller) keptOf(id string) (*asker.Kept, uint64, error) {
|
||||
// open says whether an ask is made and neither answered nor ended.
|
||||
func open(k *asker.Kept) bool { return k != nil && !k.Acted && k.Ended == "" }
|
||||
|
||||
// note keeps that the asking with this router id is the latest of the gate key, made at revision rev of it.
|
||||
func (t *Teller) note(id, key string, rev uint64, prior *stdio.StateEntry) error {
|
||||
s := seen{Key: key, Rev: rev}
|
||||
if prior == nil {
|
||||
_, err := t.Store.Create(recordKey(id), s)
|
||||
if _, err := t.Store.Create(recordKey(id), seen{Key: key, Rev: rev}); err != nil && !errors.Is(err, stdio.ErrChanged) {
|
||||
return err
|
||||
}
|
||||
_, err := t.Store.Update(recordKey(id), s, prior.Revision)
|
||||
g := seen{ID: id, Rev: rev}
|
||||
if prior == nil {
|
||||
_, err := t.Store.Create(gateRecordKey(key), g)
|
||||
return err
|
||||
}
|
||||
_, err := t.Store.Update(gateRecordKey(key), g, prior.Revision)
|
||||
return err
|
||||
}
|
||||
|
||||
// OnAsk handles the gate's asks key put at revision rev: the ask it holds is published to the router once per
|
||||
// revision of its key. A key whose value is not the ask its name says, one already answered, and a revision the teller
|
||||
// has handled are left. An ask still open and unchanged is only noted (and its warrant read from the router, in case it
|
||||
// came while the teller was away); one whose words changed is cancelled and asked afresh; one that ended, or that the
|
||||
// teller cancelled, is asked again under the same id when the gate writes its key again (ADR 0441 §1).
|
||||
// OnAsk handles the gate's asks key put at revision rev. Each asking has its own router id, which is the value's ask.id and
|
||||
// never one recomputed from the key (mesh-sdk go/delivery Ask.Asking, AskingID): the router keeps an ended ask and drops a
|
||||
// second ask under its id. So the ask is published once per id; a key overwritten at a higher revision under a new id is a
|
||||
// new asking, and the earlier asking's router ask is cancelled first. A key whose value is not the ask its name says, one
|
||||
// already answered, and a revision the teller has handled are left. An ask still open and unchanged is only noted (and its
|
||||
// warrant read from the router, in case it came while the teller was away). A value that changed its words under the id
|
||||
// it was asked under is cancelled and not asked again: the router would drop it, so the gate must give it a new asking.
|
||||
func (t *Teller) OnAsk(key string, rev uint64, value []byte) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
@@ -141,41 +162,42 @@ func (t *Teller) OnAsk(key string, rev uint64, value []byte) {
|
||||
} else if has {
|
||||
return
|
||||
}
|
||||
prior, srec, err := t.seenOf(id)
|
||||
prior, grec, err := t.seenAt(gateRecordKey(key))
|
||||
if err != nil {
|
||||
t.logf("%s: %v", key, err)
|
||||
return
|
||||
}
|
||||
k, krev, err := t.keptOf(id)
|
||||
if err != nil {
|
||||
t.logf("%s: %v", key, err)
|
||||
return
|
||||
}
|
||||
if srec != nil && rev <= prior.Rev {
|
||||
if open(k) {
|
||||
t.catchUp(id)
|
||||
if grec != nil && rev <= prior.Rev {
|
||||
if k, _, err := t.keptOf(prior.ID); err == nil && open(k) {
|
||||
t.catchUp(prior.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
if grec != nil && prior.ID != id {
|
||||
// A new asking of the group at these heads: the earlier one is taken back.
|
||||
t.cancelOpen(key, prior.ID)
|
||||
}
|
||||
k, _, err := t.keptOf(id)
|
||||
if err != nil {
|
||||
t.logf("%s: %v", key, err)
|
||||
return
|
||||
}
|
||||
if k != nil {
|
||||
if open(k) && k.Ask.Digest() == ga.Ask.Digest() {
|
||||
_ = t.note(id, key, rev, srec)
|
||||
switch {
|
||||
case open(k) && k.Ask.Digest() == ga.Ask.Digest():
|
||||
_ = t.note(id, key, rev, grec)
|
||||
t.catchUp(id)
|
||||
return
|
||||
}
|
||||
if open(k) {
|
||||
if err := t.Asker.Cancel(id); err != nil {
|
||||
t.logf("%s: the ask %s changed and could not be cancelled, so it is left: %v", key, id, err)
|
||||
return
|
||||
}
|
||||
t.logf("%s: the ask %s changed at revision %d: the old one is cancelled and the new one asked", key, id, rev)
|
||||
}
|
||||
if err := t.Store.Delete(keptKey(id)); err != nil {
|
||||
t.logf("%s: the earlier ask %s (revision %d of its record) could not be forgotten: %v", key, id, krev, err)
|
||||
return
|
||||
case open(k):
|
||||
t.cancelOpen(key, id)
|
||||
_ = t.note(id, key, rev, grec)
|
||||
t.logf("%s: the ask %s changed its words at revision %d under the same id: cancelled, and not asked again, for the router drops a second ask under an id (the gate gives a new asking a new id)", key, id, rev)
|
||||
default:
|
||||
_ = t.note(id, key, rev, grec)
|
||||
t.logf("%s: the ask %s ended, and the gate wrote it again under the same id at revision %d: not asked again, the router would drop it", key, id, rev)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := t.note(id, key, rev, srec); err != nil {
|
||||
if err := t.note(id, key, rev, grec); err != nil {
|
||||
t.logf("%s: the record of the ask %s could not be kept, so it is left: %v", key, id, err)
|
||||
return
|
||||
}
|
||||
@@ -187,19 +209,12 @@ func (t *Teller) OnAsk(key string, rev uint64, value []byte) {
|
||||
}
|
||||
return
|
||||
}
|
||||
t.logf("%s at revision %d: the ask %s is asked of the operator", key, rev, id)
|
||||
t.logf("%s at revision %d: the ask %s is asked of the operator (asking %d)", key, rev, id, ga.Asking)
|
||||
}
|
||||
|
||||
// OnAskGone handles the gate's asks key taken away (a withdrawn ask): the ask is cancelled at the router, and an answer
|
||||
// to it merges nothing.
|
||||
func (t *Teller) OnAskGone(key string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if _, _, _, err := delivery.ParseAskKey(key); err != nil {
|
||||
t.logf("the gate's key %s was taken away and is no ask key, and is left: %v", key, err)
|
||||
return
|
||||
}
|
||||
id := delivery.AskID(key)
|
||||
// cancelOpen takes an open ask back at the router and marks it ended here, since the router's word that it ended may come
|
||||
// later. An ask that is not open is left.
|
||||
func (t *Teller) cancelOpen(key, id string) {
|
||||
k, krev, err := t.keptOf(id)
|
||||
if err != nil {
|
||||
t.logf("%s: %v", key, err)
|
||||
@@ -209,15 +224,33 @@ func (t *Teller) OnAskGone(key string) {
|
||||
return
|
||||
}
|
||||
if err := t.Asker.Cancel(id); err != nil {
|
||||
t.logf("%s: the withdrawn ask %s could not be cancelled: %v", key, id, err)
|
||||
t.logf("%s: the ask %s could not be cancelled: %v", key, id, err)
|
||||
return
|
||||
}
|
||||
// The router's word that it ended may come later: until it does, the ask is not open here.
|
||||
k.Ended = asks.OutcomeCancelled
|
||||
if _, err := t.Store.Update(keptKey(id), k, krev); err != nil && !errors.Is(err, stdio.ErrChanged) {
|
||||
t.logf("%s: the cancelled ask %s could not be marked: %v", key, id, err)
|
||||
}
|
||||
t.logf("%s: the gate withdrew the ask %s: cancelled", key, id)
|
||||
t.logf("%s: the ask %s is cancelled", key, id)
|
||||
}
|
||||
|
||||
// OnAskGone handles the gate's asks key taken away (a withdrawn ask): the latest asking's router ask is cancelled, and an
|
||||
// answer to it merges nothing.
|
||||
func (t *Teller) OnAskGone(key string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if _, _, _, err := delivery.ParseAskKey(key); err != nil {
|
||||
t.logf("the gate's key %s was taken away and is no ask key, and is left: %v", key, err)
|
||||
return
|
||||
}
|
||||
prior, grec, err := t.seenAt(gateRecordKey(key))
|
||||
if err != nil || grec == nil {
|
||||
if err != nil {
|
||||
t.logf("%s: %v", key, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.cancelOpen(key, prior.ID)
|
||||
}
|
||||
|
||||
// catchUp reads the router's record of an ask the teller made and hands its warrant on, as the router's event would:
|
||||
@@ -249,7 +282,8 @@ func mergeAct(ask, option string, heads map[string]string) asks.Act {
|
||||
}
|
||||
|
||||
// headsOf is the heads an ask covers as an act names them: a delivery's id, `owner/repository@<twelve characters of its
|
||||
// commit>` (mesh-catalog modules/mesh-delivery model.go Delivery.ID), and its whole commit.
|
||||
// commit>` with the repository in lower case, as the gate's IDOf spells it (mesh-catalog modules/mesh-delivery-gate model.go),
|
||||
// and its whole commit.
|
||||
func headsOf(heads []delivery.Head) map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, h := range heads {
|
||||
@@ -257,7 +291,7 @@ func headsOf(heads []delivery.Head) map[string]string {
|
||||
if len(short) > 12 {
|
||||
short = short[:12]
|
||||
}
|
||||
out[h.Repository+"@"+short] = h.Head
|
||||
out[strings.ToLower(h.Repository)+"@"+short] = h.Head
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -290,7 +324,11 @@ func (t *Teller) Answer(o asks.Option, w asks.Warrant) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("the gate's ask %s is not an ask: %w", s.Key, err)
|
||||
}
|
||||
if ga.Ask.ID != w.Ask || ga.Ask.Digest() != w.AskDigest {
|
||||
if ga.Ask.ID != w.Ask {
|
||||
t.logf("%s: the ask %s was answered, and the gate has asked again under %s: an answer to an earlier asking merges nothing", s.Key, w.Ask, ga.Ask.ID)
|
||||
return nil
|
||||
}
|
||||
if ga.Ask.Digest() != w.AskDigest {
|
||||
return fmt.Errorf("the gate's ask %s is no longer the ask the operator answered (its digest is %s, the warrant's %s)",
|
||||
s.Key, ga.Ask.Digest(), w.AskDigest)
|
||||
}
|
||||
@@ -345,10 +383,11 @@ func oneLine(err error) string {
|
||||
// conditionKey is the key of the condition about an ask.
|
||||
func conditionKey(id string) string { return delivery.TellerModule + ".merge-ask." + id }
|
||||
|
||||
func (t *Teller) raise(id, headline, why string) {
|
||||
func (t *Teller) raise(askID, headline, why string) {
|
||||
if t.Say == nil {
|
||||
return
|
||||
}
|
||||
id := t.condID(askID)
|
||||
if _, err := t.Store.Create(condKey(id), seen{}); err != nil {
|
||||
if errors.Is(err, stdio.ErrChanged) {
|
||||
return // raised already
|
||||
@@ -366,10 +405,11 @@ func (t *Teller) raise(id, headline, why string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Teller) clear(id string) {
|
||||
func (t *Teller) clear(askID string) {
|
||||
if t.Say == nil {
|
||||
return
|
||||
}
|
||||
id := t.condID(askID)
|
||||
e, err := t.Store.Get(condKey(id))
|
||||
if err != nil || e == nil {
|
||||
return
|
||||
|
||||
@@ -97,6 +97,11 @@ type bed struct {
|
||||
pubs []published
|
||||
conds []asks.Condition
|
||||
records map[string]string
|
||||
// seenIDs are the ids the fake router has taken an ask under, and dropped the asks it took a second time under: the
|
||||
// messenger keeps an ask after it ends and returns for an ask whose id it holds (mesh-catalog modules/messenger
|
||||
// cmd/messenger/asks.go:487-489, keepAsk), so a reused id reaches no phone.
|
||||
seenIDs map[string]bool
|
||||
dropped []string
|
||||
hear func(stdio.Envelope) error
|
||||
logs strings.Builder
|
||||
}
|
||||
@@ -104,10 +109,16 @@ type bed struct {
|
||||
func newBed(t *testing.T) *bed {
|
||||
t.Helper()
|
||||
b := &bed{store: &memStore{entries: map[string]*stdio.StateEntry{}}, answers: &memAnswers{written: map[string][]byte{}},
|
||||
gate: memGate{}, records: map[string]string{}}
|
||||
gate: memGate{}, records: map[string]string{}, seenIDs: map[string]bool{}}
|
||||
b.client = &asker.Client{Module: delivery.TellerModule, State: b.store, Now: func() time.Time { return now },
|
||||
Publish: func(subject string, body any, id string) error {
|
||||
raw, err := json.Marshal(body)
|
||||
if subject == asks.AskSubject(delivery.TellerModule) {
|
||||
if b.seenIDs[id] {
|
||||
b.dropped = append(b.dropped, id)
|
||||
}
|
||||
b.seenIDs[id] = true
|
||||
}
|
||||
b.pubs = append(b.pubs, published{subject, raw, id})
|
||||
return err
|
||||
},
|
||||
@@ -139,12 +150,19 @@ func newBed(t *testing.T) *bed {
|
||||
// and :63-65 mergeAct), under the id its key gives (mesh-sdk go/delivery AskID). boundTo are the heads the options bind,
|
||||
// which are the ask's own unless a test says another.
|
||||
func gateAsk(t *testing.T, heads, boundTo []delivery.Head) (key string, value []byte, ask delivery.Ask) {
|
||||
t.Helper()
|
||||
return gateAskN(t, heads, boundTo, 1)
|
||||
}
|
||||
|
||||
// gateAskN is the asking-th writing of the group's ask at these heads: its router id is AskingID(key, asking) (mesh-sdk
|
||||
// go/delivery Ask.Asking), the first asking's the key's own.
|
||||
func gateAskN(t *testing.T, heads, boundTo []delivery.Head, asking int) (key string, value []byte, ask delivery.Ask) {
|
||||
t.Helper()
|
||||
key, err := delivery.MergeAskKey("g", heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := delivery.AskID(key)
|
||||
id := delivery.AskingID(key, asking)
|
||||
a := asks.Ask{ID: id, Headline: "Merge into main: novox/mesh-delivery#12", Explanation: "Merges two pull requests.\nReview: approved.",
|
||||
Who: asks.Operator, Expires: now.Add(asks.ApproveLasts - 10*time.Minute), OnExpiry: "the merge stays held", About: "delivery.mesh-delivery.12"}
|
||||
for _, o := range []struct{ id, label, does string }{{"approve", "Approve", "merges"}, {"decline", "Decline", "merges nothing"}} {
|
||||
@@ -154,7 +172,7 @@ func gateAsk(t *testing.T, heads, boundTo []delivery.Head) (key string, value []
|
||||
}
|
||||
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
|
||||
}
|
||||
ask = delivery.Ask{Group: "g", Heads: heads, Ask: a}
|
||||
ask = delivery.Ask{Group: "g", Heads: heads, Asking: asking, Ask: a}
|
||||
value, err = delivery.EncodeAsk(ask)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -287,7 +305,7 @@ func TestAWarrantThatDoesNotHoldWritesNoAnswerAndRaisesTheCondition(t *testing.T
|
||||
if len(b.answers.written) != 0 {
|
||||
t.Errorf("an answer was written: %v", b.answers.written)
|
||||
}
|
||||
if len(b.conds) != 1 || b.conds[0].State != asks.StateRaised || b.conds[0].Subject.ID != delivery.AskID(key) ||
|
||||
if len(b.conds) != 1 || b.conds[0].State != asks.StateRaised || b.conds[0].Subject.ID != key ||
|
||||
!strings.HasPrefix(b.conds[0].Explanation, asks.NeedsYou) {
|
||||
t.Fatalf("conditions = %+v", b.conds)
|
||||
}
|
||||
@@ -305,8 +323,8 @@ func TestAGoodAnswerClearsTheConditionOfAnAskAskedAgain(t *testing.T) { // 1343/
|
||||
if len(b.conds) != 1 || b.conds[0].State != asks.StateRaised {
|
||||
t.Fatalf("conditions = %+v", b.conds)
|
||||
}
|
||||
// The gate writes the ask again, right this time, at a new revision of its key: asked again under the same id.
|
||||
_, good, gaGood := gateAsk(t, twoHeads(), twoHeads())
|
||||
// The gate writes the ask again, right this time, at a new revision of its key: the second asking, under a new id.
|
||||
_, good, gaGood := gateAskN(t, twoHeads(), twoHeads(), 2)
|
||||
b.gate[key] = good
|
||||
b.t.OnAsk(key, 2, good)
|
||||
if n := len(b.askedOf(asks.AskSubject(delivery.TellerModule))); n != 2 {
|
||||
@@ -334,7 +352,45 @@ func TestAWarrantComingWhileTheTellerWasAwayIsReadFromTheRouter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAskWhoseWordsChangedIsCancelledAndAskedAgain(t *testing.T) {
|
||||
func TestARewriteWithAskingTwoCancelsAskingOneAndAsksUnderTheNewID(t *testing.T) { // G2 of review round 1
|
||||
b := newBed(t)
|
||||
key, one, g1 := gateAskN(t, twoHeads(), twoHeads(), 1)
|
||||
b.gate[key] = one
|
||||
b.t.OnAsk(key, 1, one)
|
||||
_, two, g2 := gateAskN(t, twoHeads(), twoHeads(), 2)
|
||||
if g1.Ask.ID == g2.Ask.ID {
|
||||
t.Fatal("the two askings share an id")
|
||||
}
|
||||
b.gate[key] = two
|
||||
b.t.OnAsk(key, 2, two)
|
||||
cancels := b.askedOf(asks.CancelSubject(delivery.TellerModule))
|
||||
if len(cancels) != 1 || string(cancels[0].body) != `{"id":"`+g1.Ask.ID+`"}` {
|
||||
t.Fatalf("cancels = %+v, want asking 1's router ask", cancels)
|
||||
}
|
||||
asked := b.askedOf(asks.AskSubject(delivery.TellerModule))
|
||||
if len(asked) != 2 || asked[1].id != g2.Ask.ID {
|
||||
t.Fatalf("asks = %+v, want the second under %s", asked, g2.Ask.ID)
|
||||
}
|
||||
if len(b.dropped) != 0 {
|
||||
t.Errorf("the router dropped asks under a reused id: %v", b.dropped)
|
||||
}
|
||||
// The same revision again, and a warrant for the first asking (the gate has moved on), change nothing.
|
||||
b.t.OnAsk(key, 2, two)
|
||||
b.deliver(t, warrantFor(g1.Ask, "approve"))
|
||||
if len(b.answers.written) != 0 || len(b.askedOf(asks.AskSubject(delivery.TellerModule))) != 2 {
|
||||
t.Errorf("answers %v asks %d", b.answers.written, len(b.askedOf(asks.AskSubject(delivery.TellerModule))))
|
||||
}
|
||||
// The warrant of the second names it: its answer's warrant is the router id and the time.
|
||||
w := warrantFor(g2.Ask, "approve")
|
||||
b.deliver(t, w)
|
||||
raw, ok := b.answers.written[delivery.AnswerKey(key)]
|
||||
a, err := delivery.DecodeAnswer(raw)
|
||||
if !ok || err != nil || a.Warrant != g2.Ask.ID+"@"+w.At.UTC().Format(time.RFC3339Nano) {
|
||||
t.Errorf("answer %+v (%v) err %v", a, ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnAskWhoseWordsChangedUnderTheSameIDIsCancelledAndNotAskedAgain(t *testing.T) {
|
||||
b := newBed(t)
|
||||
key, value, ga := gateAsk(t, twoHeads(), twoHeads())
|
||||
b.t.OnAsk(key, 1, value)
|
||||
@@ -348,8 +404,35 @@ func TestAnAskWhoseWordsChangedIsCancelledAndAskedAgain(t *testing.T) {
|
||||
if n := len(b.askedOf(asks.CancelSubject(delivery.TellerModule))); n != 1 {
|
||||
t.Errorf("cancels = %d", n)
|
||||
}
|
||||
if n := len(b.askedOf(asks.AskSubject(delivery.TellerModule))); n != 2 {
|
||||
t.Errorf("asks = %d", n)
|
||||
if n := len(b.askedOf(asks.AskSubject(delivery.TellerModule))); n != 1 || len(b.dropped) != 0 {
|
||||
t.Errorf("asks = %d dropped %v: the router would drop a second ask under the id", n, b.dropped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACapitalisedRepositoryIsSpelledInLowerCaseInTheAct(t *testing.T) { // T1 of review round 1
|
||||
b := newBed(t)
|
||||
heads := []delivery.Head{{Repository: "novox/Mesh-Delivery", Number: 12, Head: headA}}
|
||||
key, err := delivery.MergeAskKey("g", heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := delivery.AskID(key)
|
||||
// The act as the gate binds it: its IDOf lowercases the repository (mesh-catalog modules/mesh-delivery-gate model.go).
|
||||
binds, err := asks.ActDigest(asks.Act{"verb": "mesh-delivery.merge", "ask": id, "option": "approve", "heads": "novox/mesh-delivery@" + headA[:12] + "=" + headA})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
a := asks.Ask{ID: id, Headline: "Merge into main: Mesh-Delivery#12", Explanation: "Merges.", Who: asks.Operator, Expires: now.Add(asks.ApproveLasts - 10*time.Minute),
|
||||
OnExpiry: "the merge stays held", Options: []asks.Option{{ID: "approve", Label: "Approve", Does: "merges", Level: asks.Approve, Binds: binds}}}
|
||||
value, err := delivery.EncodeAsk(delivery.Ask{Group: "g", Heads: heads, Ask: a})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.gate[key] = value
|
||||
b.t.OnAsk(key, 1, value)
|
||||
b.deliver(t, warrantFor(a, "approve"))
|
||||
if _, ok := b.answers.written[delivery.AnswerKey(key)]; !ok {
|
||||
t.Errorf("no answer for the gate's spelling: %s", b.logs.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-teller
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 h1:kvtV/SPsdRVUL/BZ3xxGS6U8YrXy/t7TRBjVrD6VLw8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b h1:U7++vBnHNWRYpwdbO+Tx03HUcEzDFLBciNDe0fNiXYc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -3,7 +3,7 @@ module mesh-delivery-view
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b
|
||||
github.com/nats-io/nats-server/v2 v2.11.17
|
||||
github.com/nats-io/nats.go v1.54.0
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019223929-58373d65b47a h1:Vjk2Kq+g
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019223929-58373d65b47a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69 h1:kvtV/SPsdRVUL/BZ3xxGS6U8YrXy/t7TRBjVrD6VLw8=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019230201-5786ba190c69/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b h1:U7++vBnHNWRYpwdbO+Tx03HUcEzDFLBciNDe0fNiXYc=
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020011849-ba684e54976b/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
|
||||
|
||||
@@ -130,3 +130,27 @@ func TestTheSnapshotsJSONCarriesTheAsks(t *testing.T) { // 1345/AC2
|
||||
t.Errorf("json %s: %+v %v", raw, back, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHaveOfARemovedModuleIsShownRemovedNotLeftBehind(t *testing.T) { // 1370: the node-engine writes phase removed
|
||||
m := New()
|
||||
fill(m, haveE(t, "novox", "mesh-delivery", have(t, "b1", delivery.PhaseRemoved, delivery.HealthOK, ""), t0),
|
||||
haveE(t, "novox", "stray", have(t, "b2", delivery.PhaseRunning, delivery.HealthOK, ""), t0))
|
||||
s := m.Snapshot()
|
||||
if r := find(t, s, "novox", "mesh-delivery"); r.Status != StatusRemoved {
|
||||
t.Errorf("row = %+v, want status %q", r, StatusRemoved)
|
||||
}
|
||||
if r := find(t, s, "novox", "stray"); r.Status != StatusLeftBehind {
|
||||
t.Errorf("row = %+v, want left behind", r)
|
||||
}
|
||||
var out bytes.Buffer
|
||||
Table(&out, s)
|
||||
line := ""
|
||||
for _, l := range strings.Split(out.String(), "\n") {
|
||||
if strings.Contains(l, "mesh-delivery") {
|
||||
line = l
|
||||
}
|
||||
}
|
||||
if !strings.Contains(line, "removed") || strings.Contains(line, "left behind") {
|
||||
t.Errorf("table line %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,9 +111,9 @@ func rowText(r Row) []string {
|
||||
if r.Error != "" {
|
||||
what = "unreadable: " + r.Error
|
||||
}
|
||||
if r.Status == StatusLeftBehind && what == "" {
|
||||
if (r.Status == StatusLeftBehind || r.Status == StatusRemoved) && what == "" {
|
||||
what = r.Status
|
||||
} else if r.Status == StatusLeftBehind {
|
||||
} else if r.Status == StatusLeftBehind || r.Status == StatusRemoved {
|
||||
what = r.Status + "; " + what
|
||||
}
|
||||
return []string{r.Node, r.Module, r.Assigned, r.Have, r.Phase, r.Health, since(r.Since), r.Detail, what}
|
||||
|
||||
@@ -396,6 +396,10 @@ func (m *Model) Snapshot() Snapshot {
|
||||
}
|
||||
node, module, _, _ := delivery.ParseHaveKey(key)
|
||||
r := Row{Node: node, Module: module, Status: StatusLeftBehind, Since: h.at, Error: h.err}
|
||||
if h.err == "" && h.have.Phase == delivery.PhaseRemoved {
|
||||
// The node-engine says it took the module away: plainly removed, not left behind.
|
||||
r.Status = StatusRemoved
|
||||
}
|
||||
if h.err == "" {
|
||||
r.Have, r.Phase, r.Health, r.Detail = h.have.Build, string(h.have.Phase), string(h.have.Health), h.have.Detail
|
||||
}
|
||||
@@ -567,6 +571,7 @@ const (
|
||||
const (
|
||||
StatusAssigned = "assigned"
|
||||
StatusLeftBehind = "left behind"
|
||||
StatusRemoved = "removed"
|
||||
)
|
||||
|
||||
// askRows are the readable asks with no answer under their key, ordered by key: each with its headline, its words, the
|
||||
|
||||
Reference in New Issue
Block a user