From 551870d8d5af14903efb39e0d17de6c02496c36d Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:40:27 +0100 Subject: [PATCH] =?UTF-8?q?Fixtures=20captured=20from=20the=20holders'=20o?= =?UTF-8?q?wn=20read=20tools=20for=20the=20states=20that=20had=20none=20(c?= =?UTF-8?q?onditions,=20findings,=20presence,=20stuck,=20would,=20resolved?= =?UTF-8?q?,=20asks,=20packages);=20the=20packages=20item=20requires=20abs?= =?UTF-8?q?ent=20(ADR=200634=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/seats/meshconfig/contract_test.go | 12 ++ go/seats/nodeengine/contract.go | 6 +- go/seats/nodeengine/contract_test.go | 2 + .../go/seats/meshconditions/contract_test.go | 54 ++++++++ .../go/seats/meshhealers/contract_test.go | 48 +++++++ .../go/seats/meshwatchdogs/contract_test.go | 56 +++++++++ .../mesh-conditions/fixtures/states.json | 117 ++++++++++++++++++ .../seats/mesh-healers/fixtures/states.json | 14 +++ .../seats/mesh-watchdogs/fixtures/states.json | 29 +++++ .../go/seats/operatorchannel/contract_test.go | 4 + .../operator-channel/fixtures/states.json | 36 ++++++ seats/mesh-config/fixtures/states.json | 25 ++++ seats/node-engine/fixtures/states.json | 37 +++++- seats/node-engine/seat.json | 7 +- ts/seats/nodeengine/src/index.ts | 6 +- 15 files changed, 445 insertions(+), 8 deletions(-) create mode 100644 health-and-repair/go/seats/meshconditions/contract_test.go create mode 100644 health-and-repair/go/seats/meshhealers/contract_test.go create mode 100644 health-and-repair/go/seats/meshwatchdogs/contract_test.go create mode 100644 health-and-repair/seats/mesh-conditions/fixtures/states.json create mode 100644 health-and-repair/seats/mesh-healers/fixtures/states.json create mode 100644 health-and-repair/seats/mesh-watchdogs/fixtures/states.json create mode 100644 operator-and-conversation/seats/operator-channel/fixtures/states.json create mode 100644 seats/mesh-config/fixtures/states.json diff --git a/go/seats/meshconfig/contract_test.go b/go/seats/meshconfig/contract_test.go index 6d82f0f..7a994e6 100644 --- a/go/seats/meshconfig/contract_test.go +++ b/go/seats/meshconfig/contract_test.go @@ -25,6 +25,18 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RetiringHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.retiring$")}, {name: "retired", subject: "mesh.stream.mesh-config.event.retired", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"old-key\",\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v RetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.stream\\.[a-z0-9-]+\\.event\\.retired$")}, + {name: "a module's value that falls back to a predefined key (captured)", subject: "", given: "{\"key\":\"font-size\",\"module\":\"dunst\",\"node\":\"g14\",\"predefined\":\"font-size\",\"source\":\"module\",\"value\":13}", wire: "{\"key\":\"font-size\",\"module\":\"dunst\",\"node\":\"g14\",\"predefined\":\"font-size\",\"source\":\"module\",\"value\":13}", + decode: func(b []byte) (any, error) { + var v ResolvedByNodeModuleKeyValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module's own value (captured)", subject: "", given: "{\"key\":\"width\",\"module\":\"dunst\",\"node\":\"g14\",\"source\":\"module\",\"value\":500}", wire: "{\"key\":\"width\",\"module\":\"dunst\",\"node\":\"g14\",\"source\":\"module\",\"value\":500}", + decode: func(b []byte) (any, error) { + var v ResolvedByNodeModuleKeyValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, {name: "renames answers the open windows", subject: "", given: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", wire: "{\"renames\":[{\"at\":\"2026-11-04T12:00:00Z\",\"from\":\"old-key\",\"module\":\"messenger\",\"open\":[\"g14\"],\"to\":\"new-key\"}]}", decode: func(b []byte) (any, error) { var v RenamesAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index dd4e2d4..2fa471a 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -978,9 +978,9 @@ type DataByNodeModuleValue struct { // PackagesByNodeValuePackagesItem is a type of the contract. type PackagesByNodeValuePackagesItem struct { - Absent bool `json:"absent,omitempty"` - Modules []string `json:"modules,omitempty"` - Package string `json:"package,omitempty"` + Absent bool `json:"absent"` + Modules []string `json:"modules"` + Package string `json:"package"` } // PackagesByNodeValue is one value of the state packages under the key . diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go index f54e529..aa467b2 100644 --- a/go/seats/nodeengine/contract_test.go +++ b/go/seats/nodeengine/contract_test.go @@ -37,6 +37,8 @@ func TestFixtures(t *testing.T) { }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-engine\\.event\\.manifest-reads-changed\\.[A-Za-z0-9_-]+$")}, {name: "the finding of a seat list unread, as the engine writes it", subject: "", given: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", wire: "{\"at\":\"2026-11-05T14:00:00Z\",\"key\":\"machine.ace.seat-list-unread\",\"kind\":\"seat-list-unread\",\"observer\":\"mesh-host\",\"severity\":\"warning\",\"source\":\"node-engine.seat-list\",\"subject\":{\"id\":\"ace\",\"machine\":\"ace\",\"scope\":\"machine\"},\"summary\":\"the seat list has not been read for five minutes\",\"v\":1}", decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node's packages, present and absent (captured from ace, shortened)", subject: "", given: "{\"node\":\"ace\",\"packages\":[{\"absent\":false,\"modules\":[\"avahi\"],\"package\":\"avahi\"},{\"absent\":false,\"modules\":[\"mesh-build-go\",\"mesh-build-node\"],\"package\":\"bubblewrap\"},{\"absent\":true,\"modules\":[\"time-sync\"],\"package\":\"ntp\"}]}", wire: "{\"node\":\"ace\",\"packages\":[{\"absent\":false,\"modules\":[\"avahi\"],\"package\":\"avahi\"},{\"absent\":false,\"modules\":[\"mesh-build-go\",\"mesh-build-node\"],\"package\":\"bubblewrap\"},{\"absent\":true,\"modules\":[\"time-sync\"],\"package\":\"ntp\"}]}", + decode: func(b []byte) (any, error) { var v PackagesByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads answers what the engine reads and when it stated it", subject: "", given: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", wire: "{\"manifest-reads\":[\"form:seat-reference\",\"word:series\"],\"node\":\"ace\",\"stated-at\":\"2026-11-04T10:00:00Z\",\"v\":1}", decode: func(b []byte) (any, error) { var v ManifestReadsAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "manifest-reads of an engine built before it said them is the baseline", subject: "", given: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", wire: "{\"node\":\"g14\",\"stated-at\":\"2026-11-04T10:05:00Z\",\"v\":1}", diff --git a/health-and-repair/go/seats/meshconditions/contract_test.go b/health-and-repair/go/seats/meshconditions/contract_test.go new file mode 100644 index 0000000..fc0717f --- /dev/null +++ b/health-and-repair/go/seats/meshconditions/contract_test.go @@ -0,0 +1,54 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshconditions + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-conditions/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a condition raised from a finding, as the keeper holds it (captured; empty fields left out)", subject: "", given: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator; the details name it and the line to type. A directory of mesh-build-node was already on ace, with another owner or mode than mesh-build-node declares.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"last-observed\":\"2026-10-26T20:19:41.474823121Z\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.076843727Z\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}", wire: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator; the details name it and the line to type. A directory of mesh-build-node was already on ace, with another owner or mode than mesh-build-node declares.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"last-observed\":\"2026-10-26T20:19:41.474823121Z\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.076843727Z\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it — `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh\"}", + decode: func(b []byte) (any, error) { var v ConditionsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a condition with an empty evidence trail of headline (captured; empty fields left out)", subject: "", given: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-30T13:03:46.432309782Z\",\"said\":\"The backup of postgres failed on ace\"}],\"explanation\":\"Needs you: answer whether to back up again or go on without a backup. The backup before applying postgres at 978b5db8cb4e on ace failed: node-backup.now could not be asked: not asked: nothing on this machine answers mesh.seat.node-backup.tool.now.ace.\",\"headline\":\"The backup of postgres failed on ace\",\"key\":\"module.ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f.backup\",\"kind\":\"backup\",\"last-observed\":\"2026-10-30T13:03:46.432309782Z\",\"needs\":\"answer whether to back up again or go on without a backup\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.069818178Z\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"assigner.backup\",\"subject\":{\"id\":\"ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"The backup of postgres failed on ace\"}", wire: "{\"count\":1,\"evidence\":[{\"at\":\"2026-10-30T13:03:46.432309782Z\",\"said\":\"The backup of postgres failed on ace\"}],\"explanation\":\"Needs you: answer whether to back up again or go on without a backup. The backup before applying postgres at 978b5db8cb4e on ace failed: node-backup.now could not be asked: not asked: nothing on this machine answers mesh.seat.node-backup.tool.now.ace.\",\"headline\":\"The backup of postgres failed on ace\",\"key\":\"module.ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f.backup\",\"kind\":\"backup\",\"last-observed\":\"2026-10-30T13:03:46.432309782Z\",\"needs\":\"answer whether to back up again or go on without a backup\",\"observations\":1,\"raised\":\"2026-10-31T08:19:57.069818178Z\",\"resolver\":\"self\",\"severity\":\"warning\",\"source\":\"assigner.backup\",\"subject\":{\"id\":\"ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"The backup of postgres failed on ace\"}", + decode: func(b []byte) (any, error) { var v ConditionsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a finding of the bus scope with its zero time (captured from the watchman; empty fields left out)", subject: "", given: "{\"at\":\"0001-01-01T00:00:00Z\",\"evidence\":[{\"at\":\"0001-01-01T00:00:00Z\",\"said\":\"1 held for EVENTS.controller (counted again every 5m0s while any is held, so one delivered again or dropped ends this within it)\"}],\"key\":\"bus.EVENTS.controller.max-deliveries\",\"kind\":\"max-deliveries\",\"needs\":\"deliver them again or drop them, from the mesh MCP server.\",\"observer\":\"mesh-delivery-watchman\",\"severity\":\"warning\",\"source\":\"S9\",\"subject\":{\"id\":\"EVENTS.controller\",\"scope\":\"bus\"},\"summary\":\"the controller's consumer on EVENTS gave up on a message; it is kept in DEAD_LETTERS until delivered again or dropped, with why\",\"v\":1}", wire: "{\"at\":\"0001-01-01T00:00:00Z\",\"evidence\":[{\"at\":\"0001-01-01T00:00:00Z\",\"said\":\"1 held for EVENTS.controller (counted again every 5m0s while any is held, so one delivered again or dropped ends this within it)\"}],\"key\":\"bus.EVENTS.controller.max-deliveries\",\"kind\":\"max-deliveries\",\"needs\":\"deliver them again or drop them, from the mesh MCP server.\",\"observer\":\"mesh-delivery-watchman\",\"severity\":\"warning\",\"source\":\"S9\",\"subject\":{\"id\":\"EVENTS.controller\",\"scope\":\"bus\"},\"summary\":\"the controller's consumer on EVENTS gave up on a message; it is kept in DEAD_LETTERS until delivered again or dropped, with why\",\"v\":1}", + decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a finding of the module scope with its machine (captured from the watchman; empty fields left out)", subject: "", given: "{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: owned by 10005:10005, mode 700, as found\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observer\":\"mesh-delivery-watchman\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found\",\"v\":1}", wire: "{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"evidence\":[{\"at\":\"2026-10-26T20:19:41.474823121Z\",\"said\":\"mesh-build-node on ace uses mesh-build-node.cache as found: owned by 10005:10005, mode 700, as found\"}],\"explanation\":\"Needs you: hand the directory over from the control-node, as the operator.\",\"headline\":\"mesh-build-node waits for a directory on ace\",\"key\":\"module.mesh-build-node.ace.directory-used-as-found\",\"kind\":\"directory-used-as-found\",\"needs\":\"hand the directory over from the control-node, as the operator; the details name it and the line to type.\",\"observer\":\"mesh-delivery-watchman\",\"resolved\":\"mesh-build-node's directory on ace is the mesh's\",\"severity\":\"warning\",\"source\":\"health-report\",\"subject\":{\"id\":\"mesh-build-node.ace\",\"machine\":\"ace\",\"scope\":\"module\"},\"summary\":\"mesh-build-node on ace uses mesh-build-node.cache as found\",\"v\":1}", + decode: func(b []byte) (any, error) { var v FindingsByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/health-and-repair/go/seats/meshhealers/contract_test.go b/health-and-repair/go/seats/meshhealers/contract_test.go new file mode 100644 index 0000000..957da9d --- /dev/null +++ b/health-and-repair/go/seats/meshhealers/contract_test.go @@ -0,0 +1,48 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshhealers + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-healers/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "an act a healer would have made, kept from the controller's time (captured)", subject: "", given: "{\"act\":\"assert the module's buckets, logs and streams again through the grants part (grants_assert)\",\"at\":\"2026-11-02T08:15:56.49507336Z\",\"condition\":\"bus.EVENTS.4fs9xzyDY7CD2Cf1wVFYDK_1.consumer-lost\",\"healer\":\"H3\",\"kind\":\"consumer-lost\",\"why\":\"the controller's lease names a controller (or none is written, so one may take it): every act is written as would\"}", wire: "{\"act\":\"assert the module's buckets, logs and streams again through the grants part (grants_assert)\",\"at\":\"2026-11-02T08:15:56.49507336Z\",\"condition\":\"bus.EVENTS.4fs9xzyDY7CD2Cf1wVFYDK_1.consumer-lost\",\"healer\":\"H3\",\"kind\":\"consumer-lost\",\"why\":\"the controller's lease names a controller (or none is written, so one may take it): every act is written as would\"}", + decode: func(b []byte) (any, error) { var v WouldByWouldValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/health-and-repair/go/seats/meshwatchdogs/contract_test.go b/health-and-repair/go/seats/meshwatchdogs/contract_test.go new file mode 100644 index 0000000..564f824 --- /dev/null +++ b/health-and-repair/go/seats/meshwatchdogs/contract_test.go @@ -0,0 +1,56 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshwatchdogs + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-watchdogs/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a node's agent online (captured)", subject: "", given: "{\"online\":true,\"v\":1}", wire: "{\"online\":true,\"v\":1}", + decode: func(b []byte) (any, error) { var v PresenceByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a delivery part's instance online (captured)", subject: "", given: "{\"online\":true,\"v\":1}", wire: "{\"online\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v PresenceByPartPartInstanceValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a thing that has not moved since a time, with what it waited for (captured)", subject: "", given: "{\"bucket\":\"mesh-delivery-watchman_presence\",\"key\":\"part.mesh-delivery-builder.novox\",\"since\":\"2026-10-25T11:52:58.051397201+01:00\",\"v\":1,\"waited\":\"mesh-delivery-builder on novox to come back online\"}", wire: "{\"bucket\":\"mesh-delivery-watchman_presence\",\"key\":\"part.mesh-delivery-builder.novox\",\"since\":\"2026-10-25T11:52:58.051397201+01:00\",\"v\":1,\"waited\":\"mesh-delivery-builder on novox to come back online\"}", + decode: func(b []byte) (any, error) { var v StuckByBucketKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/health-and-repair/seats/mesh-conditions/fixtures/states.json b/health-and-repair/seats/mesh-conditions/fixtures/states.json new file mode 100644 index 0000000..0cac1dd --- /dev/null +++ b/health-and-repair/seats/mesh-conditions/fixtures/states.json @@ -0,0 +1,117 @@ +[ + { + "name": "a condition raised from a finding, as the keeper holds it (captured; empty fields left out)", + "of": "state:conditions/", + "given": { + "count": 1, + "evidence": [ + { + "at": "2026-10-26T20:19:41.474823121Z", + "said": "mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it \u2014 `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh" + } + ], + "explanation": "Needs you: hand the directory over from the control-node, as the operator; the details name it and the line to type. A directory of mesh-build-node was already on ace, with another owner or mode than mesh-build-node declares.", + "headline": "mesh-build-node waits for a directory on ace", + "key": "module.mesh-build-node.ace.directory-used-as-found", + "kind": "directory-used-as-found", + "last-observed": "2026-10-26T20:19:41.474823121Z", + "needs": "hand the directory over from the control-node, as the operator; the details name it and the line to type.", + "observations": 1, + "raised": "2026-10-31T08:19:57.076843727Z", + "resolved": "mesh-build-node's directory on ace is the mesh's", + "resolver": "self", + "severity": "warning", + "source": "health-report", + "subject": { + "id": "mesh-build-node.ace", + "machine": "ace", + "scope": "module" + }, + "summary": "mesh-build-node on ace uses mesh-build-node.cache as found: used as found: owned by 10005:10005, mode 700, as found; mesh-build-node, mode 0711 was declared and not given it \u2014 `ace/node-engine.hand-over` with the directory's path, then your yes on your channel, with its path, hands it to the mesh" + } + }, + { + "name": "a condition with an empty evidence trail of headline (captured; empty fields left out)", + "of": "state:conditions/", + "given": { + "count": 1, + "evidence": [ + { + "at": "2026-10-30T13:03:46.432309782Z", + "said": "The backup of postgres failed on ace" + } + ], + "explanation": "Needs you: answer whether to back up again or go on without a backup. The backup before applying postgres at 978b5db8cb4e on ace failed: node-backup.now could not be asked: not asked: nothing on this machine answers mesh.seat.node-backup.tool.now.ace.", + "headline": "The backup of postgres failed on ace", + "key": "module.ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f.backup", + "kind": "backup", + "last-observed": "2026-10-30T13:03:46.432309782Z", + "needs": "answer whether to back up again or go on without a backup", + "observations": 1, + "raised": "2026-10-31T08:19:57.069818178Z", + "resolver": "self", + "severity": "warning", + "source": "assigner.backup", + "subject": { + "id": "ace.postgres.978b5db8cb4ef746ce68d82cc616d1439a53266d335e60f368dc44c4cb25610f", + "machine": "ace", + "scope": "module" + }, + "summary": "The backup of postgres failed on ace" + } + }, + { + "name": "a finding of the bus scope with its zero time (captured from the watchman; empty fields left out)", + "of": "state:findings/", + "given": { + "v": 1, + "key": "bus.EVENTS.controller.max-deliveries", + "kind": "max-deliveries", + "subject": { + "id": "EVENTS.controller", + "scope": "bus" + }, + "severity": "warning", + "summary": "the controller's consumer on EVENTS gave up on a message; it is kept in DEAD_LETTERS until delivered again or dropped, with why", + "needs": "deliver them again or drop them, from the mesh MCP server.", + "observer": "mesh-delivery-watchman", + "source": "S9", + "evidence": [ + { + "at": "0001-01-01T00:00:00Z", + "said": "1 held for EVENTS.controller (counted again every 5m0s while any is held, so one delivered again or dropped ends this within it)" + } + ], + "at": "0001-01-01T00:00:00Z" + } + }, + { + "name": "a finding of the module scope with its machine (captured from the watchman; empty fields left out)", + "of": "state:findings/", + "given": { + "v": 1, + "key": "module.mesh-build-node.ace.directory-used-as-found", + "kind": "directory-used-as-found", + "subject": { + "id": "mesh-build-node.ace", + "machine": "ace", + "scope": "module" + }, + "severity": "warning", + "summary": "mesh-build-node on ace uses mesh-build-node.cache as found", + "explanation": "Needs you: hand the directory over from the control-node, as the operator.", + "headline": "mesh-build-node waits for a directory on ace", + "needs": "hand the directory over from the control-node, as the operator; the details name it and the line to type.", + "resolved": "mesh-build-node's directory on ace is the mesh's", + "observer": "mesh-delivery-watchman", + "source": "health-report", + "evidence": [ + { + "at": "2026-10-26T20:19:41.474823121Z", + "said": "mesh-build-node on ace uses mesh-build-node.cache as found: owned by 10005:10005, mode 700, as found" + } + ], + "at": "2026-10-26T20:19:41.474823121Z" + } + } +] diff --git a/health-and-repair/seats/mesh-healers/fixtures/states.json b/health-and-repair/seats/mesh-healers/fixtures/states.json new file mode 100644 index 0000000..01ede17 --- /dev/null +++ b/health-and-repair/seats/mesh-healers/fixtures/states.json @@ -0,0 +1,14 @@ +[ + { + "name": "an act a healer would have made, kept from the controller's time (captured)", + "of": "state:would/", + "given": { + "act": "assert the module's buckets, logs and streams again through the grants part (grants_assert)", + "at": "2026-11-02T08:15:56.49507336Z", + "condition": "bus.EVENTS.4fs9xzyDY7CD2Cf1wVFYDK_1.consumer-lost", + "healer": "H3", + "kind": "consumer-lost", + "why": "the controller's lease names a controller (or none is written, so one may take it): every act is written as would" + } + } +] diff --git a/health-and-repair/seats/mesh-watchdogs/fixtures/states.json b/health-and-repair/seats/mesh-watchdogs/fixtures/states.json new file mode 100644 index 0000000..6d8a3f8 --- /dev/null +++ b/health-and-repair/seats/mesh-watchdogs/fixtures/states.json @@ -0,0 +1,29 @@ +[ + { + "name": "a node's agent online (captured)", + "of": "state:presence/", + "given": { + "online": true, + "v": 1 + } + }, + { + "name": "a delivery part's instance online (captured)", + "of": "state:presence/part..", + "given": { + "online": true, + "v": 1 + } + }, + { + "name": "a thing that has not moved since a time, with what it waited for (captured)", + "of": "state:stuck/.", + "given": { + "bucket": "mesh-delivery-watchman_presence", + "key": "part.mesh-delivery-builder.novox", + "since": "2026-10-25T11:52:58.051397201+01:00", + "v": 1, + "waited": "mesh-delivery-builder on novox to come back online" + } + } +] diff --git a/operator-and-conversation/go/seats/operatorchannel/contract_test.go b/operator-and-conversation/go/seats/operatorchannel/contract_test.go index c5e8a8e..440ac8a 100644 --- a/operator-and-conversation/go/seats/operatorchannel/contract_test.go +++ b/operator-and-conversation/go/seats/operatorchannel/contract_test.go @@ -31,6 +31,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v CancelAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.cancel\\.[A-Za-z0-9_-]+$")}, {name: "a condition by its publisher", subject: "mesh.seat.operator-channel.accept.condition.mesh-watcher", given: "{\"key\":\"backup.ace.failed\",\"severity\":\"warning\",\"state\":\"raised\",\"subject\":{\"id\":\"ace\",\"scope\":\"node\"},\"v\":1}", wire: "{\"key\":\"backup.ace.failed\",\"severity\":\"warning\",\"state\":\"raised\",\"subject\":{\"id\":\"ace\",\"scope\":\"node\"},\"v\":1}", decode: func(b []byte) (any, error) { var v ConditionAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.operator-channel\\.accept\\.condition\\.[A-Za-z0-9_-]+$")}, + {name: "an ask chosen on a channel, as the router's tool states it (captured)", subject: "", given: "{\"answer\":\"the operator, via telegram (user id verified), chose Approve\",\"asker\":\"mesh-knowledge\",\"asker_told\":true,\"expires\":\"2026-11-06T12:17:43Z\",\"headline\":\"Parts wait for the seat list before acting\",\"key\":\"mesh-knowledge.kb-decision-636-1bcf1a2c\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T12:27:44Z\",\"state\":\"chosen\"}", wire: "{\"answer\":\"the operator, via telegram (user id verified), chose Approve\",\"asker\":\"mesh-knowledge\",\"asker_told\":true,\"expires\":\"2026-11-06T12:17:43Z\",\"headline\":\"Parts wait for the seat list before acting\",\"key\":\"mesh-knowledge.kb-decision-636-1bcf1a2c\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T12:27:44Z\",\"state\":\"chosen\"}", + decode: func(b []byte) (any, error) { var v AsksByAskerIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "an ask its asker took back (captured)", subject: "", given: "{\"answer\":\"cancelled: its asker took it back\",\"asker\":\"mesh-delivery-teller\",\"asker_told\":true,\"expires\":\"2026-11-06T10:04:01Z\",\"headline\":\"Check failed - merge into main\",\"key\":\"mesh-delivery-teller.merge-feat_registry_milestone-3a038b9f8b4d-4-4da6ee3cd9d5\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T10:14:04Z\",\"state\":\"cancelled\"}", wire: "{\"answer\":\"cancelled: its asker took it back\",\"asker\":\"mesh-delivery-teller\",\"asker_told\":true,\"expires\":\"2026-11-06T10:04:01Z\",\"headline\":\"Check failed - merge into main\",\"key\":\"mesh-delivery-teller.merge-feat_registry_milestone-3a038b9f8b4d-4-4da6ee3cd9d5\",\"offered_on\":[\"telegram\"],\"opened\":\"2026-11-05T10:14:04Z\",\"state\":\"cancelled\"}", + decode: func(b []byte) (any, error) { var v AsksByAskerIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/operator-and-conversation/seats/operator-channel/fixtures/states.json b/operator-and-conversation/seats/operator-channel/fixtures/states.json new file mode 100644 index 0000000..66bc5de --- /dev/null +++ b/operator-and-conversation/seats/operator-channel/fixtures/states.json @@ -0,0 +1,36 @@ +[ + { + "name": "an ask chosen on a channel, as the router's tool states it (captured)", + "of": "state:asks/.", + "given": { + "answer": "the operator, via telegram (user id verified), chose Approve", + "asker": "mesh-knowledge", + "asker_told": true, + "expires": "2026-11-06T12:17:43Z", + "headline": "Parts wait for the seat list before acting", + "key": "mesh-knowledge.kb-decision-636-1bcf1a2c", + "offered_on": [ + "telegram" + ], + "opened": "2026-11-05T12:27:44Z", + "state": "chosen" + } + }, + { + "name": "an ask its asker took back (captured)", + "of": "state:asks/.", + "given": { + "answer": "cancelled: its asker took it back", + "asker": "mesh-delivery-teller", + "asker_told": true, + "expires": "2026-11-06T10:04:01Z", + "headline": "Check failed - merge into main", + "key": "mesh-delivery-teller.merge-feat_registry_milestone-3a038b9f8b4d-4-4da6ee3cd9d5", + "offered_on": [ + "telegram" + ], + "opened": "2026-11-05T10:14:04Z", + "state": "cancelled" + } + } +] diff --git a/seats/mesh-config/fixtures/states.json b/seats/mesh-config/fixtures/states.json new file mode 100644 index 0000000..ad3d582 --- /dev/null +++ b/seats/mesh-config/fixtures/states.json @@ -0,0 +1,25 @@ +[ + { + "name": "a module's value that falls back to a predefined key (captured)", + "of": "state:resolved/..", + "given": { + "node": "g14", + "module": "dunst", + "key": "font-size", + "predefined": "font-size", + "source": "module", + "value": 13 + } + }, + { + "name": "a module's own value (captured)", + "of": "state:resolved/..", + "given": { + "node": "g14", + "module": "dunst", + "key": "width", + "source": "module", + "value": 500 + } + } +] diff --git a/seats/node-engine/fixtures/states.json b/seats/node-engine/fixtures/states.json index 2712dfd..aff5a04 100644 --- a/seats/node-engine/fixtures/states.json +++ b/seats/node-engine/fixtures/states.json @@ -6,12 +6,47 @@ "v": 1, "key": "machine.ace.seat-list-unread", "kind": "seat-list-unread", - "subject": {"scope": "machine", "id": "ace", "machine": "ace"}, + "subject": { + "scope": "machine", + "id": "ace", + "machine": "ace" + }, "severity": "warning", "summary": "the seat list has not been read for five minutes", "source": "node-engine.seat-list", "observer": "mesh-host", "at": "2026-11-05T14:00:00Z" } + }, + { + "name": "a node's packages, present and absent (captured from ace, shortened)", + "of": "state:packages/", + "given": { + "node": "ace", + "packages": [ + { + "absent": false, + "modules": [ + "avahi" + ], + "package": "avahi" + }, + { + "absent": false, + "modules": [ + "mesh-build-go", + "mesh-build-node" + ], + "package": "bubblewrap" + }, + { + "absent": true, + "modules": [ + "time-sync" + ], + "package": "ntp" + } + ] + } } ] diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 541d242..fec0fbb 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -1012,7 +1012,12 @@ "absent": { "type": "boolean" } - } + }, + "required": [ + "package", + "modules", + "absent" + ] } } }, diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index 60a34dc..a03d9ab 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -782,9 +782,9 @@ export interface DataByNodeModuleValue { } export interface PackagesByNodeValuePackagesItem { - "absent"?: boolean; - "modules"?: string[]; - "package"?: string; + "absent": boolean; + "modules": string[]; + "package": string; } /** is one value of the state packages under the key . */