The account of the module claiming the seat mesh-recovery is granted the stream snapshot and restore subjects with their chunk subjects, a stream's deletion and a durable consumer's creation, only for the bus names of every kept data of every manifest the part holds and of every restore point or aside kept-points names; never a stream of the mesh's own (CONTROL, NODES, EVENTS, DEAD_LETTER*, PROV_APPLIED, CHECKS_*, SEAT_*, a seat's KV_). The writers table names the account as a deleter of kept data's streams (ADR 0672 §4). Builds: design 47 G §554, §565, A-230 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
79 lines
3.8 KiB
Go
79 lines
3.8 KiB
Go
package grants
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// Design 47 G §565, A-230: the part reads points[].names and asides[].names of kept-points, keeps them in its state memberships,
|
|
// grants the names it kept until the call first answers after a start, takes a name away only by an answer, and asks again on
|
|
// the seat's ended (outcome done), point-removed and expiry-ended alone.
|
|
func TestKeptPointsNamesAreKeptUntilTheCallAnswers(t *testing.T) {
|
|
store := memStore{}
|
|
answer := func(s string) func(context.Context) (json.RawMessage, error) {
|
|
return func(context.Context) (json.RawMessage, error) { return json.RawMessage(s), nil }
|
|
}
|
|
p := &Part{Store: store, Now: func() time.Time { return time.Unix(100, 0) }}
|
|
v := &KeptPointsView{Ask: answer(`{"points":[{"names":["KV_a_x","KV_b_y"]}],"asides":[{"names":["OBJ_c_z"]}]}`)}
|
|
p.KeptPoints = v
|
|
v.askUntil(context.Background(), p, "start")
|
|
var in Inputs
|
|
if err := p.withKeptNames(&in); err != nil || !slices.Equal(in.KeptNames, []string{"KV_a_x", "KV_b_y", "OBJ_c_z"}) {
|
|
t.Fatalf("names after the first answer %v (%v)", in.KeptNames, err)
|
|
}
|
|
// A restart: the call fails, and the names last kept stay; the status says it waits.
|
|
p2 := &Part{Store: store}
|
|
failing := &KeptPointsView{Ask: func(context.Context) (json.RawMessage, error) { return nil, errors.New("no responders") },
|
|
Pause: func(context.Context, time.Duration) bool { return false }}
|
|
p2.KeptPoints = failing
|
|
failing.Start(context.Background(), p2)
|
|
failing.askUntil(context.Background(), p2, "start")
|
|
in = Inputs{}
|
|
if err := p2.withKeptNames(&in); err != nil || len(in.KeptNames) != 3 {
|
|
t.Fatalf("names kept before the call answers %v (%v)", in.KeptNames, err)
|
|
}
|
|
if s := failing.Status(); !s.Waiting || s.Names != 3 || s.Error == "" {
|
|
t.Errorf("status %+v, want waiting with 3 names and the error", s)
|
|
}
|
|
// An answer takes a name away that no point names any more.
|
|
failing.Ask = answer(`{"points":[{"names":["KV_a_x"]}],"asides":[]}`)
|
|
failing.askUntil(context.Background(), p2, "the seat's point-removed")
|
|
in = Inputs{}
|
|
_ = p2.withKeptNames(&in)
|
|
if !slices.Equal(in.KeptNames, []string{"KV_a_x"}) || failing.Status().Waiting {
|
|
t.Errorf("names after the answer %v, status %+v", in.KeptNames, failing.Status())
|
|
}
|
|
// Which events ask again.
|
|
for event, body := range map[string]string{"mesh-recovery.ended": `{"outcome":"done"}`, "mesh.seat.mesh-recovery.event.point-removed": `{}`,
|
|
"mesh-recovery.expiry-ended": `{}`} {
|
|
if !IsKeptPointsEvent(event, []byte(body)) {
|
|
t.Errorf("%s does not ask again", event)
|
|
}
|
|
}
|
|
for event, body := range map[string]string{"mesh-recovery.ended": `{"outcome":"failed"}`, "mesh-recovery.restored": `{"outcome":"done"}`, "mesh-broker.created": `{}`} {
|
|
if IsKeptPointsEvent(event, []byte(body)) {
|
|
t.Errorf("%s asks again", event)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Design 47 G §554, §565: the names the recovery account is granted are those of every manifest of the catalog graph (an old
|
|
// section's and a template entry's) and those a point names, once each; a name a manifest left stays while a point names it.
|
|
func TestTheRecoveryNamesAreTheCatalogGraphsAndThePointsNames(t *testing.T) {
|
|
trunk := catalogue.Manifest{Module: "a", State: []catalogue.StateDeclaration{{Name: "x"}}}
|
|
old := catalogue.Manifest{Module: "a", State: []catalogue.StateDeclaration{{Name: "old"}},
|
|
BusState: []catalogue.BusStateEntry{{Name: "tpl", Storage: catalogue.StorageKeyValue}}}
|
|
got := recoveryNamesOf(Inputs{Manifests: map[string]catalogue.Manifest{"a": trunk}, RanByFingerprint: map[string]catalogue.Manifest{"fp": old},
|
|
KeptNames: []string{"KV_a_gone", "KV_a_x"}})
|
|
want := []string{"KV_a_gone", "KV_a_old", "KV_a_tpl", "KV_a_x"}
|
|
if !slices.Equal(got, want) {
|
|
t.Errorf("names %v, want %v", got, want)
|
|
}
|
|
}
|