NATS8: the bus module's ask lasts its level's bound and a refused ask ends its request with the router's words

Builds: design 47 G §570

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 04:56:56 +01:00
parent 0699741fab
commit 24778d24b6
3 changed files with 46 additions and 5 deletions
@@ -145,3 +145,21 @@ func TestAPersonsRequestIsHandedToTheKeeperAsTheProvedCaller(t *testing.T) {
t.Errorf("an end not written: %+v on %d watch(es)", r, watches)
}
}
// The ask for a recreate or a drop lasts what its highest option level allows (design 47 G §570), so the asker client
// takes it: an expiry at the bound passes occ.Check, and the fixed 12 hours it was is refused.
func TestTheAskLastsWhatItsLevelAllows(t *testing.T) {
now := time.Now()
r := keeper.Request{ID: "r1", Verb: keeper.VerbDrop, Module: "m", Local: "drafts", Why: "unused", Caller: "person.jo"}
a, err := askFor(r, now)
if err != nil {
t.Fatal(err)
}
if err := occ.Check(a, now); err != nil {
t.Errorf("the ask is refused by the asker client: %v", err)
}
a.Expires = occ.FormatTime(now.Add(12 * time.Hour))
if err := occ.Check(a, now); err == nil || !strings.Contains(err.Error(), "at most") {
t.Errorf("12 hours is held to the level's bound: %v", err)
}
}
+17 -1
View File
@@ -656,6 +656,22 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) {
}},
// CAT-N8: a person's requests: a drop removes an orphan alone, only on the operator's warrant binding exactly
// that drop, after a snapshot, said dropped and recorded; a recreate rebuilds a drifted entry the same way.
{"an ask the router refused ends its request with the router's words (§570)", func(t *testing.T, k *Keeper, bus *fakeBus, out *said) {
store := k.Store.(*memStore)
k.Heard(Accepted{Module: "m", Revision: 1, BusState: []catalogue.BusStateEntry{drafts}})
k.Heard(Accepted{Module: "m", Revision: 2, BusState: nil})
w := &oc.Warrant{Ask: "bus-1", Asker: Module, Outcome: oc.WarrantOutcomeRefused, Words: "an ask that destroys lasts at most 10m0s"}
k.Warrant = func(string) (*oc.Ask, *oc.Warrant, error) { return nil, w, nil }
raw, _ := json.Marshal(Request{ID: "rr", Verb: VerbDrop, Module: "m", Local: "drafts", Ask: "bus-1", Caller: "person.jo", At: k.now()})
_, _ = store.Put(StateRequests, "rr", raw)
k.ActOnRequests()
raw, _, _, _ = store.Get(StateRequests, "rr")
var got Request
_ = json.Unmarshal(raw, &got)
if got.Done.IsZero() || got.Waiting != "" || !strings.Contains(got.Error, "refused") || !strings.Contains(got.Error, w.Words) || len(bus.deleted) != 0 {
t.Errorf("ended %+v, deleted %v: the router's refusal and its words, nothing removed", got, bus.deleted)
}
}},
{"a drop waits for the warrant, refuses another act's, then acts after a snapshot", func(t *testing.T, k *Keeper, bus *fakeBus, out *said) {
store := k.Store.(*memStore)
k.Heard(Accepted{Module: "m", Revision: 1, BusState: []catalogue.BusStateEntry{drafts, log}})
@@ -994,7 +1010,7 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) {
_ = json.Unmarshal(raw, &got)
return got
}
raw, _ := json.Marshal(Request{ID: "r7", Verb: VerbDrop, Module: "m", Local: "drafts", Ask: "bus-7", Caller: "person.jo", At: k.now().Add(-time.Hour)})
raw, _ := json.Marshal(Request{ID: "r7", Verb: VerbDrop, Module: "m", Local: "drafts", Ask: "bus-7", Caller: "person.jo", At: k.now().Add(-AskLasts / 2)})
_, _ = store.Put(StateRequests, "r7", raw)
raw, _ = json.Marshal(Request{ID: "r8", Verb: VerbDrop, Module: "m", Local: "drafts", Ask: "bus-8", Caller: "person.jo", At: k.now().Add(-AskLasts - time.Minute)})
_, _ = store.Put(StateRequests, "r8", raw)
+11 -4
View File
@@ -9,6 +9,7 @@ import (
"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"
)
// A person's request (design 25 § 12.3; CAT-N8): the bus module's tools hold no grant of the seat's acting verbs, so
@@ -67,10 +68,11 @@ type Request struct {
Unrecorded string `json:"unrecorded,omitempty"`
}
// AskLasts is how long the operator's ask for a recreate or a drop waits for an answer: shorter than the day the state
// requests keeps a key, so a request whose ask ended unanswered is ended by the keeper, saying so, before its key ages
// out, and the warrant's acted key (ActedKey) outlives the ask it spends.
const AskLasts = 12 * time.Hour
// AskLasts is how long the operator's ask for a recreate or a drop waits for an answer: the bound of its highest option
// level, destroy (design 47 G §570: no asker asks for longer than its level allows; the asker client refuses more). It is
// shorter than the day the state requests keeps a key, so a request whose ask ended unanswered is ended by the keeper,
// saying so, before its key ages out, and the warrant's acted key (ActedKey) outlives the ask it spends.
const AskLasts = occ.DestroyLasts
// ActedPrefix started the spend record's key while it was kept in StateRequests (`acted.<ask id>`); a key of that form
// still met there is no request and is skipped.
@@ -288,6 +290,11 @@ func (k *Keeper) warranted(r *Request) error {
if err != nil {
return fmt.Errorf("%w: the router's record of the ask %s cannot be read now: %v", ErrNoWarrant, r.Ask, err)
}
// An ask the router refused at take-in ends its request with the router's words, never "not answered yet" (design 47 G
// §570).
if w != nil && w.Outcome == oc.WarrantOutcomeRefused {
return fmt.Errorf("the router refused the operator's ask %s, so nothing is removed: %s", r.Ask, w.Words)
}
// An ask that ended unanswered ends its request, said: never a request that waits until its key ages out. And a
// warrant is not acted on once its ask ended, so a request handed after its acted key aged out acts on nothing.
var expires time.Time