The node-engine refused the whole pass when two modules claimed a seat held once, or when a consumer's credential was refused. Now the composer leaves that module out of the declaration and composes the rest: the module is returned held (Resolved.Held, resolution.ResolveHolding), reaches the declaration as left out through the filter and the declaration #218 already keep (Rendering.NotPlaced), and Declared.LeftOut names it. The second claimant of a seat is the one held; its claim is not this node's holding. The rest-of-mesh view and the network use the same, so a node with a held module stays on the network. Resolve stays strict for callers that cannot leave a module out. Quick fix: hold only the credential refusal (resolve.go planForNow), about 20 minutes. Thorough fix, chosen: every claim problem is attributed to its module (claimsOf) and both kinds go through the one left-out path of ADR 0163 rule 6, in the declaration, the filter and the network, so no per-kind refusal of a pass remains in the composer. Left for the next step: a declared-and-not-made own secret (NotMadeError) and the Foreseeing-mode refusal still refuse the pass; the frombus candidate path holds those by stub already. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
129 lines
5.2 KiB
Go
129 lines
5.2 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
func busSeatDelivering(t *testing.T, delivers string) {
|
|
t.Helper()
|
|
was := mcat.Seats()
|
|
t.Cleanup(func() { mcat.UseSeats(was) })
|
|
mcat.UseSeats([]mcat.Seat{{Name: "mesh-broker", Scope: mcat.ScopeMesh, Delivers: delivers, Decision: "test"}})
|
|
}
|
|
|
|
func oldBroker() mcat.Manifest {
|
|
return mcat.Manifest{Module: "old-broker", Provides: []mcat.Offer{{Name: "mesh-bus", Scope: mcat.ScopeMesh}},
|
|
Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh, Serves: brokerVerbs()}}}
|
|
}
|
|
|
|
func newBroker() mcat.Manifest {
|
|
return mcat.Manifest{Module: "new-broker", Provides: []mcat.Offer{{Name: "mesh-bus", Scope: mcat.ScopeMesh}},
|
|
Claims: []mcat.Claim{{Name: "mesh-broker", Scope: mcat.ScopeMesh, Serves: brokerVerbs()}}}
|
|
}
|
|
|
|
// Nothing on record: exactly the old rule. One claimant holds; two are refused.
|
|
func TestWithNoHolderOnRecordTheSoleClaimantHoldsAndTwoAreRefused(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
node := mcat.Node{Name: "anchor"}
|
|
|
|
held, problems := checkClaims([]mcat.Manifest{oldBroker()}, node, nil, nil)
|
|
if len(problems) != 0 || len(held) != 1 || held[0].Module != "old-broker" {
|
|
t.Fatalf("a sole claimant did not hold the seat: held=%v problems=%v", held, problems)
|
|
}
|
|
_, problems = checkClaims([]mcat.Manifest{oldBroker(), newBroker()}, node, nil, nil)
|
|
if len(problems) != 1 || !strings.Contains(problems[0], "both claim") {
|
|
t.Fatalf("two claimants with nothing on record were not refused: %v", problems)
|
|
}
|
|
// Held for the module that cannot hold, which then holds nothing; the other still does (issue 1966).
|
|
held, holding := holdClaims([]mcat.Manifest{oldBroker(), newBroker()}, node, nil, nil)
|
|
if len(holding) != 1 || !strings.Contains(holding["new-broker"], "both claim") || len(held) != 1 || held[0].Module != "old-broker" {
|
|
t.Fatalf("the second claimant is not the one held: held=%v holding=%v", held, holding)
|
|
}
|
|
}
|
|
|
|
// With a holder on record, the other eligible assignment is silent: not refused, and not holding.
|
|
func TestTheHolderOnRecordHoldsAndTheOtherClaimantStandsBesideIt(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
node := mcat.Node{Name: "anchor"}
|
|
record := []mcat.Held{{Claim: "mesh-broker", Scope: mcat.ScopeMesh, Node: "anchor", Module: "new-broker"}}
|
|
|
|
held, problems := checkClaims([]mcat.Manifest{oldBroker(), newBroker()}, node, nil, record)
|
|
if len(problems) != 0 {
|
|
t.Fatalf("the assignment beside the holder was refused: %v", problems)
|
|
}
|
|
if len(held) != 1 || held[0].Module != "new-broker" {
|
|
t.Fatalf("the holder on record is not the one holding: %v", held)
|
|
}
|
|
}
|
|
|
|
// The record names a node too: an eligible module on another machine holds nothing, and its
|
|
// machine's set still resolves.
|
|
func TestAHolderOnRecordElsewhereLeavesThisMachinesClaimantSilent(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
record := []mcat.Held{{Claim: "mesh-broker", Scope: mcat.ScopeMesh, Node: "anchor", Module: "new-broker"}}
|
|
|
|
held, problems := checkClaims([]mcat.Manifest{oldBroker()}, mcat.Node{Name: "laptop"}, nil, record)
|
|
if len(problems) != 0 || len(held) != 0 {
|
|
t.Fatalf("a claimant elsewhere than the recorded holder was not simply silent: held=%v problems=%v",
|
|
held, problems)
|
|
}
|
|
}
|
|
|
|
// A machine being moved is handed its membership for the new bus as a sealed file in its own
|
|
// declaration — the machine's, not any module's (design 28, task 5.2).
|
|
func TestAMembershipForTheNewBusIsComposedAsASealedFile(t *testing.T) {
|
|
r := mcat.Resolution{Node: "anchor"}
|
|
got, err := Compose(r, mcat.Rendering{BusMembership: "sealed-blob"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var found map[string]any
|
|
for _, res := range got.Resources {
|
|
if res["id"] == BusMembershipID() {
|
|
found = res
|
|
}
|
|
}
|
|
if found == nil {
|
|
t.Fatalf("no membership resource in %v", got.Resources)
|
|
}
|
|
if found["path"] != mcat.BusMembershipPath || found["sealed"] != "sealed-blob" || found["mode"] != "0600" {
|
|
t.Fatalf("the membership is not a sealed 0600 file where the host reads it: %v", found)
|
|
}
|
|
// And a machine not being moved is handed nothing.
|
|
got, _ = Compose(r, mcat.Rendering{})
|
|
for _, res := range got.Resources {
|
|
if res["id"] == BusMembershipID() {
|
|
t.Fatal("a machine with no membership on record was handed one")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestARefusalWithNothingOnRecordNamesTheHandover(t *testing.T) {
|
|
busSeatDelivering(t, "mesh-bus")
|
|
elsewhere := []mcat.Held{{Claim: "mesh-broker", Scope: mcat.ScopeMesh, Node: "anchor", Module: "old-broker"}}
|
|
|
|
_, problems := checkClaims([]mcat.Manifest{newBroker()}, mcat.Node{Name: "laptop"}, elsewhere, nil)
|
|
if len(problems) != 1 {
|
|
t.Fatalf("two derived claimants across machines were not refused: %v", problems)
|
|
}
|
|
if !strings.Contains(problems[0], "`seat mesh-broker --to anchor/old-broker`") {
|
|
t.Fatalf("the refusal does not name the handover that records the holder: %s", problems[0])
|
|
}
|
|
}
|
|
|
|
// brokerVerbs are the verbs the seat mesh-broker promises its holder, as the contracts define them: a holder names every one
|
|
// (ADR 0132), so a fixture holder does (issue 1966 ENG-3).
|
|
func brokerVerbs() []string {
|
|
seat, _ := mcat.SeatNamed("mesh-broker")
|
|
var out []string
|
|
for _, v := range seat.Serves {
|
|
if !v.Optional {
|
|
out = append(out, v.Name)
|
|
}
|
|
}
|
|
return out
|
|
}
|