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:
2026-10-20 01:30:54 +00:00
parent 34f585106f
commit 14e9d56a22
13 changed files with 236 additions and 76 deletions
+1 -1
View File
@@ -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=
+1 -1
View File
@@ -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
)
+2
View File
@@ -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())
}
}
+1 -1
View File
@@ -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
)
+2
View File
@@ -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=
+1 -1
View File
@@ -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
)
+2
View File
@@ -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