messenger, desk-channel, telegram, mesh-config take the channel benches' shapes from the operator-channel contract and its client (B5d; ADR 0634 §1, §6; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:03:24 +01:00
parent f61760f738
commit 1d3ed6e78e
20 changed files with 266 additions and 257 deletions
+3 -3
View File
@@ -26,7 +26,7 @@ import (
"strings"
"time"
ch "git.novox.be/novox/mesh-sdk/go/channel"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// Action is one answer a condition names: a label, and the seat verb its producer calls when it is chosen.
@@ -132,7 +132,7 @@ func pruneGrants(grants map[string]Grant, kind string, now time.Time, keep int)
// Chosen takes an answer a channel of a kind said it was given: an ask's option is judged and, when it
// stands, closes the ask with a warrant for its asker (asks.go); Details are shown where they were asked.
// Anything else is refused, and said there. The same answer is acted on once.
func (h *Holder) Chosen(kind string, c ch.Choice) (string, error) {
func (h *Holder) Chosen(kind string, c oc.Choice) (string, error) {
// The root-free word is asked before the work is taken (the confirmation review of 2026-10-09):
// up to RootFreeWithin, which no ask, answer or link should wait behind. Asked fresh here, for this answer,
// and judged with in it.
@@ -255,7 +255,7 @@ func firstLineOf(s string) string {
// Failed takes a channel's word that a message did not arrive: the channel is said to be failing, and the
// record it carried is tried again — unless the failure is permanent, which is said and not repeated.
func (h *Holder) Failed(kind string, f ch.Failed) {
func (h *Holder) Failed(kind string, f oc.Failed) {
h.work.Lock()
defer h.work.Unlock()
h.noteChannel(kind, errorf("%s", f.Why))
+21 -22
View File
@@ -12,7 +12,6 @@ import (
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"
ch "git.novox.be/novox/mesh-sdk/go/channel"
)
// The router of novox/hq ADR 0259: it holds every ask, judges each answer and issues a warrant; it
@@ -252,9 +251,9 @@ func link(h *Holder, store *memAsks, c *clock, identity string, at time.Time) {
Linked: at, ApproveFrom: at.Add(LinkedApproveAfter)}
}
func choiceOf(ticket, identity string, at time.Time) ch.Choice {
return ch.Choice{ID: "c-" + ticket + "-" + identity, Ticket: ticket, At: at,
Sender: ch.Sender{Identity: identity, Display: "Jo", Authenticated: true}}
func choiceOf(ticket, identity string, at time.Time) oc.Choice {
return oc.Choice{ID: "c-" + ticket + "-" + identity, Ticket: ticket, At: at,
Sender: oc.Sender{Identity: identity, Display: "Jo", Authenticated: true}}
}
func ticketFor(m Message, label string) string {
@@ -737,7 +736,7 @@ func TestAnAnswerReplayedLongAfterIsNotActedOn(t *testing.T) {
func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
h, tg, dt, c, store, _ := newRouter(t)
asked := ch.LinkAsked{ID: "l1", At: c.now(), Sender: ch.Sender{Identity: "42", Display: "Jo", Authenticated: true}}
asked := oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}}
h.Link("telegram", asked)
if len(dt.sends) != 1 || !strings.HasPrefix(dt.sends[0].Title, "Link a Telegram account as you") {
t.Fatalf("no code on the desk: %+v", dt.sends)
@@ -750,13 +749,13 @@ func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
t.Errorf("the code is kept in clear: %+v", p)
}
// Another account sending the code is refused; a wrong code is refused; the right one links.
if a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "666", Authenticated: true}, Code: code}); a.Accepted {
if a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "666", Authenticated: true}, Code: code}); a.Accepted {
t.Error("another account linked with the code")
}
if a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "42", Authenticated: true}, Code: "000000x"}); a.Accepted {
if a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "42", Authenticated: true}, Code: "000000x"}); a.Accepted {
t.Error("a wrong code linked")
}
a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"})
a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"})
if !a.Accepted || !strings.Contains(a.Words, "approve from 13:00") {
t.Fatalf("the right code: %+v (code %q)", a, code)
}
@@ -771,7 +770,7 @@ func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
}
}
// A second account cannot link while one is.
h.Link("telegram", ch.LinkAsked{ID: "l2", At: c.now(), Sender: ch.Sender{Identity: "666", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l2", At: c.now(), Sender: oc.Sender{Identity: "666", Authenticated: true}})
if _, pending := store.links["telegram"]; pending {
t.Error("a second link was begun while one is linked")
}
@@ -779,21 +778,21 @@ func TestLinkingShowsACodeOnTheDeskAndLinksOnlyWhoAsked(t *testing.T) {
func TestALinkCodeExpiresAndFiveWrongTriesEndIt(t *testing.T) {
h, _, _, c, store, _ := newRouter(t)
jo := ch.Sender{Identity: "42", Authenticated: true}
h.Link("telegram", ch.LinkAsked{ID: "l1", At: c.now(), Sender: jo})
jo := oc.Sender{Identity: "42", Authenticated: true}
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: jo})
for i := 0; i < LinkTries; i++ {
h.Prove("telegram", ch.Code{Sender: jo, Code: "nope"})
h.Prove("telegram", oc.Code{Sender: jo, Code: "nope"})
}
if _, pending := store.links["telegram"]; pending {
t.Error("five wrong codes did not end the link")
}
h.Link("telegram", ch.LinkAsked{ID: "l2", At: c.now(), Sender: jo})
h.Link("telegram", oc.LinkAsked{ID: "l2", At: c.now(), Sender: jo})
c.pass(LinkCodeFor)
if a := h.Prove("telegram", ch.Code{Sender: jo, Code: "123456"}); a.Accepted || !strings.Contains(a.Words, "expired") {
if a := h.Prove("telegram", oc.Code{Sender: jo, Code: "123456"}); a.Accepted || !strings.Contains(a.Words, "expired") {
t.Errorf("%+v", a)
}
// The desk cannot be linked: it proves nothing about who clicked.
h.Link("desktop", ch.LinkAsked{ID: "l3", At: c.now(), Sender: ch.Sender{Identity: "g14"}})
h.Link("desktop", oc.LinkAsked{ID: "l3", At: c.now(), Sender: oc.Sender{Identity: "g14"}})
if _, pending := store.links["desktop"]; pending {
t.Error("the desk began a link")
}
@@ -849,9 +848,9 @@ func TestARelinkWithinADayWaitsForTheReplacedAccount(t *testing.T) {
link(h, store, c, "42", c.now().Add(-2*time.Hour))
_, _ = h.Unlink("telegram")
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
h.Link("telegram", ch.LinkAsked{ID: "l9", At: c.now(), Sender: ch.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: oc.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
code := codeOf(t, dt.sends[len(dt.sends)-1])
a := h.Prove("telegram", ch.Code{Sender: ch.Sender{Identity: "666", Authenticated: true}, Code: code})
a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "666", Authenticated: true}, Code: code})
if !a.Accepted || !strings.Contains(a.Words, "waits for that account's approval") || len(store.ids) != 0 {
t.Fatalf("linked at once within a day of an unlink: %+v %v", a, store.ids)
}
@@ -979,12 +978,12 @@ func TestAnAnswerNeedsAChannelThatRendersExactly(t *testing.T) {
// At most three links an hour per kind, and the code is shown only where the words stay private.
func TestLinksAreBoundedAndTheCodeShownOnlyInPrivate(t *testing.T) {
h, tg, dt, c, store, _ := newRouter(t)
jo := ch.Sender{Identity: "42", Display: "Jo", Authenticated: true}
jo := oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}
for i := 0; i < LinksAnHour; i++ {
h.Link("telegram", ch.LinkAsked{ID: fmt.Sprint("l", i), At: c.now(), Sender: jo})
h.Link("telegram", oc.LinkAsked{ID: fmt.Sprint("l", i), At: c.now(), Sender: jo})
}
shown := len(dt.sends)
h.Link("telegram", ch.LinkAsked{ID: "l9", At: c.now(), Sender: jo})
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: jo})
if len(dt.sends) != shown || !strings.Contains(tg.sends[len(tg.sends)-1].Body, "Try again in an hour") {
t.Error("a fourth link this hour showed a code")
}
@@ -995,7 +994,7 @@ func TestLinksAreBoundedAndTheCodeShownOnlyInPrivate(t *testing.T) {
h2.Bench.SetKinds([]KindInfo{
{Kind: "telegram", Module: "telegram", Capabilities: []string{"choice", "verified-sender", "exact-render", "reaches-away"}},
{Kind: "desktop", Module: "desk-channel", Capabilities: []string{"choice", "exact-render"}}})
h2.Link("telegram", ch.LinkAsked{ID: "l1", At: c2.now(), Sender: jo})
h2.Link("telegram", oc.LinkAsked{ID: "l1", At: c2.now(), Sender: jo})
if _, pending := store2.links["telegram"]; pending || !strings.Contains(tg2.sends[len(tg2.sends)-1].Body, "No private place") {
t.Error("a code was shown where the words are not private")
}
@@ -1013,7 +1012,7 @@ func TestAWarningTheDeskCouldNotShowGoesAway(t *testing.T) {
if len(dt.sends) != 1 {
t.Fatalf("the desk was sent %d", len(dt.sends))
}
h.Failed("desktop", ch.Failed{Handle: "g14=1", Why: "no session answers", At: c.now()})
h.Failed("desktop", oc.Failed{Handle: "g14=1", Why: "no session answers", At: c.now()})
c.pass(BurstWindow)
h.Flush()
if len(tg.sends) == 0 {
+11 -12
View File
@@ -30,7 +30,6 @@ import (
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"
ch "git.novox.be/novox/mesh-sdk/go/channel"
)
// ErrChanged is a compare-and-set that lost: another writer was first.
@@ -1055,7 +1054,7 @@ func (h *Holder) toldLine(rec AskRecord) string {
// confirmChoice says, where an answer was given and in reply to it, that it was recorded: on Telegram the
// tap's own acknowledgement (issue 372). Called only once the ask's end is recorded.
func (h *Holder) confirmChoice(kind string, c ch.Choice, label, asker string, told bool) {
func (h *Holder) confirmChoice(kind string, c oc.Choice, label, asker string, told bool) {
words := "You chose " + label + ": recorded"
switch {
case asker == SelfAsker:
@@ -1070,7 +1069,7 @@ func (h *Holder) confirmChoice(kind string, c ch.Choice, label, asker string, to
}
// confirmAgain says, in reply to the same answer given again, that it was already recorded.
func (h *Holder) confirmAgain(kind string, c ch.Choice, label string, at time.Time) {
func (h *Holder) confirmAgain(kind string, c oc.Choice, label string, at time.Time) {
err := h.showOn(kind, Message{Title: "You already chose " + label + " at " + at.In(h.zone()).Format("15:04") + ": recorded.",
Quiet: true, Reply: c.ID})
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed again " + label, Outcome: outcomeOf(err)})
@@ -1174,7 +1173,7 @@ func joinOr(labels []string) string {
// judge is an answer to an ask's option, chosen on a kind: refused at the first failure, in words said on
// the kind; otherwise the ask closed and the warrant published (ADR 0259 §5).
func (h *Holder) judge(kind string, c ch.Choice, key string, root func(KindInfo) string) (string, error) {
func (h *Holder) judge(kind string, c oc.Choice, key string, root func(KindInfo) string) (string, error) {
e := h.askOf(key)
if e == nil {
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
@@ -1290,7 +1289,7 @@ func (h *Holder) judge(kind string, c ch.Choice, key string, root func(KindInfo)
// refuseStranger refuses an answer from an account that is not the operator's, and tells the operator, by name
// and number, on the account that is linked: a question's buttons reached somebody else (a forwarded message,
// or a phone in other hands). Nothing is said to the other account.
func (h *Holder) refuseStranger(kind string, c ch.Choice) (string, error) {
func (h *Holder) refuseStranger(kind string, c oc.Choice) (string, error) {
why := "this account is not linked as the operator"
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
@@ -1306,7 +1305,7 @@ func (h *Holder) refuseStranger(kind string, c ch.Choice) (string, error) {
}
// refuseChoice says on the kind that an answer was not taken, and why.
func (h *Holder) refuseChoice(kind string, c ch.Choice, why string) (string, error) {
func (h *Holder) refuseChoice(kind string, c oc.Choice, why string) (string, error) {
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
h.showOn(kind, Message{Title: "That answer was not taken", Body: capital(why) + ". Nothing was done.",
@@ -1326,15 +1325,15 @@ func (h *Holder) showOnHandle(kind string, m Message) (string, error) {
if h.Publish == nil {
return "", errors.New("this router cannot reach the bus")
}
out := ch.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To,
out := oc.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To,
Secret: m.Secret}
if out.To == "" && h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, ch.Button{Label: a.Label, Ticket: a.Token})
out.Buttons = append(out.Buttons, oc.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(ch.ChannelSubject(ch.Show, kind), out, out.Handle)
err := h.Publish(occ.ChannelSubject(occ.Show, kind), out, out.Handle)
h.noteChannel(kind, err)
return out.Handle, err
}
@@ -1351,7 +1350,7 @@ func (h *Holder) showOn(kind string, m Message) error {
if h.Publish == nil {
return errors.New("this router cannot reach the bus")
}
out := ch.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply}
out := oc.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply}
if h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
@@ -1360,9 +1359,9 @@ func (h *Holder) showOn(kind string, m Message) error {
}
out.Secret = m.Secret
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, ch.Button{Label: a.Label, Ticket: a.Token})
out.Buttons = append(out.Buttons, oc.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(ch.ChannelSubject(ch.Send, kind), out, out.Handle)
err := h.Publish(occ.ChannelSubject(occ.Send, kind), out, out.Handle)
h.noteChannel(kind, err)
return err
}
+6 -6
View File
@@ -16,8 +16,8 @@ import (
"testing"
"time"
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"
ch "git.novox.be/novox/mesh-sdk/go/channel"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
@@ -98,7 +98,7 @@ func (r *busRuntime) bucket(name string) jetstream.KeyValue {
func (r *busRuntime) setKinds(caps map[string][]string) {
var out []map[string]any
for kind, c := range caps {
out = append(out, map[string]any{"seat": ch.ChannelSeat, "kind": kind, "module": kind, "node": "test", "capabilities": c})
out = append(out, map[string]any{"seat": occ.ChannelSeat, "kind": kind, "module": kind, "node": "test", "capabilities": c})
}
r.mu.Lock()
r.kinds = out
@@ -236,7 +236,7 @@ func (r *busRuntime) runtime(id json.RawMessage, method string, params json.RawM
}
return keys, err
case "mesh/subscribe":
if strings.HasPrefix(p.Pattern, ch.IntakeSeat) {
if strings.HasPrefix(p.Pattern, occ.IntakeSeat) {
r.mu.Lock()
r.subscribed = true
r.mu.Unlock()
@@ -279,7 +279,7 @@ func (r *busRuntime) forward() {
}); err != nil {
r.t.Fatal(err)
}
if _, err := r.nc.Subscribe("mesh.seat."+ch.IntakeSeat+".event.>", func(m *nats.Msg) {
if _, err := r.nc.Subscribe("mesh.seat."+occ.IntakeSeat+".event.>", func(m *nats.Msg) {
key := strings.Replace(strings.TrimPrefix(m.Subject, "mesh.seat."), ".event", "", 1)
_, _ = r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": key, "node": "test", "body": json.RawMessage(m.Data)}})
}); err != nil {
@@ -329,7 +329,7 @@ func TestAnApprovalAskIsHeldThenOfferedWhenAChannelCanCarryItOverTheBus(t *testi
if _, err := r.nc.Subscribe(oc.SubjectDecided("mesh-delivery"), collect(&told)); err != nil {
t.Fatal(err)
}
if _, err := r.nc.Subscribe(ch.ChannelSubject(ch.Show, "telegram"), collect(&shown)); err != nil {
if _, err := r.nc.Subscribe(occ.ChannelSubject(occ.Show, "telegram"), collect(&shown)); err != nil {
t.Fatal(err)
}
r.forward()
@@ -340,7 +340,7 @@ func TestAnApprovalAskIsHeldThenOfferedWhenAChannelCanCarryItOverTheBus(t *testi
return r.subscribed && r.taking && r.kindsRead > 0
})
standing, _ := json.Marshal(Standing{Ready: true, At: time.Now().UTC()})
if err := r.nc.Publish(ch.IntakeSubject(ch.StandingWhat, "telegram"), standing); err != nil {
if err := r.nc.Publish(occ.IntakeSubject(occ.StandingWhat, "telegram"), standing); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
@@ -12,7 +12,6 @@ import (
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"
ch "git.novox.be/novox/mesh-sdk/go/channel"
"github.com/nats-io/nats.go"
)
@@ -25,7 +24,7 @@ import (
type conditionBed struct {
r *busRuntime
mu sync.Mutex
desk []ch.Message
desk []oc.Message
acks []string // the answer of the router to each work item: "" when acknowledged
}
@@ -40,8 +39,8 @@ func newConditionBedWith(t *testing.T, settings string) *conditionBed {
b := &conditionBed{r: newBusRuntime(t)}
b.r.settings = settings
b.r.setKinds(map[string][]string{"desktop": {"choice", "edit"}})
if _, err := b.r.nc.Subscribe("mesh.seat."+ch.ChannelSeat+".accept.*.desktop", func(m *nats.Msg) {
var msg ch.Message
if _, err := b.r.nc.Subscribe("mesh.seat."+occ.ChannelSeat+".accept.*.desktop", func(m *nats.Msg) {
var msg oc.Message
if json.Unmarshal(m.Data, &msg) == nil {
b.mu.Lock()
b.desk = append(b.desk, msg)
@@ -83,7 +82,7 @@ func newConditionBedWith(t *testing.T, settings string) *conditionBed {
return b.r.subscribed
})
standing, _ := json.Marshal(Standing{Ready: true, At: time.Now().UTC()})
if err := b.r.nc.Publish(ch.IntakeSubject(ch.StandingWhat, "desktop"), standing); err != nil {
if err := b.r.nc.Publish(occ.IntakeSubject(occ.StandingWhat, "desktop"), standing); err != nil {
t.Fatal(err)
}
time.Sleep(time.Second)
@@ -93,7 +92,7 @@ func newConditionBedWith(t *testing.T, settings string) *conditionBed {
// forwardEvents hands the bundle the channels' word, as bus_test.go's forward does for it.
func (b *conditionBed) forwardEvents(t *testing.T) {
t.Helper()
if _, err := b.r.nc.Subscribe("mesh.seat."+ch.IntakeSeat+".event.>", func(m *nats.Msg) {
if _, err := b.r.nc.Subscribe("mesh.seat."+occ.IntakeSeat+".event.>", func(m *nats.Msg) {
key := strings.Replace(strings.TrimPrefix(m.Subject, "mesh.seat."), ".event", "", 1)
_, _ = b.r.request("mesh/event", map[string]any{"envelope": map[string]any{"key": key, "node": "test", "body": json.RawMessage(m.Data)}})
}); err != nil {
@@ -128,10 +127,10 @@ func (b *conditionBed) record(key string) *Record {
return &rec
}
func (b *conditionBed) told() []ch.Message {
func (b *conditionBed) told() []oc.Message {
b.mu.Lock()
defer b.mu.Unlock()
return append([]ch.Message(nil), b.desk...)
return append([]oc.Message(nil), b.desk...)
}
func (b *conditionBed) acknowledged(n int) bool {
+7 -8
View File
@@ -32,7 +32,6 @@ import (
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"
ch "git.novox.be/novox/mesh-sdk/go/channel"
)
const (
@@ -187,7 +186,7 @@ func (h *Holder) offerDeskUnlink(id Identity) bool {
// Link takes a link asked for on a kind: a code shown where only the operator sees it, or a refusal said on
// the kind.
func (h *Holder) Link(kind string, l ch.LinkAsked) string {
func (h *Holder) Link(kind string, l oc.LinkAsked) string {
h.warmRootFree(false) // outside the work: a question it asks reads a recent word
h.work.Lock()
defer h.work.Unlock()
@@ -278,11 +277,11 @@ func (h *Holder) Link(kind string, l ch.LinkAsked) string {
}
// Prove answers a code sent on a kind: the link it confirms, or why not. Never said in an event.
func (h *Holder) Prove(kind string, c ch.Code) ch.ProofAnswer {
func (h *Holder) Prove(kind string, c oc.Code) oc.ProofAnswer {
h.warmRootFree(false) // outside the work: a question it asks reads a recent word
h.work.Lock()
defer h.work.Unlock()
no := func(words string) ch.ProofAnswer { return ch.ProofAnswer{Accepted: false, Words: words} }
no := func(words string) oc.ProofAnswer { return oc.ProofAnswer{Accepted: false, Words: words} }
if c.Purpose != "" && c.Purpose != "link" {
return no("The mesh takes no code for that yet.")
}
@@ -341,12 +340,12 @@ func (h *Holder) Prove(kind string, c ch.Code) ch.ProofAnswer {
h.announce(Message{Title: "A new " + Display(kind) + " account asks to be linked as you",
Body: fmt.Sprintf("%s (number %s), less than a day after an unlink. It waits for the account it replaces, "+
"or for root at the router's machine.", p.Display, p.Identity), Urgent: true})
return ch.ProofAnswer{Accepted: true, Words: "The code is right. Because an account was unlinked here less " +
return oc.ProofAnswer{Accepted: true, Words: "The code is right. Because an account was unlinked here less " +
"than a day ago, the link waits for that account's approval, or for you at the router's machine."}
}
h.linkNow(id, "with the code shown on the desk")
from := now.Add(LinkedApproveAfter).In(h.zone()).Format("15:04")
return ch.ProofAnswer{Accepted: true, Words: "Linked. You can answer acknowledgements now, and approve from " + from + "."}
return oc.ProofAnswer{Accepted: true, Words: "Linked. You can answer acknowledgements now, and approve from " + from + "."}
}
// Identities is every linked account, by kind.
@@ -386,7 +385,7 @@ func sixDigits() (string, error) {
}
// decodeLink reads a link asked for.
func decodeLink(body json.RawMessage) (ch.LinkAsked, error) {
var l ch.LinkAsked
func decodeLink(body json.RawMessage) (oc.LinkAsked, error) {
var l oc.LinkAsked
return l, json.Unmarshal(body, &l)
}
+13 -13
View File
@@ -16,9 +16,9 @@ import (
_ "time/tzdata" // the operator's zone is known whatever this machine has installed
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"
stdio "git.novox.be/novox/mesh-sdk/go"
ch "git.novox.be/novox/mesh-sdk/go/channel"
)
func errorf(format string, a ...any) error { return fmt.Errorf(format, a...) }
@@ -493,7 +493,7 @@ func run(h *Holder, l *listening) {
// which the bus grants that kind's holder alone.
func kindOf(key string) (what, kind string, ok bool) {
parts := strings.Split(key, ".")
if len(parts) != 3 || parts[0] != ch.IntakeSeat {
if len(parts) != 3 || parts[0] != occ.IntakeSeat {
return "", "", false
}
return parts[1], parts[2], true
@@ -508,8 +508,8 @@ func listenForChannels(h *Holder) {
return nil
}
switch what {
case ch.Chosen:
var c ch.Choice
case occ.Chosen:
var c oc.Choice
if err := json.Unmarshal(e.Body, &c); err != nil || c.Ticket == "" {
logf("[messenger] an answer from %s could not be read: %s", kind, firstLineOf(string(e.Body)))
return nil
@@ -518,19 +518,19 @@ func listenForChannels(h *Holder) {
c.ID = e.Headers["x-event-id"]
}
_, _ = h.Chosen(kind, c)
case ch.Link:
case occ.Link:
l, err := decodeLink(e.Body)
if err != nil {
logf("[messenger] a link asked on %s could not be read", kind)
return nil
}
h.Link(kind, l)
case ch.FailedWhat:
var f ch.Failed
case occ.FailedWhat:
var f oc.Failed
if json.Unmarshal(e.Body, &f) == nil {
h.Failed(kind, f)
}
case ch.StandingWhat:
case occ.StandingWhat:
// Read as this module's Standing: the bench's word, and answers-lost-at beside it (issue 384).
var s Standing
if json.Unmarshal(e.Body, &s) == nil {
@@ -541,7 +541,7 @@ func listenForChannels(h *Holder) {
return nil
}
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
if err := stdio.Subscribe(ch.IntakeSeat+".#", handle); err == nil {
if err := stdio.Subscribe(occ.IntakeSeat+".#", handle); err == nil {
logf("[messenger] listening for what the channels say")
break
} else {
@@ -580,14 +580,14 @@ func answerProofs(h *Holder) {
handle := func(subject string, body json.RawMessage) (any, error) {
parts := strings.Split(subject, ".")
kind := parts[len(parts)-1]
var c ch.Code
var c oc.Code
if err := json.Unmarshal(body, &c); err != nil {
return ch.ProofAnswer{Words: "The code could not be read."}, nil
return oc.ProofAnswer{Words: "The code could not be read."}, nil
}
return h.Prove(kind, c), nil
}
for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) {
if err := stdio.SeatAnswer(ch.ProofSubject(ch.CodeProof, "*"), handle); err == nil {
if err := stdio.SeatAnswer(occ.ProofSubject(occ.CodeProof, "*"), handle); err == nil {
logf("[messenger] answering the codes the channels are sent")
break
} else {
@@ -614,7 +614,7 @@ func followKinds(h *Holder) {
} else {
var out []KindInfo
for _, k := range kinds {
if k.Seat == ch.ChannelSeat {
if k.Seat == occ.ChannelSeat {
out = append(out, KindInfo{Kind: k.Kind, Module: k.Module, Node: k.Node, Capabilities: k.Capabilities})
}
}
@@ -9,8 +9,8 @@ import (
"strings"
"testing"
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"
ch "git.novox.be/novox/mesh-sdk/go/channel"
)
// The manifest says what the code does (novox/hq ADR 0259): the seats it declares — the operator's, and
@@ -72,9 +72,9 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
want := []seat{
{Name: oc.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided"},
ByCaller: []string{"ask", "cancel", "condition", "decided"}, Records: []string{"asks"}},
{Name: ch.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{ch.Show, ch.Edit, ch.Send}},
{Name: ch.IntakeSeat, Scope: "mesh", Kinded: true,
Emits: []string{ch.Chosen, ch.Link, ch.FailedWhat, ch.StandingWhat}, Proofs: []string{ch.CodeProof}},
{Name: occ.ChannelSeat, Scope: "mesh", Kinded: true, Accepts: []string{occ.Show, occ.Edit, occ.Send}},
{Name: occ.IntakeSeat, Scope: "mesh", Kinded: true,
Emits: []string{occ.Chosen, occ.Link, occ.FailedWhat, occ.StandingWhat}, Proofs: []string{occ.CodeProof}},
}
if !reflect.DeepEqual(m.Seats, want) {
t.Fatalf("seats %+v", m.Seats)
@@ -82,7 +82,7 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
if len(m.Claims) != 1 || m.Claims[0].Name != oc.Seat || !reflect.DeepEqual(m.Claims[0].Serves, verbs) {
t.Fatalf("claims %+v", m.Claims)
}
if !reflect.DeepEqual(m.Uses, []string{ch.ChannelSeat}) {
if !reflect.DeepEqual(m.Uses, []string{occ.ChannelSeat}) {
t.Fatalf("uses %v", m.Uses)
}
if !reflect.DeepEqual(m.Consumes, []string{
+18 -19
View File
@@ -17,7 +17,6 @@ import (
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"
chn "git.novox.be/novox/mesh-sdk/go/channel"
)
// H3: approval rests on the controller's positive, recent word that no agent can become root where the router
@@ -206,11 +205,11 @@ func TestALinksCodeIsSecretOnEveryPath(t *testing.T) {
bench.Stand("telegram", Standing{Ready: true, At: c.now()})
bench.Stand("desktop", Standing{Ready: true, At: c.now()})
var mu sync.Mutex
put := map[string][]chn.Message{}
put := map[string][]oc.Message{}
publish := func(subject string, body any, id string) error {
mu.Lock()
defer mu.Unlock()
if m, ok := body.(chn.Message); ok {
if m, ok := body.(oc.Message); ok {
put[subject] = append(put[subject], m)
}
return nil
@@ -219,8 +218,8 @@ func TestALinksCodeIsSecretOnEveryPath(t *testing.T) {
Desk: &SeatChannel{Bench: bench, Role: RoleDesk, Publish: publish}, Bench: bench, Publish: publish,
Asks: newMemAsks(), Node: "anchor", Now: c.now, Logf: t.Logf, Zone: func() *time.Location { return time.UTC }}
h.init()
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
shown := put[chn.ChannelSubject(chn.Send, "desktop")]
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
shown := put[occ.ChannelSubject(occ.Send, "desktop")]
if len(shown) != 1 || !strings.Contains(shown[0].Body, "send this code") {
t.Fatalf("the code was not shown on the desk: %+v", put)
}
@@ -307,9 +306,9 @@ func TestALinkIsNotMadeWhileAnotherAccountIsLinked(t *testing.T) {
link(h, store, c, "42", c.now().Add(-2*time.Hour))
_, _ = h.Unlink("telegram")
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
h.Link("telegram", chn.LinkAsked{ID: "l9", At: c.now(), Sender: chn.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: oc.Sender{Identity: "666", Display: "Eve", Authenticated: true}})
code := codeOf(t, dt.sends[len(dt.sends)-1])
_ = h.Prove("telegram", chn.Code{Sender: chn.Sender{Identity: "666", Authenticated: true}, Code: code})
_ = h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "666", Authenticated: true}, Code: code})
asked := tg.sends[len(tg.sends)-1]
// Meanwhile root linked another account (allowed at the machine).
store.ids[identityKey("telegram", "77")] = Identity{Kind: "telegram", Identity: "77", Linked: c.now(), ApproveFrom: c.now().Add(time.Hour)}
@@ -326,7 +325,7 @@ func TestAnUnlinkedAccountDoesNotComeBackByACodeWithinADay(t *testing.T) {
_, _ = h.Unlink("telegram")
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
shown := len(dt.sends)
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
if _, pending := store.links["telegram"]; pending {
t.Error("a link was begun for the account unlinked an hour ago")
}
@@ -345,7 +344,7 @@ func TestAnUnlinkedAccountDoesNotComeBackByACodeWithinADay(t *testing.T) {
func TestAnAccountInItsFirstHourIsUnlinkedFromTheDesk(t *testing.T) {
h, tg, dt, c, store, w := newRouter(t)
link(h, store, c, "666", c.now().Add(-10*time.Minute)) // an account the operator did not link
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
if reply := tg.sends[len(tg.sends)-1]; !strings.Contains(reply.Body, "unlink it at your desk") {
t.Fatalf("the sender was not told how: %q", reply.Body)
}
@@ -367,7 +366,7 @@ func TestAnAccountInItsFirstHourIsUnlinkedFromTheDesk(t *testing.T) {
// Past its hour, no question goes to the desk.
h2, tg2, dt2, c2, store2, _ := newRouter(t)
link(h2, store2, c2, "666", c2.now().Add(-2*time.Hour))
h2.Link("telegram", chn.LinkAsked{ID: "l1", At: c2.now(), Sender: chn.Sender{Identity: "42", Authenticated: true}})
h2.Link("telegram", oc.LinkAsked{ID: "l1", At: c2.now(), Sender: oc.Sender{Identity: "42", Authenticated: true}})
for _, m := range dt2.sends {
if labelsOf(m) == "Unlink it" {
t.Error("the desk was asked to unlink an account past its first hour")
@@ -383,7 +382,7 @@ func TestAnAccountInItsFirstHourIsUnlinkedFromTheDesk(t *testing.T) {
func TestNoLinkIsBegunWhileTheDeskCannotShowTheCode(t *testing.T) {
h, tg, dt, c, store, _ := newRouter(t)
h.Bench.Stand("desktop", Standing{Ready: false, Why: "notifications are paused on the desk", At: c.now()})
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
if _, pending := store.links["telegram"]; pending {
t.Error("a link was begun")
}
@@ -399,12 +398,12 @@ func TestNoLinkIsBegunWhileTheDeskCannotShowTheCode(t *testing.T) {
// operator's link, and a restart forgets no count.
func TestLinksAreBoundedPerAccountAndTheCountIsKept(t *testing.T) {
h, tg, dt, c, store, _ := newRouter(t)
eve := chn.Sender{Identity: "666", Display: "Eve", Authenticated: true}
eve := oc.Sender{Identity: "666", Display: "Eve", Authenticated: true}
for i := 0; i < LinksAnHour; i++ {
h.Link("telegram", chn.LinkAsked{ID: "e" + string(rune('0'+i)), At: c.now(), Sender: eve})
h.Link("telegram", oc.LinkAsked{ID: "e" + string(rune('0'+i)), At: c.now(), Sender: eve})
}
shown := len(dt.sends)
h.Link("telegram", chn.LinkAsked{ID: "jo", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "jo", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
if len(dt.sends) != shown+1 || !strings.Contains(dt.sends[len(dt.sends)-1].Body, "(number 42)") {
t.Fatalf("a stranger's tries stopped the operator's link: %q", tg.sends[len(tg.sends)-1].Body)
}
@@ -414,7 +413,7 @@ func TestLinksAreBoundedPerAccountAndTheCountIsKept(t *testing.T) {
c2.pass(c.now().Sub(c2.now()))
stand(h2, c2)
before := len(dt2.sends)
h2.Link("telegram", chn.LinkAsked{ID: "e9", At: c2.now(), Sender: eve})
h2.Link("telegram", oc.LinkAsked{ID: "e9", At: c2.now(), Sender: eve})
if len(dt2.sends) != before || !strings.Contains(tg2.sends[len(tg2.sends)-1].Body, "Try again in an hour") {
t.Error("a restart forgot the count")
}
@@ -463,7 +462,7 @@ func TestTwoAnswersAtOnceMakeOneWarrant(t *testing.T) {
func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
h, tg, dt, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-10*time.Minute)) // the operator's own, linked just now
h.Link("telegram", chn.LinkAsked{ID: "l1", At: c.now(), Sender: chn.Sender{Identity: "666", Display: "X", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l1", At: c.now(), Sender: oc.Sender{Identity: "666", Display: "X", Authenticated: true}})
asked := dt.sends[len(dt.sends)-1]
ch := choiceOf(ticketFor(asked, "Unlink it"), "g14", c.now()) // a click nobody can prove was the operator's
ch.Sender.Authenticated = false
@@ -479,7 +478,7 @@ func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
// The operator sends /start again: a code is shown at once, and the code links without waiting for anybody.
c.pass(time.Minute)
stand(h, c)
h.Link("telegram", chn.LinkAsked{ID: "l2", At: c.now(), Sender: chn.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l2", At: c.now(), Sender: oc.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
code := ""
for _, m := range dt.sends {
if strings.Contains(m.Body, "number 42") && strings.Contains(m.Body, "send this code") {
@@ -489,7 +488,7 @@ func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
if code == "" {
t.Fatalf("no code was shown to relink the operator's account; said %q", tg.sends[len(tg.sends)-1].Body)
}
if a := h.Prove("telegram", chn.Code{Sender: chn.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"}); !a.Accepted ||
if a := h.Prove("telegram", oc.Code{Sender: oc.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"}); !a.Accepted ||
!strings.Contains(a.Words, "Linked") {
t.Fatalf("the operator's code was not taken at once: %+v", a)
}
@@ -549,7 +548,7 @@ func TestTheRootFreeWordIsNotAskedWhileTheWorkIsHeld(t *testing.T) {
<-asking
linked := make(chan struct{})
go func() {
h.Link("telegram", chn.LinkAsked{ID: "l9", At: c.now(), Sender: chn.Sender{Identity: "77", Authenticated: true}})
h.Link("telegram", oc.LinkAsked{ID: "l9", At: c.now(), Sender: oc.Sender{Identity: "77", Authenticated: true}})
close(linked)
}()
select {
@@ -24,7 +24,8 @@ import (
"sync"
"time"
ch "git.novox.be/novox/mesh-sdk/go/channel"
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"
)
// StandingFresh is how long a channel's word on whether it can send is believed: a channel says it at
@@ -277,16 +278,16 @@ func (c *SeatChannel) putOn(kind, handle string, m Message, edit bool) error {
if c.Publish == nil {
return errors.New("this router cannot reach the bus")
}
verb := ch.Send
verb := occ.Send
switch {
case edit:
verb = ch.Edit
verb = occ.Edit
case len(m.Actions) > 0:
verb = ch.Show
verb = occ.Show
}
// Secret travels whatever the path: a link's code is kept by no channel (the review of 2026-10-09 found it
// dropped here, where the desk's own role sends).
out := ch.Message{Handle: handle, Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply,
out := oc.Message{Handle: handle, Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply,
Secret: m.Secret}
if c.Bench.To != nil {
out.To = c.Bench.To(kind)
@@ -295,9 +296,9 @@ func (c *SeatChannel) putOn(kind, handle string, m Message, edit bool) error {
out.To = m.To
}
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, ch.Button{Label: a.Label, Ticket: a.Token})
out.Buttons = append(out.Buttons, oc.Button{Label: a.Label, Ticket: a.Token})
}
return c.Publish(ch.ChannelSubject(verb, kind), out, handle+"."+fmt.Sprint(time.Now().UnixNano()))
return c.Publish(occ.ChannelSubject(verb, kind), out, handle+"."+fmt.Sprint(time.Now().UnixNano()))
}
func newHandle() string {