M1-M5: the router holds back the asker's oldest ask behind a fourth and offers it again when one ends; the seat operator-channel's ask-again offers it now (milestone 47/G, issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW ask-again is the seat operator-channel's verb (key in; state or error out), not the module tool messenger_ask_again, and records no hand act (G §394, §450). (Joined from 681975ec.) Builds: design 47 G §40, §83, §403 (RTR1, RTR2) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -592,12 +592,15 @@ func TestAnAskIsRefusedInWordsToItsAsker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
all := strings.Join(refused, "\n")
|
||||
for _, want := range []string{"d-1: the ask is refused: an ask that destroys lasts at most", "d-2: its words carry",
|
||||
"d-6: its asker already has 3 open asks"} {
|
||||
// A fourth offered ask is never refused (ADR 0666 §12): it is taken and the asker's oldest is held back.
|
||||
for _, want := range []string{"d-1: the ask is refused: an ask that destroys lasts at most", "d-2: its words carry"} {
|
||||
if !strings.Contains(all, want) {
|
||||
t.Errorf("not refused: %q in\n%s", want, all)
|
||||
}
|
||||
}
|
||||
if strings.Contains(all, "d-6") || store.asks["mesh-delivery.d-6"].State != StateOpen || store.asks["mesh-delivery.d-3"].State != StateHeldBack {
|
||||
t.Errorf("the fourth ask was refused or the oldest not held back: d-6 %s, d-3 %s", store.asks["mesh-delivery.d-6"].State, store.asks["mesh-delivery.d-3"].State)
|
||||
}
|
||||
if _, kept := store.asks["mesh-delivery.d-1"]; kept {
|
||||
t.Error("a refused ask was recorded")
|
||||
}
|
||||
|
||||
@@ -65,6 +65,9 @@ type AskRecord struct {
|
||||
Warrant *oc.Warrant `json:"warrant,omitempty"`
|
||||
// Told: the warrant, or the end, was published to the asker.
|
||||
Told bool `json:"told,omitempty"`
|
||||
// HeldBackSince is, while the ask is held back, since when: kept open behind newer asks of its asker, its tickets
|
||||
// withdrawn, offered again when one of its asker's offered asks ends (novox/hq ADR 0666 §12).
|
||||
HeldBackSince time.Time `json:"held-back-since,omitempty"`
|
||||
// HeldSince and HeldWhy say, while the ask is held, since when and why no channel can carry it: per kind, the
|
||||
// reason from the judgement that judges an answer. The asker reads them in this record, as it reads
|
||||
// the state (the asks protocol has no word for a held ask on `decided`, which means an ask ended).
|
||||
@@ -558,23 +561,24 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
|
||||
h.refuseAsk(asker, a, "its whole words carry "+refusal.String()+", which may not leave the mesh on any channel: withhold it")
|
||||
return nil
|
||||
}
|
||||
// A fourth offered ask is taken, never refused: the asker's oldest offered one is held back once this one is recorded
|
||||
// (holdBackOldest). An ask with the same `about` replaces the open one, a held-back one too.
|
||||
open, replaced := 0, ""
|
||||
h.mu.Lock()
|
||||
for k, e := range h.asks {
|
||||
if e.rec.Asker != asker || (e.rec.State != StateOpen && e.rec.State != StateHeld) {
|
||||
if e.rec.Asker != asker || !liveState(e.rec.State) {
|
||||
continue
|
||||
}
|
||||
if a.About != "" && e.rec.Ask.About == a.About {
|
||||
replaced = k
|
||||
continue
|
||||
}
|
||||
open++
|
||||
if e.rec.State != StateHeldBack {
|
||||
open++
|
||||
}
|
||||
}
|
||||
h.mu.Unlock()
|
||||
if open >= occ.MostOpen {
|
||||
h.refuseAsk(asker, a, fmt.Sprintf("its asker already has %d open asks", open))
|
||||
return nil
|
||||
}
|
||||
holdBack := open >= occ.MostOpen
|
||||
rec := AskRecord{Asker: asker, Ask: a, State: StateOpen, Opened: now, Tickets: h.ticketsFor(a)}
|
||||
if len(rec.Tickets) == 0 {
|
||||
// Held, not refused (ADR 0397): the ask breaks no rule, and the operator is asked when a channel can carry it.
|
||||
@@ -605,6 +609,9 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
|
||||
} else {
|
||||
h.keepAsk(key, rec, 0)
|
||||
}
|
||||
if holdBack {
|
||||
h.holdBackOldest(asker, key)
|
||||
}
|
||||
if rec.State == StateHeld {
|
||||
h.Logf("[messenger] %s asks %q (%s): held, not shown: %s", asker, a.Headline, a.ID, rec.HeldWhy)
|
||||
return nil
|
||||
@@ -897,12 +904,14 @@ func (h *Holder) closeAsk(key string, outcome string, words string, w *oc.Warran
|
||||
rec, rev = *got, r
|
||||
}
|
||||
}
|
||||
if rec.State != StateOpen && rec.State != StateHeld {
|
||||
if !liveState(rec.State) {
|
||||
h.keepAsk(key, rec, rev)
|
||||
return ErrChanged
|
||||
}
|
||||
now := h.Now()
|
||||
freed := rec.State == StateOpen || rec.State == StateHeld // a slot of the asker's offered asks, unless a newer ask takes it
|
||||
rec.State = string(outcome)
|
||||
rec.HeldBackSince = time.Time{}
|
||||
rec.HeldWhy = ""
|
||||
rec.Closed = now
|
||||
if t, ok := rec.Tickets[used]; ok {
|
||||
@@ -926,6 +935,9 @@ func (h *Holder) closeAsk(key string, outcome string, words string, w *oc.Warran
|
||||
rev = newRev
|
||||
}
|
||||
h.keepAsk(key, rec, rev)
|
||||
if freed && outcome != occ.OutcomeReplaced {
|
||||
defer h.offerHeldBack(rec.Asker)
|
||||
}
|
||||
h.sayClosed(key, rec, outcome, words)
|
||||
if rec.Asker == SelfAsker {
|
||||
// The router's own ask: it does what was decided, and tells nobody else (selfact.go).
|
||||
@@ -1016,7 +1028,7 @@ func (h *Holder) endLeftovers() {
|
||||
if r.Origin != "ask" || r.Ask == "" || !r.Cleared.IsZero() {
|
||||
continue
|
||||
}
|
||||
if e := h.asks[r.Ask]; e != nil && e.rec.State != StateOpen && e.rec.Record == r.Key {
|
||||
if e := h.asks[r.Ask]; e != nil && !liveState(e.rec.State) && e.rec.Record == r.Key {
|
||||
ended = append(ended, e.rec)
|
||||
}
|
||||
}
|
||||
@@ -1081,9 +1093,9 @@ func (h *Holder) sweepAsks(now time.Time) {
|
||||
var expired, untold []string
|
||||
for k, e := range h.asks {
|
||||
switch {
|
||||
case (e.rec.State == StateOpen || e.rec.State == StateHeld) && !now.Before(askExpires(e.rec.Ask)):
|
||||
case liveState(e.rec.State) && !now.Before(askExpires(e.rec.Ask)):
|
||||
expired = append(expired, k)
|
||||
case e.rec.State != StateOpen && e.rec.State != StateHeld && !e.rec.Told && e.rec.Warrant != nil:
|
||||
case !liveState(e.rec.State) && !e.rec.Told && e.rec.Warrant != nil:
|
||||
untold = append(untold, k)
|
||||
}
|
||||
}
|
||||
@@ -1195,6 +1207,8 @@ func (h *Holder) judge(kind string, c oc.Choice, key string, root func(KindInfo)
|
||||
return "chosen", nil
|
||||
}
|
||||
switch {
|
||||
case rec.State == StateHeldBack:
|
||||
return h.refuseChoice(kind, c, "the question waits behind newer questions of its asker, and is offered again when one of them ends")
|
||||
case !issued:
|
||||
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
|
||||
case t.Kind != kind:
|
||||
@@ -1386,6 +1400,10 @@ type AskView struct {
|
||||
// HeldSince and HeldWhy: while the ask is held, since when and why no channel can carry it yet (ADR 0397 §4).
|
||||
HeldSince string `json:"held_since,omitempty"`
|
||||
HeldWhy string `json:"held_why,omitempty"`
|
||||
// HeldBackSince and Behind: while the ask is held back (ADR 0666 §12), since when and the keys of the newer asks of its
|
||||
// asker it waits behind, in the names of the seat operator-channel's asks answer.
|
||||
HeldBackSince string `json:"held-since,omitempty"`
|
||||
Behind []string `json:"behind,omitempty"`
|
||||
}
|
||||
|
||||
// AsksNow is every ask, open first, newest first.
|
||||
@@ -1400,6 +1418,9 @@ func (h *Holder) AsksNow() []AskView {
|
||||
if e.rec.State == StateHeld {
|
||||
v.HeldSince, v.HeldWhy = e.rec.HeldSince.UTC().Format(time.RFC3339), e.rec.HeldWhy
|
||||
}
|
||||
if e.rec.State == StateHeldBack {
|
||||
v.HeldBackSince, v.Behind = e.rec.HeldBackSince.UTC().Format(time.RFC3339), h.behindLocked(k, e.rec)
|
||||
}
|
||||
kinds := map[string]bool{}
|
||||
for _, t := range e.rec.Tickets {
|
||||
kinds[t.Kind] = true
|
||||
@@ -1418,7 +1439,7 @@ func (h *Holder) AsksNow() []AskView {
|
||||
out = append(out, v)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
oi, oj := out[i].State == StateOpen || out[i].State == StateHeld, out[j].State == StateOpen || out[j].State == StateHeld
|
||||
oi, oj := liveState(out[i].State), liveState(out[j].State)
|
||||
if oi != oj {
|
||||
return oi
|
||||
}
|
||||
@@ -1479,7 +1500,7 @@ func (h *Holder) sayClosed(key string, rec AskRecord, outcome string, words stri
|
||||
}
|
||||
}
|
||||
|
||||
// AsksQuery narrows what the tool `operator-channel.asks` answers (issue 595): Key one ask, State open (open and held) or
|
||||
// AsksQuery narrows what the tool `operator-channel.asks` answers (issue 595): Key one ask, State open (open, held and held-back) or
|
||||
// ended (all takes both), Since the asks opened at or after that time, Bound the most bytes of the answer's asks. With no
|
||||
// argument at all the query is the open asks within DefaultAsksBound (issue 1974), and the answer says so.
|
||||
type AsksQuery struct {
|
||||
@@ -1543,7 +1564,7 @@ func (q AsksQuery) Narrow(all []AskView) AsksAnswer {
|
||||
}
|
||||
var kept []AskView
|
||||
for _, v := range all {
|
||||
open := v.State == StateOpen || v.State == StateHeld
|
||||
open := liveState(v.State)
|
||||
switch {
|
||||
case q.Key != "" && v.Key != q.Key,
|
||||
q.State == "open" && !open,
|
||||
|
||||
@@ -105,17 +105,17 @@ func TestAHeldAskExpiresAsAnyOpenAskDoes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAHeldAskCountsTowardTheOpenAsksOfItsAsker(t *testing.T) {
|
||||
h, _, _, c, _, w := newRouter(t)
|
||||
func TestAHeldAskCountsTowardTheOffersOfItsAskerAndTheOldestIsHeldBack(t *testing.T) {
|
||||
h, _, _, c, store, w := newRouter(t)
|
||||
withoutVerifiedSender(h)
|
||||
for i := 1; i <= 4; i++ {
|
||||
a := heldAsk(c, "d-"+string(rune('0'+i)))
|
||||
a.About = "about-" + string(rune('0'+i))
|
||||
submit(t, h, "mesh-delivery", a)
|
||||
c.pass(time.Second)
|
||||
}
|
||||
got := w.of("mesh-delivery")
|
||||
if len(got) != 1 || got[0].Outcome != occ.OutcomeRefused || !strings.Contains(got[0].Words, "already has 3 open asks") {
|
||||
t.Fatalf("%+v", got)
|
||||
if got := w.of("mesh-delivery"); len(got) != 0 || store.asks["mesh-delivery.d-1"].State != StateHeldBack || store.asks["mesh-delivery.d-4"].State != StateHeld {
|
||||
t.Fatalf("%+v: a fourth is taken, never refused; the oldest is held back (%s)", got, store.asks["mesh-delivery.d-1"].State)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
// An ask held back (novox/hq ADR 0666 §12, design 46 § 6): an asker has at most MostOpen asks offered at once; a fourth
|
||||
// is offered and nothing is refused, so no ask is hidden: the asker's oldest offered ask is held back, its tickets
|
||||
// withdrawn and its message edited to say it waits behind newer asks, and it is offered again, with new tickets, when
|
||||
// one of the asker's offered asks ends. `held` is another state, an ask no ready channel can carry (ADR 0397).
|
||||
|
||||
// StateHeldBack is an ask kept open behind newer asks of its asker.
|
||||
const StateHeldBack = "held-back"
|
||||
|
||||
// liveState is whether an ask has not ended: offered, held for want of a channel, or held back.
|
||||
func liveState(state string) bool {
|
||||
return state == StateOpen || state == StateHeld || state == StateHeldBack
|
||||
}
|
||||
|
||||
// behindLocked are the keys of the newer offered asks of the asker of rec, oldest first; h.mu is held.
|
||||
func (h *Holder) behindLocked(key string, rec AskRecord) []string {
|
||||
var behind []string
|
||||
for k, e := range h.asks {
|
||||
if k != key && e.rec.Asker == rec.Asker && (e.rec.State == StateOpen || e.rec.State == StateHeld) && e.rec.Opened.After(rec.Opened) {
|
||||
behind = append(behind, k)
|
||||
}
|
||||
}
|
||||
sort.Slice(behind, func(i, j int) bool {
|
||||
a, b := h.asks[behind[i]].rec, h.asks[behind[j]].rec
|
||||
if !a.Opened.Equal(b.Opened) {
|
||||
return a.Opened.Before(b.Opened)
|
||||
}
|
||||
return behind[i] < behind[j]
|
||||
})
|
||||
return behind
|
||||
}
|
||||
|
||||
// pickLocked is the key of the asker's asks in one of the states, the oldest first (by opening, then key); "" when
|
||||
// there is none, never except; h.mu is held.
|
||||
func (h *Holder) pickLocked(asker, except string, states ...string) string {
|
||||
pick := ""
|
||||
for k, e := range h.asks {
|
||||
if e.rec.Asker != asker || k == except {
|
||||
continue
|
||||
}
|
||||
in := false
|
||||
for _, s := range states {
|
||||
in = in || e.rec.State == s
|
||||
}
|
||||
if !in {
|
||||
continue
|
||||
}
|
||||
if pick == "" || e.rec.Opened.Before(h.asks[pick].rec.Opened) || (e.rec.Opened.Equal(h.asks[pick].rec.Opened) && k < pick) {
|
||||
pick = k
|
||||
}
|
||||
}
|
||||
return pick
|
||||
}
|
||||
|
||||
// offeredCount is how many of the asker's asks are offered now: open, or held for want of a channel.
|
||||
func (h *Holder) offeredCount(asker string) int {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
n := 0
|
||||
for _, e := range h.asks {
|
||||
if e.rec.Asker == asker && (e.rec.State == StateOpen || e.rec.State == StateHeld) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// freshAsk is an ask's record read again from the store, where there is one.
|
||||
func (h *Holder) freshAsk(key string) (AskRecord, uint64, bool) {
|
||||
e := h.askOf(key)
|
||||
if e == nil {
|
||||
return AskRecord{}, 0, false
|
||||
}
|
||||
rec, rev := e.rec, e.rev
|
||||
if h.Asks != nil {
|
||||
if got, r, err := h.Asks.GetAsk(key); err == nil && got != nil {
|
||||
rec, rev = *got, r
|
||||
}
|
||||
}
|
||||
return rec, rev, true
|
||||
}
|
||||
|
||||
// holdBackOldest holds back the asker's oldest offered ask other than except (the newest, just taken): its tickets
|
||||
// are withdrawn, it stays open, and its own message is edited to say it waits. One held for want of a channel counts as offered
|
||||
// and is held back the same way. The work lock is held.
|
||||
func (h *Holder) holdBackOldest(asker, except string) {
|
||||
h.mu.Lock()
|
||||
key := h.pickLocked(asker, except, StateOpen, StateHeld)
|
||||
h.mu.Unlock()
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
rec, rev, ok := h.freshAsk(key)
|
||||
if !ok || (rec.State != StateOpen && rec.State != StateHeld) {
|
||||
return
|
||||
}
|
||||
now := h.Now()
|
||||
rec.State, rec.HeldBackSince, rec.Tickets, rec.HeldSince, rec.HeldWhy = StateHeldBack, now, nil, time.Time{}, ""
|
||||
if !h.saveHeld(key, rec, rev) {
|
||||
return
|
||||
}
|
||||
h.Logf("[messenger] %s's ask %q (%s) is held back behind its newer asks: it is offered again when one ends", asker, rec.Ask.Headline, rec.Ask.ID)
|
||||
h.mu.Lock()
|
||||
r := h.open[rec.Record]
|
||||
h.mu.Unlock()
|
||||
if r == nil || r.Origin != "ask" {
|
||||
return // a condition's message is the newer ask's to show
|
||||
}
|
||||
h.mu.Lock()
|
||||
r.Answered = "Waits behind newer questions of " + asker + ".\nIt is offered again, with its answers, when one of them ends."
|
||||
h.mu.Unlock()
|
||||
h.edit(r, "answered", false)
|
||||
h.persist(r)
|
||||
}
|
||||
|
||||
// offerHeldBack offers the asker's oldest held-back ask again when a slot is free: new tickets, its message edited back
|
||||
// to the question. The work lock is held.
|
||||
func (h *Holder) offerHeldBack(asker string) {
|
||||
if h.offeredCount(asker) >= occ.MostOpen {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
key := h.pickLocked(asker, "", StateHeldBack)
|
||||
h.mu.Unlock()
|
||||
if key != "" {
|
||||
h.offerOneHeldBack(key)
|
||||
}
|
||||
}
|
||||
|
||||
// offerOneHeldBack offers one held-back ask again now.
|
||||
func (h *Holder) offerOneHeldBack(key string) {
|
||||
rec, rev, ok := h.freshAsk(key)
|
||||
if !ok || rec.State != StateHeldBack {
|
||||
return
|
||||
}
|
||||
now := h.Now()
|
||||
rec.HeldBackSince = time.Time{}
|
||||
rec.Tickets = h.ticketsFor(rec.Ask)
|
||||
rec.State = StateOpen
|
||||
if len(rec.Tickets) == 0 {
|
||||
rec.Tickets, rec.State, rec.HeldSince, rec.HeldWhy = nil, StateHeld, now, h.holdWhy(rec.Ask)
|
||||
}
|
||||
if !h.saveHeld(key, rec, rev) {
|
||||
return
|
||||
}
|
||||
h.Logf("[messenger] %s's ask %q (%s) is offered again", rec.Asker, rec.Ask.Headline, rec.Ask.ID)
|
||||
if rec.State != StateOpen {
|
||||
return
|
||||
}
|
||||
h.mu.Lock()
|
||||
r := h.open[rec.Record]
|
||||
h.mu.Unlock()
|
||||
if r != nil && r.Origin == "ask" {
|
||||
h.mu.Lock()
|
||||
r.Answered, r.Ask = "", key
|
||||
h.mu.Unlock()
|
||||
h.offerAsk(r)
|
||||
h.persist(r)
|
||||
return
|
||||
}
|
||||
rec.Record = h.recordFor(rec.Asker, key, rec.Ask)
|
||||
h.showAsk(key, rec, now)
|
||||
}
|
||||
|
||||
// AskAgain is the seat operator-channel's verb ask-again (G §394, §450): offer a held-back ask now, holding back the
|
||||
// asker's oldest offered ask in its place when the asker already has the most offered. It answers the ask's state after it,
|
||||
// or the error that says why it was not offered.
|
||||
func (h *Holder) AskAgain(key string) oc.AskAgainAnswer {
|
||||
h.work.Lock()
|
||||
defer h.work.Unlock()
|
||||
h.mu.Lock()
|
||||
h.init()
|
||||
h.mu.Unlock()
|
||||
rec, _, ok := h.freshAsk(key)
|
||||
switch {
|
||||
case !ok:
|
||||
return oc.AskAgainAnswer{Error: fmt.Sprintf("this router holds no ask %q: its key is <asker>.<id>, as operator-channel.asks lists", key)}
|
||||
case rec.State != StateHeldBack:
|
||||
return oc.AskAgainAnswer{Error: fmt.Sprintf("the ask %s is %s, not held back: only a held-back ask is offered again", key, rec.State)}
|
||||
}
|
||||
if h.offeredCount(rec.Asker) >= occ.MostOpen {
|
||||
h.holdBackOldest(rec.Asker, key)
|
||||
}
|
||||
h.offerOneHeldBack(key)
|
||||
got, _, _ := h.freshAsk(key)
|
||||
return oc.AskAgainAnswer{State: string(got.State)}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
)
|
||||
|
||||
// ADR 0666 §12, design 46 §6 and test 71: an asker has at most three asks offered at once; a fourth is offered and the
|
||||
// oldest is held back (its tickets withdrawn, its message edited, its record open), offered again with new tickets
|
||||
// when one of the three ends, offered now by messenger_ask_again; an ask with the same `about` replaces a held-back
|
||||
// one, and a Decline ends an ask whose asker may ask again at once.
|
||||
func TestNoAskIsHiddenTheOldestOfFourIsHeldBackAndOfferedAgain(t *testing.T) {
|
||||
h, tg, _, c, store, w := newRouter(t)
|
||||
link(h, store, c, "42", c.now().Add(-2*time.Hour))
|
||||
ask := func(id, about string) {
|
||||
a := ackAsk(c, id)
|
||||
a.About = about
|
||||
submit(t, h, "mesh-delivery", a)
|
||||
c.pass(time.Second)
|
||||
settle(h, c)
|
||||
}
|
||||
state := func(id string) string { return store.asks["mesh-delivery."+id].State }
|
||||
for i, id := range []string{"d-1", "d-2", "d-3"} {
|
||||
ask(id, "about-"+string(rune('1'+i)))
|
||||
}
|
||||
oldTicket := ""
|
||||
for tk := range store.asks["mesh-delivery.d-1"].Tickets {
|
||||
oldTicket = tk
|
||||
}
|
||||
ask("d-4", "about-4")
|
||||
// M1: the fourth reaches the channel, the oldest is held back, none is refused.
|
||||
if len(w.of("mesh-delivery")) != 0 || state("d-4") != StateOpen || state("d-1") != StateHeldBack {
|
||||
t.Fatalf("refused %+v; d-4 %s, d-1 %s", w.of("mesh-delivery"), state("d-4"), state("d-1"))
|
||||
}
|
||||
if rec := store.asks["mesh-delivery.d-1"]; len(rec.Tickets) != 0 || rec.HeldBackSince.IsZero() {
|
||||
t.Errorf("held back %+v: tickets withdrawn, since set", rec)
|
||||
}
|
||||
if m, ok := tg.edits["1"]; !ok || len(m.Actions) != 0 || !strings.Contains(m.Note, "Waits behind newer questions of mesh-delivery") {
|
||||
t.Errorf("its message was not edited to say it waits: %v %+v", ok, m)
|
||||
}
|
||||
if _, err := h.Chosen("telegram", choiceOf(oldTicket, "42", c.now())); err == nil || !strings.Contains(err.Error(), "waits behind newer questions") || len(w.of("mesh-delivery")) != 0 {
|
||||
t.Errorf("a tap of a withdrawn ticket: %v %+v", err, w.of("mesh-delivery"))
|
||||
}
|
||||
// M3: asks lists it with since when and the newer asks it waits behind; open includes it.
|
||||
q, _ := AsksQueryOf(map[string]any{"key": "mesh-delivery.d-1"})
|
||||
raw, _ := json.Marshal(q.Narrow(h.AsksNow()))
|
||||
for _, want := range []string{`"state":"held-back"`, `"held-since":"`, `"behind":["mesh-delivery.d-2","mesh-delivery.d-3","mesh-delivery.d-4"]`} {
|
||||
if !strings.Contains(string(raw), want) {
|
||||
t.Errorf("asks lacks %s: %s", want, raw)
|
||||
}
|
||||
}
|
||||
if q, _ = AsksQueryOf(nil); !strings.Contains(string(mustJSON(q.Narrow(h.AsksNow()))), "mesh-delivery.d-1") {
|
||||
t.Error("the open asks leave out the held-back one")
|
||||
}
|
||||
// M5: an ask with the same about replaces a held-back one; the three offered stay three, so the oldest of them is held back.
|
||||
ask("d-5", "about-1")
|
||||
if state("d-1") != string(occ.OutcomeReplaced) || state("d-2") != StateHeldBack || state("d-5") != StateOpen || state("d-3") != StateOpen {
|
||||
t.Fatalf("after d-5 about the held-back ask's thing: d-1 %s d-2 %s d-3 %s d-5 %s", state("d-1"), state("d-2"), state("d-3"), state("d-5"))
|
||||
}
|
||||
// M2: when one of the offered ends, the oldest held back is offered again with new tickets and its message asks again.
|
||||
if a := h.AskAgain("mesh-delivery.d-3"); !strings.Contains(a.Error, "not held back") {
|
||||
t.Errorf("ask-again of an offered ask: %+v", a)
|
||||
}
|
||||
if err := h.TakeAsk("cancel", "mesh-delivery", []byte(`{"id":"d-3"}`)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settle(h, c)
|
||||
if rec := store.asks["mesh-delivery.d-2"]; rec.State != StateOpen || len(rec.Tickets) == 0 || !rec.HeldBackSince.IsZero() {
|
||||
t.Fatalf("d-2 after d-3 ended: %+v", rec)
|
||||
}
|
||||
if m := tg.edits["2"]; len(m.Actions) == 0 || strings.Contains(m.Note, "Waits behind") {
|
||||
t.Errorf("its message does not ask again: %+v", m)
|
||||
}
|
||||
// M4: ask-again by hand offers a held-back ask now, holding back the oldest offered one; the seat operator-channel's ask-again (G §450).
|
||||
ask("d-6", "about-6")
|
||||
ask("d-7", "about-7")
|
||||
held := ""
|
||||
for _, id := range []string{"d-2", "d-4", "d-5", "d-6", "d-7"} {
|
||||
if state(id) == StateHeldBack {
|
||||
held = id
|
||||
}
|
||||
}
|
||||
if held == "" {
|
||||
t.Fatal("no ask is held back after two more")
|
||||
}
|
||||
if a := h.AskAgain("mesh-delivery.d-none"); a.Error == "" || a.State != "" {
|
||||
t.Errorf("ask-again of an ask the router holds none of: %+v", a)
|
||||
}
|
||||
if a := h.AskAgain("mesh-delivery." + held); a.Error != "" || a.State != string(StateOpen) || state(held) != StateOpen {
|
||||
t.Fatalf("%s is %s; ask-again answered %+v", held, state(held), a)
|
||||
}
|
||||
n := 0
|
||||
for _, id := range []string{"d-2", "d-4", "d-5", "d-6", "d-7"} {
|
||||
if state(id) == StateHeldBack {
|
||||
n++
|
||||
}
|
||||
}
|
||||
if n != 2 || state("d-5") != StateHeldBack {
|
||||
t.Errorf("%d asks held back after ask-again (d-5 %s): the oldest offered one took its place", n, state("d-5"))
|
||||
}
|
||||
// A Decline ends an ask; its asker may ask again at once, and nothing is refused.
|
||||
for _, wt := range w.of("mesh-delivery") {
|
||||
if wt.Outcome == occ.OutcomeRefused {
|
||||
t.Errorf("refused: %+v", wt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustJSON(v any) []byte { raw, _ := json.Marshal(v); return raw }
|
||||
@@ -741,7 +741,7 @@ func tools(h *Holder, l *listening) []stdio.Tool {
|
||||
"most bytes of the answer.",
|
||||
Input: map[string]any{
|
||||
"key": str("one ask, by its key (asker.id); none for the open asks"),
|
||||
"state": str("open (open and held), ended or all; none is open when no key or since is given, else both"),
|
||||
"state": str("open (open, held and held-back), ended or all; none is open when no key or since is given, else both"),
|
||||
"since": str("only the asks opened at or after this time, RFC 3339"),
|
||||
"bound": map[string]any{"type": "integer", "description": "the most bytes of the asks in the answer; said when it cuts the answer short"}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
@@ -751,6 +751,15 @@ func tools(h *Holder, l *listening) []stdio.Tool {
|
||||
}
|
||||
return q.Narrow(h.AsksNow()), nil
|
||||
}},
|
||||
{Name: "operator-channel." + oc.VerbAskAgain,
|
||||
Description: "Offer a held-back ask again now (G §394, §450): an asker has at most three asks offered at once, and a fourth " +
|
||||
"holds its oldest back behind it (operator-channel.asks lists it with state held-back, since when and the newer asks it waits behind). " +
|
||||
"It is offered again by itself when one of the asker's asks ends; this offers it now, holding back the asker's oldest offered ask in its " +
|
||||
"place. Answers the ask's state, or why it was not offered (an ask that is not held back).",
|
||||
Input: map[string]any{"key": str("the held-back ask, by its key (asker.id)")},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
return h.AskAgain(strArg(a, "key")), nil
|
||||
}},
|
||||
{Name: "messenger_identities",
|
||||
Description: "The accounts linked as the operator, one per channel kind: the account, the name its " +
|
||||
"service shows, when it was linked, and from when it can approve (an hour after linking).",
|
||||
|
||||
@@ -68,7 +68,7 @@ func readManifest(t *testing.T) (manifest, string) {
|
||||
|
||||
func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { // 1217/AC6: condition beside ask and cancel
|
||||
m, _ := readManifest(t)
|
||||
verbs := []string{"open", "history", "notify", "tell", "asks"}
|
||||
verbs := []string{"open", "history", "notify", "tell", "asks", oc.VerbAskAgain}
|
||||
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"}},
|
||||
|
||||
@@ -3,6 +3,7 @@ module messenger
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
|
||||
@@ -12,6 +13,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
|
||||
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
|
||||
github.com/google/go-tpm v0.9.8 // indirect
|
||||
github.com/klauspost/compress v1.20.1 // indirect
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"history",
|
||||
"notify",
|
||||
"tell",
|
||||
"asks"
|
||||
"asks",
|
||||
"ask-again"
|
||||
],
|
||||
"accepts": [
|
||||
"ask",
|
||||
@@ -66,7 +67,8 @@
|
||||
"history",
|
||||
"notify",
|
||||
"tell",
|
||||
"asks"
|
||||
"asks",
|
||||
"ask-again"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user