mesh-recovery: the seat's verbs, answered by the process with the proved caller
Builds: design 47 G §516, §554, §656, §682, §691, A-107, A-115 Each acting verb answers one of accepted, busy or error; arguments are read afresh per call and refused when unknown (local-name, from, aside-only, discard-aside) or without why; restore takes exactly one of snapshot and request (request is refused until built); the hand-act log names the proved account and the asker. Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshrecovery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
)
|
||||
|
||||
// Handlers are the seat's verbs as the process answers them over the bus with the caller the bus proved (§554, §656, A-107):
|
||||
// every acting verb answers one of accepted, busy or error and nothing more (§691); the result is the event.
|
||||
func (e *Engine) Handlers() map[string]process.SeatHandler {
|
||||
h := map[string]process.SeatHandler{}
|
||||
h[seat.VerbSnapshotKept] = actOf(e, func(sk seat.SnapshotKeptInput) (Act, error) {
|
||||
return Act{Kind: KindTake, Module: sk.Module, Kept: sk.Kept, Form: sk.Form, Fingerprint: sk.Fingerprint, CatchUp: sk.CatchUp, Why: sk.Why}, nil
|
||||
})
|
||||
restoreKept := actOf(e, func(rk seat.RestoreKeptInput) (Act, error) {
|
||||
return Act{Kind: KindRestore, Module: rk.Module, Kept: rk.Kept, Form: rk.Form, Fingerprint: rk.Fingerprint, Snapshot: rk.Snapshot, FromBackup: rk.FromBackup, Why: rk.Why}, nil
|
||||
})
|
||||
h[seat.VerbRestoreKept] = func(caller string, body json.RawMessage) (any, error) {
|
||||
// aside-only and discard-aside are the operator's hand tools alone (§516, §554).
|
||||
var loose map[string]json.RawMessage
|
||||
_ = json.Unmarshal(body, &loose)
|
||||
for _, k := range []string{"aside-only", "discard-aside"} {
|
||||
if _, ok := loose[k]; ok {
|
||||
return Answer{Error: k + " is the operator's hand tool alone, not an argument of restore-kept"}, nil
|
||||
}
|
||||
}
|
||||
return restoreKept(caller, body)
|
||||
}
|
||||
h[seat.VerbKeptPoints] = func(_ string, body json.RawMessage) (any, error) {
|
||||
var in seat.KeptPointsInput
|
||||
if err := strict(body, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.KeptPoints(in.Module, in.Kept)
|
||||
}
|
||||
h[seat.VerbTake] = actOf(e, func(tk seat.TakeInput) (Act, error) {
|
||||
return Act{Kind: KindTake, Module: tk.Module, Kept: tk.Kept, Fingerprint: tk.Fingerprint, Why: tk.Why, Asker: tk.Asker}, nil
|
||||
})
|
||||
h[seat.VerbRestore] = actOf(e, func(rs seat.RestoreInput) (Act, error) {
|
||||
if (rs.Snapshot == "") == (rs.Request == "") {
|
||||
return Act{}, errors.New("restore takes exactly one of snapshot and request")
|
||||
}
|
||||
if rs.Request != "" {
|
||||
return Act{}, errors.New("the restore of a keeper's snapshot by request is not built yet (design 47 G §681)")
|
||||
}
|
||||
return Act{Kind: KindRestore, Module: rs.Module, Kept: rs.Kept, Snapshot: rs.Snapshot, FromBackup: rs.FromBackup, Why: rs.Why, Asker: rs.Asker}, nil
|
||||
})
|
||||
h[seat.VerbAside] = actOf(e, func(ad seat.AsideInput) (Act, error) {
|
||||
return Act{Kind: KindAside, Module: ad.Module, Kept: ad.Kept, Why: ad.Why, Asker: ad.Asker}, nil
|
||||
})
|
||||
h[seat.VerbDiscardAside] = actOf(e, func(da seat.DiscardAsideInput) (Act, error) {
|
||||
return Act{Kind: KindDiscardAside, Module: da.Module, Kept: da.Kept, AsideID: da.ID, Why: da.Why, Asker: da.Asker}, nil
|
||||
})
|
||||
h[seat.VerbRestoreAside] = actOf(e, func(ra seat.RestoreAsideInput) (Act, error) {
|
||||
return Act{Kind: KindRestoreAside, Module: ra.Module, Kept: ra.Kept, AsideID: ra.ID, Why: ra.Why, Asker: ra.Asker}, nil
|
||||
})
|
||||
h[seat.VerbExpirePoints] = actOf(e, func(ex seat.ExpirePointsInput) (Act, error) {
|
||||
return Act{Kind: KindExpire, Why: ex.Why, Asker: ex.Asker}, nil
|
||||
})
|
||||
h[seat.VerbRetry] = func(caller string, body json.RawMessage) (any, error) {
|
||||
var in seat.RetryInput
|
||||
if err := strict(body, &in); err != nil {
|
||||
return Answer{Error: err.Error()}, nil
|
||||
}
|
||||
return e.Retry(in.Module, in.Kept, in.Why, caller, in.Asker), nil
|
||||
}
|
||||
h[seat.VerbCancel] = func(caller string, body json.RawMessage) (any, error) {
|
||||
var in seat.CancelInput
|
||||
if err := strict(body, &in); err != nil {
|
||||
return Answer{Error: err.Error()}, nil
|
||||
}
|
||||
return e.Cancel(in.Act, in.Why, caller, in.Asker), nil
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// actOf is one acting verb: its arguments are read afresh for each call (calls come on concurrent bus callbacks), a call
|
||||
// without the `why` the hand-act log keeps is refused, and the act is accepted as the engine says.
|
||||
func actOf[T any](e *Engine, build func(T) (Act, error)) process.SeatHandler {
|
||||
return func(caller string, body json.RawMessage) (any, error) {
|
||||
var in T
|
||||
if err := strict(body, &in); err != nil {
|
||||
return Answer{Error: err.Error()}, nil
|
||||
}
|
||||
a, err := build(in)
|
||||
if err != nil {
|
||||
return Answer{Error: err.Error()}, nil
|
||||
}
|
||||
if a.Why == "" {
|
||||
return Answer{Error: "why is required: the hand-act log keeps it"}, nil
|
||||
}
|
||||
a.Caller = caller
|
||||
return e.Accept(a), nil
|
||||
}
|
||||
}
|
||||
|
||||
// strict reads a call's arguments, refusing a field the verb does not take (no `local-name`, no `from`: §516) and a call
|
||||
// that leaves out the `why` the hand-act log keeps (§682).
|
||||
func strict(body json.RawMessage, into any) error {
|
||||
dec := json.NewDecoder(bytes.NewReader(body))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(into); err != nil {
|
||||
return fmt.Errorf("the arguments are refused: %s", strings.TrimPrefix(err.Error(), "json: "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package recovery_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
seat "git.novox.be/novox/mesh-contracts/go/seats/meshrecovery"
|
||||
|
||||
"mesh-recovery/internal/recovery"
|
||||
)
|
||||
|
||||
func call(t *testing.T, r *rig, verb, body string) recovery.Answer {
|
||||
t.Helper()
|
||||
out, err := r.e.Handlers()[verb]("node.engine", json.RawMessage(body))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", verb, err)
|
||||
}
|
||||
r.e.Wait()
|
||||
return out.(recovery.Answer)
|
||||
}
|
||||
|
||||
// Every verb of the seat's definition is answered; an acting verb answers one of accepted, busy and error and nothing more
|
||||
// (§691); an argument the verb does not take (`local-name`, `from`), `aside-only` and `discard-aside` on restore-kept, a
|
||||
// restore naming both a snapshot and a request, and a call without `why` are refused naming the fault (§516, §682, §554).
|
||||
func TestEveryVerbIsAnsweredAndAnActingVerbAnswersOneForm(t *testing.T) {
|
||||
r := newRig(t)
|
||||
h := r.e.Handlers()
|
||||
for _, v := range seat.Verbs() {
|
||||
if h[v] == nil {
|
||||
t.Errorf("verb %s has no handler", v)
|
||||
}
|
||||
}
|
||||
taken := call(t, r, "snapshot-kept", `{"module":"app","kept":"notes","form":2,"fingerprint":"fp1","why":"form change"}`)
|
||||
if taken.Accepted == "" || taken.Busy != "" || taken.Error != "" {
|
||||
t.Fatalf("answer %+v", taken)
|
||||
}
|
||||
for name, c := range map[string]struct{ verb, body, want string }{
|
||||
"local-name": {"snapshot-kept", `{"module":"app","kept":"notes","form":2,"fingerprint":"fp1","why":"w","local-name":"x"}`, "local-name"},
|
||||
"from": {"restore-kept", `{"module":"app","kept":"notes","form":2,"fingerprint":"fp1","why":"w","from":"x"}`, "from"},
|
||||
"aside-only": {"restore-kept", `{"module":"app","kept":"notes","form":2,"fingerprint":"fp1","why":"w","aside-only":true}`, "aside-only"},
|
||||
"discard": {"restore-kept", `{"module":"app","kept":"notes","form":2,"fingerprint":"fp1","why":"w","discard-aside":true}`, "discard-aside"},
|
||||
"both": {"restore", `{"module":"app","kept":"notes","snapshot":"a","request":"b","why":"w"}`, "exactly one"},
|
||||
"no why": {"restore", `{"module":"app","kept":"notes","snapshot":"a"}`, "why"},
|
||||
} {
|
||||
if got := call(t, r, c.verb, c.body); !strings.Contains(got.Error, c.want) || got.Accepted != "" {
|
||||
t.Errorf("%s: answer %+v", name, got)
|
||||
}
|
||||
}
|
||||
if len(r.s.named(seat.EventEnded)) != 1 {
|
||||
t.Errorf("a refused call acted: %+v", r.s.events)
|
||||
}
|
||||
out, err := h["kept-points"]("tools", json.RawMessage(`{"module":"app"}`))
|
||||
if kp, ok := out.(seat.KeptPointsAnswer); err != nil || !ok || len(kp.Points) != 1 {
|
||||
t.Errorf("kept-points %+v %v", out, err)
|
||||
}
|
||||
if got := call(t, r, "take", `{"module":"app","kept":"notes","fingerprint":"fp9","why":"by hand","asker":"op@shanks"}`); got.Accepted == "" {
|
||||
t.Errorf("take %+v", got)
|
||||
}
|
||||
if !strings.Contains(r.s.handActs[len(r.s.handActs)-1], "node.engine|op@shanks|take app notes|by hand") {
|
||||
t.Errorf("hand acts %v: the account the bus proved and the asker are both logged", r.s.handActs)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user