Research 031 counted the repairs people made by hand: a push to unstick a plan waiting on a report, a controller restarted to make an object again, a plan closed, a consumer re-made from now. Each was the ordinary path taken again by someone who noticed. The healer registry makes each a registered response to one condition kind, with a budget, a settle and its event: - H1 sent-not-reported: ask the machine's node-engine to report again (mesh.node.<n>.ask.report); if it does not report what it was sent, send it again, never moving a build a policy or a plan holds back - H2 stalled: close a plan whose wait is superseded or finished - H3 holder-silent / consumer-lost: the send's own assertion of the bus's objects (issue 208's note) - H4 consumer-behind: consumer-reset, only for a consumer the stream table marks resettable (the controller's own events consumer) - H5 is the identity provider's own repair (ADR 0224 §5), registered only Success is the observation clearing the condition, never the healer; a spent budget hands the condition to the operator, urgent, with what was tried, and no healer touches it again. Every act is begun in the store before it is made (migration 0070), kept in the condition's tried as "healer Hn" and said as the seat event healer-acted; a heal is never a hand act. More than twelve acts in an hour stop every healer until an hour after the last, said urgently. Only the lease holder heals. S15 is live: a cause repaired by hand twice in a fortnight raises healer-wanted, naming the healer that was not enough where one exists. D6's far-behind finding has its own kind, consumer-behind. Nodes are granted the question; the controller's grant gains healer-acted (genesis lock in mesh-host). `healers` lists the registry, the acts and the brake; status counts the week's heals.
145 lines
5.9 KiB
Go
145 lines
5.9 KiB
Go
package broker
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The writers table, checked (novox/hq to-be 45 §1, ADR 0227 rule 1, "how it is checked"): it is the
|
|
// design's table row for row; every row that writes on the bus names its writer; a whole mesh composes
|
|
// with one writer per piece of state; and a grant that would make a second writer is refused, naming
|
|
// the state and whose it is.
|
|
|
|
// designRows are the states of to-be 45 §1, in its order. A row added to the design is added here and
|
|
// to the table; one dropped from either fails.
|
|
var designRows = []string{
|
|
"a machine's declaration",
|
|
"a machine's applied state and its report",
|
|
"the controller lease",
|
|
"plans and their tiers",
|
|
"conditions",
|
|
"calls and their outcomes",
|
|
"the hand-act log",
|
|
"the healers' acts and their brake",
|
|
"stream definitions and bus permissions",
|
|
"builds and their outcomes",
|
|
"a merge announced",
|
|
"a provider's standing",
|
|
"the operator-channel's open messages",
|
|
"the facts snapshot",
|
|
}
|
|
|
|
func TestTheWritersTableIsTheDesigns(t *testing.T) {
|
|
var states []string
|
|
for _, row := range WritersTable {
|
|
states = append(states, row.State)
|
|
if row.Writer == "" || row.KeptIn == "" || row.Others == "" {
|
|
t.Errorf("%q does not say who writes it, where it is kept and what others do", row.State)
|
|
}
|
|
if len(row.Subjects) > 0 && row.Writes == nil {
|
|
t.Errorf("%q is written on the bus and names no writer among the principals", row.State)
|
|
}
|
|
}
|
|
if !slices.Equal(states, designRows) {
|
|
t.Fatalf("the writers table is not to-be 45 §1's:\n have %q\n want %q", states, designRows)
|
|
}
|
|
}
|
|
|
|
// A mesh of every kind of principal composes: nobody is granted a second writer's subject.
|
|
func TestAWholeMeshComposesWithOneWriterPerState(t *testing.T) {
|
|
builder := Seat{Name: "node-build-agent", Scope: "node", Accepts: []string{"build"}, Emits: []string{"built", "started"}}
|
|
principals := []Principal{
|
|
{Kind: KindController, PasswordHash: "x"},
|
|
{Kind: KindNode, Node: "one", PasswordHash: "x"},
|
|
{Kind: KindEnrolment, Node: "two", PasswordHash: "x"},
|
|
{Kind: KindPerson, Module: "jochen", Invokes: []string{"*"}, PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "gitea", Emits: []string{"pull.merged"}, PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "postgres", Emits: []string{"provisioner.failing", "provisioner.recovered"},
|
|
PasswordHash: "x"},
|
|
{Kind: KindModule, Node: "one", Module: "build-agent", Holds: []Seat{builder}, PasswordHash: "x"},
|
|
{Kind: KindNodeTools, Node: "one", Module: RuntimeModule, PasswordHash: "x", Carries: []Declared{
|
|
{Module: "gitea", Emits: []string{"pull.merged"}},
|
|
{Module: "build-agent", Holds: []Seat{builder}}}},
|
|
}
|
|
for _, p := range principals {
|
|
if _, err := PermissionsFor(p); err != nil {
|
|
t.Errorf("%s does not compose: %v", p.Username(), err)
|
|
}
|
|
}
|
|
if _, err := ComposeAccounts(principals); err != nil {
|
|
t.Fatalf("the mesh does not compose: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestASecondWriterIsRefusedAtComposition(t *testing.T) {
|
|
for _, c := range []struct {
|
|
name string
|
|
p Principal
|
|
publish []string
|
|
state string
|
|
}{
|
|
{"the controller publishing what machines say", Principal{Kind: KindController},
|
|
[]string{"mesh.control.>"}, "a machine's applied state and its report"},
|
|
{"a machine publishing another's report", Principal{Kind: KindNode, Node: "one"},
|
|
[]string{"mesh.control.two.report"}, "a machine's applied state and its report"},
|
|
{"a machine publishing every machine's", Principal{Kind: KindNode, Node: "one"},
|
|
[]string{"mesh.control.*.>"}, "a machine's applied state and its report"},
|
|
{"a module writing the lease", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"$KV.mesh-controller_lease.>"}, "the controller lease"},
|
|
{"a module sending a declaration", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.node.one.declare"}, "a machine's declaration"},
|
|
{"a module defining a stream", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"$JS.API.>"}, "stream definitions and bus permissions"},
|
|
{"a module announcing another forge's merge", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.mod.gitea.event.pull.merged"}, "a merge announced"},
|
|
{"a module saying a build it did not do", Principal{Kind: KindModule, Module: "shop"},
|
|
[]string{"mesh.seat.node-build-agent.event.built"}, "builds and their outcomes"},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
err := CheckWriters(c.p, c.publish)
|
|
if err == nil {
|
|
t.Fatalf("%v granted to %s was not refused", c.publish, c.p.Username())
|
|
}
|
|
if !strings.Contains(err.Error(), c.state) {
|
|
t.Fatalf("the refusal does not name %q: %v", c.state, err)
|
|
}
|
|
})
|
|
}
|
|
// And the writers themselves are not refused.
|
|
for _, ok := range []struct {
|
|
p Principal
|
|
publish []string
|
|
}{
|
|
{Principal{Kind: KindNode, Node: "one"}, []string{"mesh.control.one.>"}},
|
|
{Principal{Kind: KindController}, []string{"mesh.node.>", "$JS.API.>", "$KV.mesh-controller_lease.>"}},
|
|
{Principal{Kind: KindModule, Module: "gitea"}, []string{"mesh.mod.gitea.event.pull.merged"}},
|
|
{Principal{Kind: KindModule, Module: "shop"}, []string{"$JS.API.CONSUMER.CREATE.KV_shop_carts.>"}},
|
|
} {
|
|
if err := CheckWriters(ok.p, ok.publish); err != nil {
|
|
t.Errorf("a writer was refused its own: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSubjectsOverlap(t *testing.T) {
|
|
for _, c := range []struct {
|
|
a, b string
|
|
want bool
|
|
}{
|
|
{"mesh.control.>", "mesh.control.*.report", true},
|
|
{"mesh.control.one.>", "mesh.control.*.report", true},
|
|
{"mesh.control.one.alive", "mesh.control.*.report", false},
|
|
{"mesh.control.*", "mesh.control.*.report", false},
|
|
{"$JS.API.>", "$JS.API.STREAM.CREATE.>", true},
|
|
{"$JS.API.CONSUMER.CREATE.KV_x.>", "$JS.API.STREAM.CREATE.>", false},
|
|
{"a.b", "a.b", true},
|
|
{"a.b", "a.b.c", false},
|
|
{"a.>", "a", false},
|
|
} {
|
|
if got := SubjectsOverlap(c.a, c.b); got != c.want || SubjectsOverlap(c.b, c.a) != c.want {
|
|
t.Errorf("%s ~ %s: %v, want %v", c.a, c.b, got, c.want)
|
|
}
|
|
}
|
|
}
|