From 8e51d93c2fad25e4bf14c59a88e86533b0ade2cb Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 15:19:03 +0100 Subject: [PATCH] node-engine seat: a node's windows, busy, strays and filters are lists, as the engine writes them (the contract said objects and refused every real value; ADR 0634; issue 1966) Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- go/clients/nodeengineclient/have_test.go | 13 +++++++++++++ go/seats/nodeengine/contract.go | 8 ++++---- seats/node-engine/seat.json | 20 ++++++++++++++++---- ts/seats/nodeengine/src/index.ts | 8 ++++---- 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/go/clients/nodeengineclient/have_test.go b/go/clients/nodeengineclient/have_test.go index 4b93f33..6a408d7 100644 --- a/go/clients/nodeengineclient/have_test.go +++ b/go/clients/nodeengineclient/have_test.go @@ -148,3 +148,16 @@ func TestHealthAndDataCodecs(t *testing.T) { t.Errorf("node key %q %v", k, err) } } + +// The lists a node's facts carry (windows, busy, strays, filters) are arrays on the bus, as the engine has always written +// them; the contract once said objects and refused every real value. +func TestHaveNodeReadsTheListsTheEngineWrites(t *testing.T) { + raw := []byte(`{"v":1,"windows":[{"step":"backup"}],"busy":[{"id":"x"}],"strays":[{"kind":"container"}],"filters":[{"name":"f"}]}`) + n, err := DecodeHaveNode(raw) + if err != nil { + t.Fatal(err) + } + if len(n.Windows) != 1 || len(n.Busy) != 1 || len(n.Strays) != 1 || len(n.Filters) != 1 { + t.Fatalf("lists lost: %+v", n) + } +} diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index b0d565a..26dc404 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -1288,7 +1288,7 @@ const ( // HaveByNodeNodeValue is one value of the state have under the key .node. type HaveByNodeNodeValue struct { - Busy map[string]any `json:"busy,omitempty"` + Busy []map[string]any `json:"busy,omitempty"` // the node's answer to each placement candidate naming it, by proposal id Candidates map[string]CandidateAnswer `json:"candidates,omitempty"` // the digest of the node's own composition it runs @@ -1296,14 +1296,14 @@ type HaveByNodeNodeValue struct { // own, or none (empty) Composes string `json:"composes,omitempty"` ComposesWhy string `json:"composes-why,omitempty"` - Filters map[string]any `json:"filters,omitempty"` + Filters []map[string]any `json:"filters,omitempty"` Profile map[string]any `json:"profile,omitempty"` Record *NodeRecord `json:"record,omitempty"` Sealed []string `json:"sealed,omitempty"` SealedAt map[string]string `json:"sealed-at,omitempty"` - Strays map[string]any `json:"strays,omitempty"` + Strays []map[string]any `json:"strays,omitempty"` V int64 `json:"v"` - Windows map[string]any `json:"windows,omitempty"` + Windows []map[string]any `json:"windows,omitempty"` } // ComposedByNodeModuleFingerprintValue is one value of the state composed under the key ... diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 237a651..49462e9 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -856,16 +856,28 @@ "description": "the digest of the node's own composition it runs" }, "windows": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "busy": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "strays": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "filters": { - "type": "object" + "type": "array", + "items": { + "type": "object" + } }, "profile": { "type": "object" diff --git a/ts/seats/nodeengine/src/index.ts b/ts/seats/nodeengine/src/index.ts index dbc0378..4399ede 100644 --- a/ts/seats/nodeengine/src/index.ts +++ b/ts/seats/nodeengine/src/index.ts @@ -1043,7 +1043,7 @@ export interface HaveByNodeModuleValue { /** is one value of the state have under the key .node. */ export interface HaveByNodeNodeValue { - "busy"?: Record; + "busy"?: Record[]; /** the node's answer to each placement candidate naming it, by proposal id */ "candidates"?: Record; /** the digest of the node's own composition it runs */ @@ -1051,14 +1051,14 @@ export interface HaveByNodeNodeValue { /** own, or none (empty) */ "composes"?: string; "composes-why"?: string; - "filters"?: Record; + "filters"?: Record[]; "profile"?: Record; "record"?: NodeRecord; "sealed"?: string[]; "sealed-at"?: Record; - "strays"?: Record; + "strays"?: Record[]; "v": number; - "windows"?: Record; + "windows"?: Record[]; } /** is one value of the state composed under the key ... */