diff --git a/modules/nats/cmd/nats-tools/requests_test.go b/modules/nats/cmd/nats-tools/requests_test.go index 63148231e..474f06fbb 100644 --- a/modules/nats/cmd/nats-tools/requests_test.go +++ b/modules/nats/cmd/nats-tools/requests_test.go @@ -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) + } +} diff --git a/modules/nats/internal/keeper/keeper_test.go b/modules/nats/internal/keeper/keeper_test.go index 298f57322..31a5105b5 100644 --- a/modules/nats/internal/keeper/keeper_test.go +++ b/modules/nats/internal/keeper/keeper_test.go @@ -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) diff --git a/modules/nats/internal/keeper/requests.go b/modules/nats/internal/keeper/requests.go index ef07a9742..b9b87803e 100644 --- a/modules/nats/internal/keeper/requests.go +++ b/modules/nats/internal/keeper/requests.go @@ -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.`); 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