diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go index bcadf13..b4c5c4b 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/integration_test.go @@ -165,6 +165,8 @@ const ( tellerAnswers = "mesh-delivery-teller_answers" tellerAsked = "mesh-delivery-teller_asked" gateAsksKV = "mesh-delivery-gate_asks" + assignerAsks = "mesh-delivery-assigner_asks" + viewAnswers = "mesh-delivery-view_answers" seatAsk = "mesh.seat.operator-channel.accept.ask.mesh-delivery-teller" seatCancel = "mesh.seat.operator-channel.accept.cancel.mesh-delivery-teller" seatCondition = "mesh.seat.operator-channel.accept.condition.mesh-delivery-teller" @@ -174,7 +176,9 @@ const ( func fullConf(dir string) string { teller := append(append(append(keepsOf(tellerAnswers), keepsOf(tellerAsked)...), readOf(gateAsksKV)...), seatAsk, seatCancel, seatCondition, "$JS.API.DIRECT.GET.KV_asks.$KV.asks.mesh-delivery-teller.>") - view := append(readOf(tellerAnswers), readOf(gateAsksKV)...) + teller = append(append(teller, readOf(assignerAsks)...), readOf(viewAnswers)...) // issue 1297 B10: the manifest's reads + view := append(append(append(readOf(tellerAnswers), readOf(gateAsksKV)...), readOf(assignerAsks)...), keepsOf(viewAnswers)...) + assigner := keepsOf(assignerAsks) tools := append(keepsOf(gateAsksKV), readOf(tellerAnswers)...) return fmt.Sprintf(` listen: 127.0.0.1:-1 @@ -184,12 +188,13 @@ accounts { jetstream: enabled users = [ { user: "ctl", password: "pw" } -%s%s%s ] +%s%s%s%s ] } } `, filepath.Join(dir, "js"), user("n1.mesh-delivery-teller", teller, []string{seatDecided}), user("n1.mesh-delivery-view", view, nil), + user("n1.mesh-delivery-assigner", assigner, nil), user("n1.node-tools", tools, nil)) } @@ -238,7 +243,7 @@ func startFull(t *testing.T) *fullBed { } ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) t.Cleanup(cancel) - for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1} { + for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1, assignerAsks: delivery.AssignerAsksHistory, viewAnswers: delivery.ViewAnswersHistory} { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil { t.Fatal(err) } @@ -468,3 +473,75 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1 say(asks.Warrant{Ask: ga2.Ask.ID, Asker: delivery.TellerModule, Outcome: asks.OutcomeExpired, At: now, AskDigest: ga2.Ask.Digest()}) lastAsk(held.Ask.ID, second) } + +// TestTellerFillsAnAskFromTheViewsAnswerOnce starts the program against the bus with the permissions above: the assigner's +// fixture hold ask is written, then the view's answer under `.view`; the answer is under the ask's own key, once +// (rewriting the view's answer does not write a second), and a view answer under the merge's key fills nothing +// (1297/B10 AC: a view answer fills the ask once; an ask needing proof is not answered from a view key). +func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC + now = time.Now().UTC() + b := startFull(t) + assigner, view := b.as(t, "n1.mesh-delivery-assigner"), b.as(t, "n1.mesh-delivery-view") + broker := filepath.Join(t.TempDir(), "broker") + cred, _ := json.Marshal(credential{URL: b.srv.ClientURL(), Node: "n1", Module: "mesh-delivery-teller", User: "n1.mesh-delivery-teller", Password: "pw"}) + if err := os.WriteFile(broker, cred, 0o600); err != nil { + t.Fatal(err) + } + env := map[string]string{"MESH_BROKER_FILE": broker} + logs := &syncBuffer{} + rn := startRunner(b.ctx, func(c context.Context) error { return run(c, func(k string) string { return env[k] }, logs) }) + t.Cleanup(func() { rn.finish(t, 10*time.Second) }) + + key, value, ask := holdAsk(t) + askKV, err := assigner.KeyValue(b.ctx, assignerAsks) + if err != nil { + t.Fatal(err) + } + viewKV, err := view.KeyValue(b.ctx, viewAnswers) + if err != nil { + t.Fatal(err) + } + answers, err := b.js.KeyValue(b.ctx, tellerAnswers) + if err != nil { + t.Fatal(err) + } + time.Sleep(500 * time.Millisecond) // the program is watching + if _, err := askKV.Put(b.ctx, key, value); err != nil { + t.Fatal(err) + } + if _, err := viewKV.Put(b.ctx, key+".view", viewAnswer(t, "release", "the check was a flake")); err != nil { + t.Fatal(err) + } + var e jetstream.KeyValueEntry + for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { + if e, err = answers.Get(b.ctx, delivery.AnswerKey(key)); err == nil { + break + } + } + if e == nil { + t.Fatalf("no answer under %s: %v\n%s", key, err, logs.String()) + } + if a, err := delivery.DecodeAnswer(e.Value()); err != nil || a.Option != "release" || answeredAsking(e.Value()) != ask.ID { + t.Fatalf("answer %+v err %v", a, err) + } + // The view's answer written again changes nothing under the ask. + if _, err := viewKV.Put(b.ctx, key+".view", viewAnswer(t, "keep", "")); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + if again, err := answers.Get(b.ctx, delivery.AnswerKey(key)); err != nil || again.Revision() != e.Revision() { + t.Errorf("the answer was written a second time: %v", err) + } + // An answer under the merge's key fills nothing. + mkey, mvalue, _ := gateAsk(t, twoHeads(), twoHeads()) + if _, err := askKV.Put(b.ctx, mkey, mvalue); err != nil { + t.Fatal(err) + } + if _, err := viewKV.Put(b.ctx, mkey+".view", viewAnswer(t, "approve", "")); err != nil { + t.Fatal(err) + } + time.Sleep(time.Second) + if _, err := answers.Get(b.ctx, delivery.AnswerKey(mkey)); err == nil { + t.Error("the merge's ask was answered from a view key") + } +} diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go index 525e003..f6b5e09 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/main.go @@ -137,10 +137,43 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro if err := client.Listen(t.Answer, t.Said); err != nil { return fmt.Errorf("hearing the router's warrants: %w", err) } - if err := watchAsks(ctx, gate, t.OnAsk, t.OnAskGone, t.OnCurrent); err != nil { - return fmt.Errorf("watching the gate's asks: %w", err) + // The view's answers to the assigner's asks (issue 1297 B10): both buckets are the controller's to create, and a bucket not + // there is said and leaves the teller without that path rather than without the gate's asks (a wait for it needs a timer). + failed := make(chan error, 2) + assignerKV, viewKV := optionalBucket(ctx, js, delivery.AssignerAsksBucket, logger), optionalBucket(ctx, js, delivery.ViewAnswersBucket, logger) + if assignerKV != nil && viewKV != nil { + t.Assigner, t.View = kvGate{ctx: ctx, kv: assignerKV}, kvGate{ctx: ctx, kv: viewKV} + go func() { + failed <- watchAsks(ctx, viewKV, func(key string, _ uint64, _ []byte) { t.OnViewAnswer(key) }, func(string) {}, nil) + }() + go func() { + failed <- watchAsks(ctx, assignerKV, func(key string, _ uint64, _ []byte) { t.OnAssignerAsk(key) }, func(string) {}, nil) + }() } - return nil + go func() { + if err := watchAsks(ctx, gate, t.OnAsk, t.OnAskGone, t.OnCurrent); err != nil { + failed <- fmt.Errorf("watching the gate's asks: %w", err) + } + }() + select { + case <-ctx.Done(): + return nil + case err := <-failed: + if err != nil { + return fmt.Errorf("a watch ended: %w", err) + } + return nil + } +} + +// optionalBucket is a bucket the teller reads when it exists, and nil, said once, when it does not. +func optionalBucket(ctx context.Context, js jetstream.JetStream, name string, logger *log.Logger) jetstream.KeyValue { + kv, err := js.KeyValue(ctx, name) + if err != nil { + logger.Printf("the bucket %s cannot be read (%v): the view's answers fill no ask until the teller is started again with it", name, err) + return nil + } + return kv } // askedBucket is the teller's state bucket asked, `_`: what it asked of the router and which condition it diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go index 5d64273..3e36b3f 100644 --- a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/teller.go @@ -58,6 +58,10 @@ type Teller struct { Store store Answers answers Gate gateAsks + // Assigner is the assigner's bucket asks and View the view's bucket answers (issue 1297 B10); with either nil the teller + // answers nothing from the view. + Assigner gateAsks + View gateAsks // Say publishes a condition to the operator (mesh.seat.operator-channel.accept.condition.). Say func(c asks.Condition) error Logf func(format string, args ...any) diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go new file mode 100644 index 0000000..235bb4d --- /dev/null +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers.go @@ -0,0 +1,107 @@ +package main + +import ( + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The view's answers (novox/hq ADR 0528 clause 6, ADR 0437 §10, issue 1297 B10). The operator may answer, in the view, the +// asks of the assigner that need no proof of who answered (the hold of a fingerprint and the restore of a data module set back, +// mesh-sdk go/delivery AskNeedsProof); the view writes its answer under `.view` in its bucket answers, and the teller, +// which alone fills an ask's answer, takes it from there as it takes a warrant from the phone: under the ask's own key, once, +// by compare-and-set. The merge's ask needs proof (ADR 0421 §1, issue 953) and is never answered from a view key. + +// OnViewAnswer handles a key put in the view's bucket answers. A key that is no view answer key, or is the answer to an ask that +// needs proof of who answered, is left and said. +func (t *Teller) OnViewAnswer(key string) { + t.mu.Lock() + defer t.mu.Unlock() + askKey, err := delivery.ParseViewAnswerKey(key) + if err != nil { + t.logf("the view's key %s is left: %v", key, err) + return + } + t.fillFromView(askKey) +} + +// OnAssignerAsk handles a key put in the assigner's bucket asks: a view answer that came before its ask fills it now. +func (t *Teller) OnAssignerAsk(key string) { + t.mu.Lock() + defer t.mu.Unlock() + if _, err := delivery.ViewAnswerKey(key); err != nil { + t.logf("the assigner's key %s is left and never answered from the view: %v", key, err) + return + } + t.fillFromView(key) +} + +// fillFromView writes the answer to the assigner's ask at askKey from the view's answer, when both are there and readable, the +// ask is of a kind that needs no proof, the option is one the ask offers and the asking is not answered yet. +func (t *Teller) fillFromView(askKey string) { + if t.Assigner == nil || t.View == nil { + return + } + viewKey, err := delivery.ViewAnswerKey(askKey) // refuses an ask that needs proof, naming the phone + if err != nil { + t.logf("%s is not answered from the view: %v", askKey, err) + return + } + rawAnswer, found, err := t.View.Get(viewKey) + if err != nil { + t.logf("%s: the view's answer could not be read, so the ask is left: %v", askKey, err) + return + } + if !found { + return + } + rawAsk, found, err := t.Assigner.Get(askKey) + if err != nil { + t.logf("%s: the assigner's ask could not be read, so the view's answer is left: %v", askKey, err) + return + } + if !found { + return // the answer waits for its ask: OnAssignerAsk fills it when it comes + } + va, err := delivery.DecodeViewAnswer(rawAnswer) + if err != nil { + t.logf("%s: the view's answer is not readable and fills nothing: %v", viewKey, err) + return + } + ask, err := delivery.DecodeAssignerAsk(askKey, rawAsk) + if err != nil { + t.logf("%s: the assigner's ask is not readable and takes no answer: %v", askKey, err) + return + } + if err := delivery.ViewAnswerFits(va, ask); err != nil { + t.logf("%s: nothing is written: %v", askKey, err) + return + } + akey := delivery.AnswerKey(askKey) + cur, crev, has, err := t.Answers.Get(akey) + if err != nil { + t.logf("%s: the answer could not be read: %v", askKey, err) + return + } + if has && answeredAsking(cur) == ask.Ask.ID { + return // this asking is answered, from the view or elsewhere + } + words := "answered in the view (mesh-delivery-view): " + va.Option + if w := strings.TrimSpace(va.Words); w != "" { + words += ": " + w + } + body, err := delivery.EncodeAnswer(delivery.Answer{Option: va.Option, Warrant: ask.Ask.ID + "@" + t.now().UTC().Format("2006-01-02T15:04:05.999999999Z07:00"), Words: words}) + if err != nil { + t.logf("%s: the answer could not be made: %v", askKey, err) + return + } + written, err := t.Answers.Write(akey, body, crev, has) + switch { + case err != nil: + t.logf("%s: the answer could not be written: %v", askKey, err) + case !written: + t.logf("%s: another writer answered first, and the view's answer is not written", askKey) + default: + t.logf("%s: %s", askKey, words) + } +} diff --git a/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers_test.go b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers_test.go new file mode 100644 index 0000000..cc86e6d --- /dev/null +++ b/modules/mesh-delivery-teller/cmd/mesh-delivery-teller/viewanswers_test.go @@ -0,0 +1,176 @@ +package main + +import ( + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/asks" + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +const fpHeld = "src2:2222222222222222222222222222222222222222222222222222222222222222" + +// holdAsk is a key of the assigner's asks: the ask about a held fingerprint, as a fixture (in release 5 the assigner writes +// none yet; the value is `{"v":1,"ask":}`). +func holdAsk(t *testing.T) (key string, value []byte, ask asks.Ask) { + t.Helper() + key, err := delivery.HoldAskKey("plex", fpHeld) + if err != nil { + t.Fatal(err) + } + ask = asks.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: asks.Operator, + Expires: now.Add(asks.ApproveLasts), OnExpiry: "the fingerprint stays held", + Options: []asks.Option{{ID: "release", Label: "Release", Does: "lets the move go on", Level: asks.Approve}, {ID: "keep", Label: "Keep held", Does: "keeps it held", Level: asks.Approve}}} + value, err = delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: ask}) + if err != nil { + t.Fatal(err) + } + return key, value, ask +} + +func holdKey() string { + k, _ := delivery.HoldAskKey("plex", fpHeld) + return k +} + +func viewAnswer(t *testing.T, option, words string) []byte { + t.Helper() + v, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: delivery.AskID(holdKey()), Option: option, Words: words}) + if err != nil { + t.Fatal(err) + } + return v +} + +// withView gives the bed's teller the assigner's asks and the view's answers, both empty. +func withView(b *bed) (assigner, view memGate) { + assigner, view = memGate{}, memGate{} + b.t.Assigner, b.t.View = assigner, view + return assigner, view +} + +func TestAViewAnswerFillsTheAskOnce(t *testing.T) { // 1297/B10 AC + b := newBed(t) + assigner, view := withView(b) + key, value, ask := holdAsk(t) + assigner[key] = value + view[key+".view"] = viewAnswer(t, "release", "the check was a flake") + b.t.OnViewAnswer(key + ".view") + raw, ok := b.answers.written[key] + if !ok { + t.Fatalf("no answer under %s; log:\n%s", key, b.logs.String()) + } + a, err := delivery.DecodeAnswer(raw) + if err != nil || a.Option != "release" || answeredAsking(raw) != ask.ID || !strings.Contains(a.Words, "view") || !strings.Contains(a.Words, "the check was a flake") { + t.Errorf("answer = %+v, %v", a, err) + } + // The same answer heard again (the watch replays, the ask is rewritten) writes nothing more. + b.t.OnViewAnswer(key + ".view") + b.t.OnAssignerAsk(key) + if b.answers.writes != 1 { + t.Errorf("%d writes, want one", b.answers.writes) + } +} + +func TestAViewAnswerHeardBeforeItsAskFillsItWhenTheAskComes(t *testing.T) { // 1297/B10 + b := newBed(t) + assigner, view := withView(b) + key, value, _ := holdAsk(t) + view[key+".view"] = viewAnswer(t, "keep", "") + b.t.OnViewAnswer(key + ".view") + if b.answers.writes != 0 { + t.Fatalf("answered an ask the teller does not hold") + } + assigner[key] = value + b.t.OnAssignerAsk(key) + if a, err := delivery.DecodeAnswer(b.answers.written[key]); err != nil || a.Option != "keep" { + t.Errorf("answer = %+v, %v", a, err) + } +} + +func TestAnAskNeedingProofIsNotAnsweredFromAViewKey(t *testing.T) { // 1297/B10 AC + b := newBed(t) + assigner, view := withView(b) + key, value, _ := gateAsk(t, twoHeads(), twoHeads()) + // Even with the merge's ask under the assigner's bucket and a view answer under its key, nothing is written. + assigner[key] = value + view[key+".view"] = viewAnswer(t, "approve", "") + b.t.OnViewAnswer(key + ".view") + b.t.OnAssignerAsk(key) + if b.answers.writes != 0 || len(b.answers.written) != 0 { + t.Fatalf("the merge's ask was answered from the view: %v", b.answers.written) + } + if !strings.Contains(b.logs.String(), "issue 953") { + t.Errorf("the refusal does not say why:\n%s", b.logs.String()) + } +} + +func TestAViewAnswerOfAnOptionTheAskDoesNotOfferFillsNothing(t *testing.T) { // 1297/B10 + b := newBed(t) + assigner, view := withView(b) + key, value, _ := holdAsk(t) + assigner[key] = value + view[key+".view"] = viewAnswer(t, "delete", "") + b.t.OnViewAnswer(key + ".view") + if b.answers.writes != 0 || !strings.Contains(b.logs.String(), `chose "delete"`) { + t.Errorf("writes %d, log:\n%s", b.answers.writes, b.logs.String()) + } +} + +func TestAViewAnswerNeverReplacesTheAnswerOfTheSameAsking(t *testing.T) { // 1297/B10 + b := newBed(t) + assigner, view := withView(b) + key, value, ask := holdAsk(t) + assigner[key] = value + view[key+".view"] = viewAnswer(t, "keep", "") + early, err := delivery.EncodeAnswer(delivery.Answer{Option: "release", Warrant: ask.ID + "@2026-10-20T09:00:00Z", Words: "answered elsewhere"}) + if err != nil { + t.Fatal(err) + } + b.answers.written[key], b.answers.revs[key] = early, 7 + b.t.OnViewAnswer(key + ".view") + if b.answers.writes != 0 || string(b.answers.written[key]) != string(early) { + t.Errorf("the answer of the same asking was replaced") + } + // An earlier asking's answer (another router id) does not stop this one. + older, _ := delivery.EncodeAnswer(delivery.Answer{Option: "release", Warrant: "an-older-id@2026-10-19T09:00:00Z", Words: "old"}) + b.answers.written[key], b.answers.revs[key] = older, 8 + b.t.OnViewAnswer(key + ".view") + if b.answers.writes != 1 || answeredAsking(b.answers.written[key]) != ask.ID { + t.Errorf("writes %d, answer %s", b.answers.writes, b.answers.written[key]) + } +} + +func TestAnUnreadableViewAnswerOrAskFillsNothing(t *testing.T) { // 1297/B10 + b := newBed(t) + assigner, view := withView(b) + key, value, _ := holdAsk(t) + assigner[key] = []byte(`{"v":1}`) + view[key+".view"] = viewAnswer(t, "release", "") + b.t.OnViewAnswer(key + ".view") + assigner[key] = value + view[key+".view"] = []byte(`{"v":99}`) + b.t.OnViewAnswer(key + ".view") + b.t.OnViewAnswer("not-a-view-key") + b.t.OnViewAnswer("hold.x.view") + if b.answers.writes != 0 { + t.Errorf("filled from something unreadable; log:\n%s", b.logs.String()) + } +} + +func TestAStaleViewAnswerOfAnotherAskingFillsNothing(t *testing.T) { // 1297/B10 (stale answer) + b := newBed(t) + assigner, view := withView(b) + key, value, _ := holdAsk(t) + assigner[key] = value + stale, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: "an-earlier-asking", Option: "release"}) + if err != nil { + t.Fatal(err) + } + view[key+".view"] = stale + b.t.OnViewAnswer(key + ".view") + b.t.OnAssignerAsk(key) + if b.answers.writes != 0 || !strings.Contains(b.logs.String(), "an-earlier-asking") { + t.Errorf("writes %d; log:\n%s", b.answers.writes, b.logs.String()) + } +} diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 7138f28..536c6e5 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261020060201-88581d0acaef + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023132058-f625d98713e4 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/manifest_test.go b/modules/mesh-delivery-teller/manifest_test.go index 438964f..c13bd86 100644 --- a/modules/mesh-delivery-teller/manifest_test.go +++ b/modules/mesh-delivery-teller/manifest_test.go @@ -81,7 +81,8 @@ func TestManifestDeclarations(t *testing.T) { // 1240/AC1 if want := []state{{Name: "answers", History: 5}, {Name: "asked", History: 1}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } - if want := []string{"mesh-delivery-gate.asks"}; !reflect.DeepEqual(sorted(m.Reads), want) { + // The assigner's asks and the view's answers to them are read so the view's answer fills an ask that needs no proof (1297/B10). + if want := []string{"mesh-delivery-assigner.asks", "mesh-delivery-gate.asks", "mesh-delivery-view.answers"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } if want := []string{"operator-channel"}; !reflect.DeepEqual(sorted(m.Uses), want) { diff --git a/modules/mesh-delivery-teller/module.json b/modules/mesh-delivery-teller/module.json index 3c616f5..2ec994e 100644 --- a/modules/mesh-delivery-teller/module.json +++ b/modules/mesh-delivery-teller/module.json @@ -7,7 +7,9 @@ { "name": "asked", "history": 1 } ], "reads": [ - "mesh-delivery-gate.asks" + "mesh-delivery-assigner.asks", + "mesh-delivery-gate.asks", + "mesh-delivery-view.answers" ], "uses": [ "operator-channel" diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main.go index fc78740..d5c1e29 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-cli/main.go @@ -1,5 +1,6 @@ // Command mesh-delivery-cli is the operator's command for the delivery view (novox/hq ADR 0429 §3). It asks the -// view's local socket for the rows and prints them; it holds no credential and changes nothing. +// view's local socket for the rows and prints them; it holds no credential. `answer