From 3e9a38f81bd44612d21ec08e14b026acec8c893d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 7 Oct 2026 20:18:05 +0200 Subject: [PATCH] Mark the uplink seat's verbs optional, the one mechanism, and keep every optional verb out of the seat row Staged duplicated Verb.Optional. Optional, though, was still seeded into the row, where the mark is not kept: read back, the delivery seat's checks verb and these would be required of every holder, and an older controller would require them too. Stored leaves optional verbs out, and the working set takes them from the compiled seat, still optional. --- internal/catalogue/seats.go | 54 +++++++++++------------- internal/catalogue/uplink_verbs_test.go | 55 ++++++++++++++++++------- internal/catalogue/verbs.go | 4 +- internal/inventory/seats.go | 2 +- internal/inventory/seats_test.go | 13 +++--- 5 files changed, 76 insertions(+), 52 deletions(-) diff --git a/internal/catalogue/seats.go b/internal/catalogue/seats.go index afc80ddd..31abb4ea 100644 --- a/internal/catalogue/seats.go +++ b/internal/catalogue/seats.go @@ -2,7 +2,6 @@ package catalogue import ( "fmt" - "slices" "sort" "strings" ) @@ -54,14 +53,6 @@ type Seat struct { // ${contribution::}. Compiled, never stored: like the protocol, it is the mesh's // definition of the role, and the store's rows carry no column for it. Receives []Receivable - // Staged names verbs of Serves that are promised and routed but not yet a condition of holding the - // seat: a verb added to a seat whose holders do not serve it yet. Holding requires every verb, and a - // claim may name only promised ones (ADR 0132), so a verb required at once would refuse every holder - // until the catalogue serves it — and the catalogue could not name it until this controller ran. Staged, - // the controller rolls out first, the holders serve the verb next, and a later change requires it. - // Compiled, never stored, and kept out of the stored row's serves: a controller older than the verb - // reads the row strictly and would require what no holder of its time serves. - Staged []string // Decision is the record that made it a seat. Decision string } @@ -294,9 +285,8 @@ var defaultSeats = append([]Seat{ // Its verbs say what the machine resolves through and over which links (novox/hq ADR 0241): the // resolver file as it is and who wrote it, and every link with its addresses, routes and the resolvers // the manager knows for it — the same for every holder, so a question about a machine's names is asked - // the same way whatever manages its network. Staged until both holders serve them. - {Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", - Serves: uplinkVerbs(), Staged: []string{"resolvers", "links"}}, + // the same way whatever manages its network. Optional until both holders serve them. + {Name: "node-uplink", Scope: ScopeNode, Decision: "novox/hq ADR 0117", Serves: uplinkVerbs()}, }, // The graphical session's roles (novox/hq ADR 0208), last because they are a workstation's. graphicalSessionSeats()...) @@ -353,14 +343,23 @@ func UseSeats(s []Seat) { if d, known := byName[row.Name]; known { row.Receives = d.Receives row.Replicated = d.Replicated - // Staged verbs are never in the row: they come from the binary that promises them. - row.Staged = d.Staged + // Optional verbs are never in the row (Verb.Optional): they come from the binary that promises + // them, still optional; and a row kept from before the mark keeps none of its own required. + optional := map[string]Verb{} + for _, v := range d.Serves { + if v.Optional { + optional[v.Name] = v + } + } have := map[string]bool{} - for _, v := range row.Serves { + for i, v := range row.Serves { have[v.Name] = true + if o, ok := optional[v.Name]; ok { + row.Serves[i] = o + } } for _, v := range d.Serves { - if slices.Contains(d.Staged, v.Name) && !have[v.Name] { + if v.Optional && !have[v.Name] { row.Serves = append(row.Serves, v) } } @@ -503,7 +502,7 @@ func CanHold(m Manifest, seat Seat) error { // **Serving the seat's tools is a condition of holding it** (novox/hq ADR 0132). A holder that // does not answer what the role promises is every caller's timeout, found at registration and // at handover instead, naming the verbs rather than the fact that something is missing. - if missing := unservedVerbs(claimed.ServesFor(m), seat.Required()); len(missing) > 0 { + if missing := unservedVerbs(claimed.ServesFor(m), seat.Serves); len(missing) > 0 { return fmt.Errorf("%s claims %s but does not serve %s, which that seat's protocol promises "+ "(novox/hq ADR 0132) — a holder names every verb its seat declares, under the claim's "+ "serves or among its own tools", @@ -617,20 +616,15 @@ func serviceManagerVerbs() []Verb { } } -// Required is the verbs a holder must serve: every one the seat promises but those still staged. -func (s Seat) Required() []Verb { - var out []Verb +// Stored is the seat as its row keeps it: its optional verbs left out (Verb.Optional). +func (s Seat) Stored() Seat { + var kept []Verb for _, v := range s.Serves { - if !slices.Contains(s.Staged, v.Name) { - out = append(out, v) + if !v.Optional { + kept = append(kept, v) } } - return out -} - -// Stored is the seat as its row keeps it: its staged verbs left out (see Staged). -func (s Seat) Stored() Seat { - s.Serves = s.Required() + s.Serves = kept return s } @@ -642,11 +636,11 @@ func uplinkVerbs() []Verb { "domains and options it lists, whether it is the file the mesh declares, and when it is not, who " + "wrote it as far as the machine shows — its header, a backup a VPN client left beside it, a link " + "in its place, a known writer running.", - Input: schema(nil, nil)}, + Input: schema(nil, nil), Optional: true}, {Name: "links", Description: "Every network link on the machine: its state, its addresses, whether " + "the default route leaves through it, and the resolvers and search domains the network manager " + "knows for it — a VPN's tunnel included.", - Input: schema(nil, nil)}, + Input: schema(nil, nil), Optional: true}, } } diff --git a/internal/catalogue/uplink_verbs_test.go b/internal/catalogue/uplink_verbs_test.go index ae83e351..7c1e9307 100644 --- a/internal/catalogue/uplink_verbs_test.go +++ b/internal/catalogue/uplink_verbs_test.go @@ -6,8 +6,10 @@ import ( "testing" ) -// The node-uplink seat's verbs (novox/hq ADR 0241): promised and routed, staged — not yet a condition of +// The node-uplink seat's verbs (novox/hq ADR 0241): promised and routed, optional — not yet a condition of // holding — so the holders of today still hold it, a holder may name them, and nothing it does not promise. +// And an optional verb is never stored in the seat's row (Verb.Optional): a controller that reads the row +// back must still find it optional, and one older than the verb must not find it at all. func uplinkSeat(t *testing.T) Seat { t.Helper() @@ -25,9 +27,12 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) { var names []string for _, v := range seat.Serves { names = append(names, v.Name) + if !v.Optional { + t.Errorf("%s is required already", v.Name) + } } - if !slices.Equal(names, []string{"resolvers", "links"}) || len(seat.Required()) != 0 { - t.Fatalf("node-uplink promises %v and requires %v", names, seat.Required()) + if !slices.Equal(names, []string{"resolvers", "links"}) { + t.Fatalf("node-uplink promises %v", names) } today := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}} if err := CanHold(today, seat); err != nil { @@ -45,24 +50,44 @@ func TestTheUplinkSeatPromisesItsVerbsAndRequiresNoneYet(t *testing.T) { } } -func TestAStagedVerbIsNeverStoredAndComesBackFromTheBinary(t *testing.T) { +func TestAnOptionalVerbIsNeverStoredAndComesBackOptionalFromTheBinary(t *testing.T) { seat := uplinkSeat(t) if stored := seat.Stored(); len(stored.Serves) != 0 { t.Fatalf("the row would keep %v", stored.Serves) } before := seats t.Cleanup(func() { seats = before }) - // A row as the store keeps it: no protocol of its own. And one an operator gave a verb of its own. + optionalBoth := func(want int) { + t.Helper() + for _, s := range Seats() { + if s.Name != "node-uplink" { + continue + } + if len(s.Serves) != want { + t.Fatalf("the working set's node-uplink serves %+v", s.Serves) + } + holder := Manifest{Module: "networkmanager", Version: "1", Claims: []Claim{{Name: "node-uplink", Scope: ScopeNode}}} + for _, v := range s.Serves { + if v.Name != "operators-own" { + if !v.Optional { + t.Fatalf("%s came back required", v.Name) + } + } else { + holder.Claims[0].Serves = []string{"operators-own"} + } + } + if err := CanHold(holder, s); err != nil { + t.Fatalf("a holder serving none of the optional verbs is refused: %v", err) + } + } + } + // A row as the store keeps it: no protocol of its own. UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode}, {Name: "node-service-manager", Scope: ScopeNode}}) - for _, s := range Seats() { - if s.Name == "node-uplink" && (len(s.Serves) != 2 || len(s.Staged) != 2) { - t.Fatalf("the working set's node-uplink is %+v", s) - } - } + optionalBoth(2) + // A row with a verb an operator added: it keeps it and gains the optional ones. UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "operators-own"}}}}) - for _, s := range Seats() { - if s.Name == "node-uplink" && len(s.Serves) != 3 { - t.Fatalf("a row with a verb of its own lost or did not gain the staged ones: %+v", s.Serves) - } - } + optionalBoth(3) + // A row that stored the verb before it was marked (or by an older seeding): read back, still optional. + UseSeats([]Seat{{Name: "node-uplink", Scope: ScopeNode, Serves: []Verb{{Name: "resolvers"}, {Name: "links"}}}}) + optionalBoth(2) } diff --git a/internal/catalogue/verbs.go b/internal/catalogue/verbs.go index af3318fd..e025ba72 100644 --- a/internal/catalogue/verbs.go +++ b/internal/catalogue/verbs.go @@ -28,7 +28,9 @@ type Verb struct { // does not serve the verb, the controller before it refusing the holder that does — and every check of // the catalogue between the two would fail on a module nobody touched. Once every holder serves it, // the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the - // seat set's protocol is the compiled one. + // seat's row leaves an optional verb out (Seat.Stored) and the working set takes it, still optional, + // from the compiled seat (UseSeats) — the mark itself is not a column, so a stored verb would read back + // required, and a controller older than the verb would require it of every holder (novox/hq ADR 0241). Optional bool `json:"-"` } diff --git a/internal/inventory/seats.go b/internal/inventory/seats.go index 0832e6b7..b12503eb 100644 --- a/internal/inventory/seats.go +++ b/internal/inventory/seats.go @@ -62,7 +62,7 @@ func (i *Inventory) Seats(ctx context.Context) ([]catalogue.Seat, error) { func (i *Inventory) SeedSeats(ctx context.Context, defaults []catalogue.Seat) (int, error) { var added int for _, s := range defaults { - // A verb still staged is never stored (catalogue.Seat.Staged). + // An optional verb is never stored (catalogue.Verb.Optional). s = s.Stored() accepts, emits, serves, err := protocolJSON(s) if err != nil { diff --git a/internal/inventory/seats_test.go b/internal/inventory/seats_test.go index 94594362..168eed62 100644 --- a/internal/inventory/seats_test.go +++ b/internal/inventory/seats_test.go @@ -157,9 +157,10 @@ func TestTheControllersVerbsInTheRowAreTheBinarys(t *testing.T) { } } -// A verb still staged (novox/hq ADR 0241) is never in the store's row: a controller older than it reads -// the row and would require what no holder of its time serves. -func TestAStagedVerbIsNotSeeded(t *testing.T) { +// An optional verb (catalogue.Verb.Optional; novox/hq ADR 0241, ADR 0239) is never in the store's row: a +// controller older than it reads the row and would require what no holder of its time serves, and the mark +// itself is not stored, so the row would read back required. +func TestAnOptionalVerbIsNotSeeded(t *testing.T) { inv := ForTest(t) if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil { t.Fatal(err) @@ -173,8 +174,10 @@ func TestAStagedVerbIsNotSeeded(t *testing.T) { t.Fatal(err) } for _, s := range got { - if s.Name == "node-uplink" && len(s.Serves) != 0 { - t.Fatalf("the row keeps %v", s.Serves) + for _, v := range s.Serves { + if (s.Name == "node-uplink") || (s.Name == catalogue.DeliverySeat && v.Name == "checks") { + t.Fatalf("%s's row keeps the optional %s", s.Name, v.Name) + } } } }