Text callers stay read in the release (G's own reader refuses them); both old and new keep forms are read; check stays beside pipeline. Builds: design 47 G §340, §409, §336, §338, §391, §396, §397, §203, §266, §324 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
264 lines
18 KiB
Go
264 lines
18 KiB
Go
package seatdef
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestADefinitionIsReadStrictlyNamingWhatIsWrong is the format's own table (design 26: a valid definition parses;
|
|
// each missing required field is refused naming it; one over 256 KiB is refused), with the parts and fixtures beside.
|
|
func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
|
|
const listGrain = `{"format":1,"seat":"node-hostname","scope":"node","grain":"seat-list","domain":"Core","decision":"ADR 1",
|
|
"verbs":[{"name":"add","input":{"type":"object"}}]}`
|
|
const whole = `{"format":1,"seat":"mesh-broker","scope":"mesh","grain":"whole","domain":"Core","decision":"ADR 1",
|
|
"verbs":[{"name":"list","input":{"type":"object"},"answer":{"type":"object"}}],
|
|
"events":[{"name":"created","body":{"type":"object"}}],"family":{"name":"broker.events","max-age-days":7}}`
|
|
cases := []struct {
|
|
name, raw string
|
|
refused string // a text the refusal names, empty when it parses
|
|
}{
|
|
{"the seat list's grain parses", listGrain, ""},
|
|
{"a whole definition parses", whole, ""},
|
|
{"no format", `{"seat":"a-b","scope":"mesh","grain":"seat-list","domain":"Core","decision":"x"}`, "format: missing"},
|
|
{"a later format", `{"format":2,"seat":"a-b","scope":"mesh","grain":"seat-list","domain":"Core","decision":"x"}`, "format 2"},
|
|
{"no seat", `{"format":1,"scope":"mesh","grain":"seat-list","domain":"Core","decision":"x"}`, "seat: missing"},
|
|
{"no scope", `{"format":1,"seat":"a-b","grain":"seat-list","domain":"Core","decision":"x"}`, "scope: missing"},
|
|
{"another scope", `{"format":1,"seat":"a-b","scope":"room","grain":"seat-list","domain":"Core","decision":"x"}`, "scope"},
|
|
{"no grain", `{"format":1,"seat":"a-b","scope":"mesh","domain":"Core","decision":"x"}`, "grain: missing"},
|
|
{"no decision", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list"}`, "decision: missing"},
|
|
{"no domain", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list","decision":"x"}`, "domain: missing"},
|
|
{"a domain the glossary lacks", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list","domain":"Weather","decision":"x"}`, "domain"},
|
|
{"the Placement domain is gone (ADR 0641 section 5)", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list","domain":"Placement","decision":"x"}`, "domain"},
|
|
{"an unknown field", `{"format":1,"seat":"a-b","scope":"mesh","grain":"seat-list","domain":"Core","decision":"x","sevres":[]}`,
|
|
"sevres: a field the format does not know"},
|
|
{"a whole verb with no answer", strings.Replace(whole, `,"answer":{"type":"object"}`, "", 1),
|
|
"verbs[0].answer: missing"},
|
|
{"a whole verb with no input", strings.Replace(whole, `"input":{"type":"object"},`, "", 1),
|
|
"verbs[0].input: missing"},
|
|
{"a whole event with no body", strings.Replace(whole, `"name":"created","body":{"type":"object"}`,
|
|
`"name":"created"`, 1), "events[0].body: missing"},
|
|
{"whole events kept nowhere", strings.Replace(whole, `,"family":{"name":"broker.events","max-age-days":7}`,
|
|
"", 1), "family: missing"},
|
|
{"a family kept longer than the shared events stream", strings.Replace(whole, `"max-age-days":7`,
|
|
`"max-age-days":30`, 1), "family.max-age-days: missing, or not 7"},
|
|
{"a family with a cap of its own", strings.Replace(whole, `"max-age-days":7`, `"max-age-days":7,"max-mib":256`, 1),
|
|
"max-mib: a field the format does not know"},
|
|
{"a holder's event on its own stream", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
|
|
`"subject":"mesh.stream.<holder>.event.verdict","family":"checker.asks"}],"family":`, 1), ""},
|
|
{"a holder's event on its own stream with no family", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
|
|
`"subject":"mesh.stream.<holder>.event.verdict"}],"family":`, 1), "no family names that stream"},
|
|
{"a holder's event kept in its own stream under its module's subjects", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
|
|
`"subject":"mesh.mod.<holder>.event.verdict","family":"checker.asks"}],"family":`, 1), "a family names the holder's own stream"},
|
|
{"a holder's event on a subject of no module", strings.Replace(whole, `"family":`, `"holder-events":[{"name":"verdict",`+
|
|
`"subject":"mesh.event.verdict"}],"family":`, 1), "holder-events[0].subject: not mesh.mod.<holder>.event.verdict"},
|
|
{"an event with dotted words", strings.Replace(whole, `"name":"created"`, `"name":"pull.updated"`, 1), ""},
|
|
{"an event whose subject carries parts", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"milestone.asked","subject":"milestone.<milestone>.asked"`, 1), ""},
|
|
{"a subject whose words are not the name", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","subject":"owing.<repository>"`, 1), `its words are "owing", not the event's name "owed"`},
|
|
{"a subject with no part", strings.Replace(whole, `"name":"created"`, `"name":"owed","subject":"owed"`, 1),
|
|
"events[0].subject: no part"},
|
|
{"a subject's part twice", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"running","subject":"running.<node>.<node>"`, 1), "part <node> twice"},
|
|
{"a subject on an event named by its caller", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","by-caller":true,"subject":"owed.<number>"`, 1), "carries no subject of its own"},
|
|
{"an event kept last per subject", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"running","subject":"running.<node>.<module>","keep":"last-per-subject"`, 1), ""},
|
|
{"an event kept otherwise", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"running","subject":"running.<node>","keep":"forever"`, 1), `events[0].keep: "forever" is not last-per-subject or stream`},
|
|
{"an event kept in a stream of its own, bounded", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","subject":"owed.<repository>.<number>","keep":"stream","max-age-days":7,"max-mib":64`, 1), ""},
|
|
{"an event kept in a stream of its own, unbounded", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","subject":"owed.<repository>.<number>","keep":"stream","max-age-days":7`, 1), "gives its max-age-days and max-mib"},
|
|
{"a bound on an event kept by its family", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","max-mib":64`, 1), "events[0].max-age-days: only an event kept as a stream"},
|
|
{"an event of one subject kept last per subject", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"running","keep":"last-per-subject"`, 1), "give it a subject with parts"},
|
|
{"an accept with dotted words", strings.Replace(whole, `"events":`,
|
|
`"accepts":[{"name":"run.now","body":{}}],"events":`, 1), "accepts[0].name"},
|
|
{"a verb twice", strings.Replace(listGrain, `]}`, `,{"name":"add"}]}`, 1), `"add" twice`},
|
|
{"states at the seat list's grain", strings.Replace(listGrain, `"domain":"Core","decision":"ADR 1",`,
|
|
`"domain":"Core","decision":"ADR 1","states":[{"name":"asks"}],`, 1), "grain: seat-list holds"},
|
|
{"the old records field is gone", strings.Replace(whole, `"domain":"Core","decision":"ADR 1",`,
|
|
`"domain":"Core","decision":"ADR 1","records":[{"name":"asks"}],`, 1), "records: a field the format does not know"},
|
|
{"a state with a key form and a value", withState(whole, `{"name":"asks","template":"state","writer":"caller",
|
|
"history":3,"key-forms":[{"key":"<asker>.<id>","value":{"type":"object"}}]}`), ""},
|
|
{"a stream state with an age", withState(whole, `{"name":"placement","template":"events","max-age-days":7,
|
|
"key-forms":[{"key":"changed.<node>","value":{"type":"object"}}]}`), ""},
|
|
{"a state of an unknown template", withState(whole, `{"name":"asks","template":"bucket",
|
|
"key-forms":[{"key":"<id>","value":{"type":"object"}}]}`), `states[0].template: "bucket" is none of state, events`},
|
|
{"a key form with no value", withState(whole, `{"name":"asks","template":"state","key-forms":[{"key":"<id>"}]}`),
|
|
`states[0].key-forms[0].value: missing: key form "<id>"`},
|
|
{"a state with no key form", withState(whole, `{"name":"asks","template":"state"}`), "states[0].key-forms: missing"},
|
|
{"a key part that is no name", withState(whole, `{"name":"asks","template":"state",
|
|
"key-forms":[{"key":"<Asker>.<id>","value":{}}]}`), "part <Asker> is not a lower-case name"},
|
|
{"a state written by neither", withState(whole, `{"name":"asks","template":"state","writer":"anyone",
|
|
"key-forms":[{"key":"<id>","value":{}}]}`), "states[0].writer"},
|
|
{"history of a stream", withState(whole, `{"name":"asks","template":"log","history":2,
|
|
"key-forms":[{"key":"<id>","value":{}}]}`), "states[0].history"},
|
|
{"a state twice", withState(withState(whole, `{"name":"asks","template":"state","key-forms":[{"key":"<id>","value":{}}]}`),
|
|
`{"name":"asks","template":"log","key-forms":[{"key":"<id>","value":{}}]}`), `"asks" twice`},
|
|
// W1.1: a seat's acting rules (design 47 *What ADR 0656 leaves to this design*), at either grain.
|
|
{"acting rules", strings.Replace(whole, `"family":`, `"acts":[{"step":"take-in"},{"step":"validate","on":"every"},`+
|
|
`{"step":"build","on":"go","bound":"60m"}],"family":`, 1), ""},
|
|
{"an acting rule at the seat list's grain", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"compose","bound":"30m"}],"verbs":`, 1), ""},
|
|
{"an acting rule with no step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"on":"go"}],"verbs":`, 1), "acts[0].step: missing"},
|
|
{"an acting rule of an unknown step", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"deploy"}],"verbs":`, 1),
|
|
`acts[0].step: "deploy" is not a step (take-in, validate, check, build, compose, grant, place, pipeline)`},
|
|
{"an acting rule on no language", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"Go Lang"}],"verbs":`, 1),
|
|
"acts[0].on"},
|
|
{"an acting rule's bound that is no duration", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","bound":"an hour"}],"verbs":`, 1),
|
|
`acts[0].bound: "an hour" is not a duration`},
|
|
{"an acting rule's field the format does not know", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","when":"now"}],"verbs":`, 1),
|
|
"when: a field the format does not know"},
|
|
{"one step on one language twice", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"grant"},{"step":"grant","on":"every"}],"verbs":`, 1),
|
|
"acts[1]: the step grant on every twice"},
|
|
// The reader release (design 47 G §340, §409): the forms G writes beside the ones on main.
|
|
{"callers as text", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":"a person",`, 1), ""},
|
|
{"callers as a list", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["operator","seat:mesh-grants","invokers"],"class":"read",`, 1), ""},
|
|
{"callers as neither", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":7,`, 1), "callers is a text or a list"},
|
|
{"a list with an empty entry", strings.Replace(whole, `"name":"list",`, `"name":"list","callers":["operator",""],`, 1), "verbs[0].callers[1]: is empty"},
|
|
{"a class that is none", strings.Replace(whole, `"name":"list",`, `"name":"list","class":"free",`, 1), `verbs[0].class: "free" is not a class (read, act, warrant)`},
|
|
{"an accepted kind's callers", strings.Replace(whole, `"events":`, `"accepts":[{"name":"step","body":{},"callers":["seat:mesh-pipeline-runner"]}],"events":`, 1), ""},
|
|
{"an event's callers", strings.Replace(whole, `"name":"created"`, `"name":"created","callers":["operator"]`, 1), "only an accepted kind carries callers"},
|
|
{"keep as an object", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","keep":{"max-age-days":7,"max-mib":64}`, 1), ""},
|
|
{"keep as an object with no age limit", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","keep":{"max-age-days":0,"last-per-subject":true},"subject":"owed.<repository>"`, 1), ""},
|
|
{"keep as an object, last per subject, one subject", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","keep":{"last-per-subject":true}`, 1), "give it a subject with parts"},
|
|
{"keep as an object with a field it does not know", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","keep":{"days":7}`, 1), "keep is a string or an object"},
|
|
{"keep as an object with a negative age", strings.Replace(whole, `"name":"created"`,
|
|
`"name":"owed","keep":{"max-age-days":-1}`, 1), "events[0].keep.max-age-days: is negative"},
|
|
{"the step pipeline and an image rule", strings.Replace(listGrain, `"verbs":`,
|
|
`"acts":[{"step":"pipeline"},{"step":"build","on":"kind:image"},{"step":"check"}],"verbs":`, 1), ""},
|
|
{"an acting rule on another kind", strings.Replace(listGrain, `"verbs":`, `"acts":[{"step":"build","on":"kind:Image"}],"verbs":`, 1), "acts[0].on"},
|
|
{"over 256 KiB", listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) + `"}`,
|
|
ErrTooLarge.Error()},
|
|
}
|
|
for _, c := range cases {
|
|
_, err := Parse([]byte(c.raw))
|
|
switch {
|
|
case c.refused == "" && err != nil:
|
|
t.Errorf("%s: refused: %v", c.name, err)
|
|
case c.refused != "" && err == nil:
|
|
t.Errorf("%s: parsed, want refused naming %q", c.name, c.refused)
|
|
case c.refused != "" && !strings.Contains(err.Error(), c.refused):
|
|
t.Errorf("%s: refused with %q, want it to name %q", c.name, err, c.refused)
|
|
}
|
|
}
|
|
if _, err := Parse([]byte(listGrain[:len(listGrain)-1] + `,"description":"` + strings.Repeat("x", MaxSize) +
|
|
`"}`)); !errors.Is(err, ErrTooLarge) {
|
|
t.Errorf("over 256 KiB is ErrTooLarge, got %v", err)
|
|
}
|
|
|
|
// A part is taken in whole, its verbs before the definition's own; a part not given, or a type defined twice,
|
|
// is refused.
|
|
part, err := ParsePart([]byte(`{"format":1,"part":"registry","verbs":[{"name":"publish","input":{"type":"object"},
|
|
"answer":{"type":"object"}}],"events":[{"name":"published","body":{"type":"object"}}],
|
|
"defs":{"Ref":{"type":"string"}}}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
d, err := Parse([]byte(strings.Replace(whole, `"domain":"Core","decision":"ADR 1",`, `"domain":"Core","decision":"ADR 1","includes":["registry"],`, 1)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := Resolve(d, map[string]Part{"registry": part})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v := VerbNames(got.Verbs); strings.Join(v, ",") != "publish,list" {
|
|
t.Errorf("resolved verbs %v, want the part's first", v)
|
|
}
|
|
if m := MessageNames(got.Events); strings.Join(m, ",") != "published,created" || got.Defs["Ref"] == nil {
|
|
t.Errorf("resolved events %v, defs %v", m, got.Defs)
|
|
}
|
|
if _, err := Resolve(d, nil); err == nil || !strings.Contains(err.Error(), `part "registry" is not given`) {
|
|
t.Errorf("a part not given: %v", err)
|
|
}
|
|
d.Defs = map[string]map[string]any{"Ref": {"type": "string"}}
|
|
if _, err := Resolve(d, map[string]Part{"registry": part}); err == nil || !strings.Contains(err.Error(), "defs.Ref") {
|
|
t.Errorf("a type defined twice: %v", err)
|
|
}
|
|
|
|
// Fixtures name what they are an instance of.
|
|
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created",
|
|
"subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {
|
|
t.Errorf("fixtures: %v", err)
|
|
}
|
|
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"state:asks/<asker>.<id>","given":{}}]`)); err != nil {
|
|
t.Errorf("a state's fixture: %v", err)
|
|
}
|
|
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"state:asks","given":{}}]`)); err == nil {
|
|
t.Error("a fixture of a state with no key form parsed")
|
|
}
|
|
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list","given":{}}]`)); err == nil ||
|
|
!strings.Contains(err.Error(), "fixtures[0].of") {
|
|
t.Errorf("a fixture of nothing known: %v", err)
|
|
}
|
|
if _, err := ParseIndex([]byte(`{"format":1,"seats":["a-b","a-b"]}`)); err == nil {
|
|
t.Error("an index naming a seat twice parsed")
|
|
}
|
|
}
|
|
|
|
// withState adds one state to a whole definition's JSON.
|
|
func withState(def, state string) string {
|
|
if strings.Contains(def, `"states":[`) {
|
|
return strings.Replace(def, `"states":[`, `"states":[`+state+`,`, 1)
|
|
}
|
|
return strings.Replace(def, `"domain":"Core","decision":"ADR 1",`, `"domain":"Core","decision":"ADR 1","states":[`+state+`],`, 1)
|
|
}
|
|
|
|
// TestAnEventsSubjectNamesItsParts holds the three readings of an event's subject: its pattern, its parts in order and
|
|
// the local name a manifest's emits and consumes give it.
|
|
func TestAnEventsSubjectNamesItsParts(t *testing.T) {
|
|
for _, c := range []struct {
|
|
m Message
|
|
pattern, parts, local string
|
|
}{
|
|
{Message{Name: "created"}, "created", "", "created"},
|
|
{Message{Name: "pull.updated"}, "pull.updated", "", "pull.updated"},
|
|
{Message{Name: "running", Subject: "running.<node>.<module>"}, "running.<node>.<module>", "node,module", "running.*.*"},
|
|
{Message{Name: "milestone.asked", Subject: "milestone.<milestone>.asked"}, "milestone.<milestone>.asked", "milestone", "milestone.*.asked"},
|
|
} {
|
|
if got := c.m.SubjectPattern(); got != c.pattern {
|
|
t.Errorf("%s: pattern %q, want %q", c.m.Name, got, c.pattern)
|
|
}
|
|
if got := strings.Join(c.m.Parts(), ","); got != c.parts {
|
|
t.Errorf("%s: parts %q, want %q", c.m.Name, got, c.parts)
|
|
}
|
|
if got := c.m.Local(); got != c.local {
|
|
t.Errorf("%s: local %q, want %q", c.m.Name, got, c.local)
|
|
}
|
|
}
|
|
}
|
|
|
|
// W1.1: what an acting rule reads as where it says less (design 47 *An acting part's new version acts first*).
|
|
func TestAnActingRuleReadsItsStepsDefaults(t *testing.T) {
|
|
for _, c := range []struct {
|
|
rule ActingRule
|
|
language string
|
|
acts bool
|
|
wait string
|
|
}{
|
|
{ActingRule{Step: StepBuild, On: "go"}, "go", true, "1h0m0s"},
|
|
{ActingRule{Step: StepBuild, On: "go"}, "typescript", false, "1h0m0s"},
|
|
{ActingRule{Step: StepCompose}, "anything", true, "30m0s"},
|
|
{ActingRule{Step: StepCheck, On: OnEvery}, "", true, "20m0s"},
|
|
{ActingRule{Step: StepGrant, Bound: "5m"}, "go", true, "5m0s"},
|
|
{ActingRule{Step: StepPlace}, "go", true, "10m0s"},
|
|
{ActingRule{Step: StepTakeIn}, "go", true, "0s"},
|
|
{ActingRule{Step: StepPipeline}, "go", true, "30m0s"},
|
|
} {
|
|
if got := c.rule.ActsOn(c.language); got != c.acts {
|
|
t.Errorf("%+v acts on %q: %v, want %v", c.rule, c.language, got, c.acts)
|
|
}
|
|
if got := c.rule.Wait().String(); got != c.wait {
|
|
t.Errorf("%+v waits %s, want %s", c.rule, got, c.wait)
|
|
}
|
|
}
|
|
}
|