Refuse an unmet seat dependency the catalogue could meet (hq ADR 0207 §4)

status reported no unmet dependency on any node once systemd, pacman and docker
were assigned to all four (to-be 42), which is the condition ADR 0207 set for the
switch. A dependency no catalogue module could meet stays a report before and
after the switch, as assign already said it: there is no remedy to name.
This commit is contained in:
jochen
2026-10-04 13:07:45 +02:00
parent ec2e6255a9
commit 35314175f2
4 changed files with 41 additions and 19 deletions
@@ -105,6 +105,8 @@ func TestUnassigningTheLastHolderUnderItsDependentsIsRefused(t *testing.T) {
} }
func TestStatusReportsAnUnheldDependencyWithoutRefusingTheMachine(t *testing.T) { func TestStatusReportsAnUnheldDependencyWithoutRefusingTheMachine(t *testing.T) {
// The mesh as it ran before the switch (novox/hq ADR 0207 §4).
defer catalogue.EnforcingSeatDependencies(false)()
open := aMesh(t) open := aMesh(t)
ctx := t.Context() ctx := t.Context()
register(t, open, serviceManagerHolder()) register(t, open, serviceManagerHolder())
+5
View File
@@ -675,11 +675,16 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world
// Reported until the switch; refused after it, though never in the first pass, whose refusals // Reported until the switch; refused after it, though never in the first pass, whose refusals
// make a machine vanish from the network rather than report anything. // make a machine vanish from the network rather than report anything.
resolution.Unheld = UnheldDependencies(catalogue, node.Name, resolution.Modules, nil) resolution.Unheld = UnheldDependencies(catalogue, node.Name, resolution.Modules, nil)
// Only a dependency some module in the catalogue could meet is refused: one with no possible
// holder has no remedy to name, and stays a report in `status` (novox/hq ADR 0207 §4, read with
// the assign rule above it).
if enforceSeatDependencies && !world.Unchecked { if enforceSeatDependencies && !world.Unchecked {
for _, u := range resolution.Unheld { for _, u := range resolution.Unheld {
if len(u.Holders) > 0 {
problems = append(problems, u.String()) problems = append(problems, u.String())
} }
} }
}
if len(problems) > 0 { if len(problems) > 0 {
sort.Strings(problems) sort.Strings(problems)
+15 -4
View File
@@ -39,8 +39,19 @@ var appliedThrough = map[string]string{
// which is when the three holders are assigned to every node: switching it before then would stop // which is when the three holders are assigned to every node: switching it before then would stop
// every machine lacking one from being sent anything at all. // every machine lacking one from being sent anything at all.
// //
// Switched on 2026-10-04, when `status` first reported no unmet dependency on any node: systemd,
// pacman and docker were assigned to all four machines that afternoon (novox/hq to-be 42).
//
// A variable rather than a constant only so a test can hold both behaviours; nothing else sets it. // A variable rather than a constant only so a test can hold both behaviours; nothing else sets it.
var enforceSeatDependencies = false var enforceSeatDependencies = true
// EnforcingSeatDependencies sets the switch and returns what puts it back. For tests in other
// packages that hold the behaviour from before the switch; nothing else calls it.
func EnforcingSeatDependencies(on bool) (restore func()) {
was := enforceSeatDependencies
enforceSeatDependencies = on
return func() { enforceSeatDependencies = was }
}
// foundationModules are the pieces genesis lays before any module exists (novox/hq ADR 0207 §5): // foundationModules are the pieces genesis lays before any module exists (novox/hq ADR 0207 §5):
// the host and the private network. Registered as modules so they can be assigned, but what they // the host and the private network. Registered as modules so they can be assigned, but what they
@@ -207,8 +218,8 @@ func manifestsOf(catalogue map[string]Manifest, names []string) []Manifest {
// **A dependency nothing in the catalogue can meet is said, not refused.** A refusal names the // **A dependency nothing in the catalogue can meet is said, not refused.** A refusal names the
// module that would meet it; with none registered there is no remedy to name, and refusing would // module that would meet it; with none registered there is no remedy to name, and refusing would
// stop every assignment of that kind until a module that does not exist yet is written. The answer // stop every assignment of that kind until a module that does not exist yet is written. The answer
// still says it, and `status` reports it, until the switch (enforceSeatDependencies) makes the mesh // still says it, and `status` reports it — before the switch and after it alike: there is never a
// refuse what it cannot meet. The first return is those lines. // remedy to name for it. The first return is those lines.
func AssignRefusal(catalogue map[string]Manifest, node string, assigned, adding []string) ([]string, error) { func AssignRefusal(catalogue map[string]Manifest, node string, assigned, adding []string) ([]string, error) {
set := manifestsOf(catalogue, append(append([]string(nil), assigned...), adding...)) set := manifestsOf(catalogue, append(append([]string(nil), assigned...), adding...))
judged := map[string]bool{} judged := map[string]bool{}
@@ -217,7 +228,7 @@ func AssignRefusal(catalogue map[string]Manifest, node string, assigned, adding
} }
var refused, said []string var refused, said []string
for _, u := range UnheldDependencies(catalogue, node, set, judged) { for _, u := range UnheldDependencies(catalogue, node, set, judged) {
if len(u.Holders) == 0 && !enforceSeatDependencies { if len(u.Holders) == 0 {
said = append(said, u.String()) said = append(said, u.String())
continue continue
} }
+14 -10
View File
@@ -120,24 +120,25 @@ func TestAnAssignmentMissingAHolderIsRefusedNamingTheSeatAndItsPossibleHolders(t
} }
} }
func TestADependencyNoCatalogueModuleCanMeetIsSaidNotRefusedUntilTheSwitch(t *testing.T) { func TestADependencyNoCatalogueModuleCanMeetIsSaidNeverRefused(t *testing.T) {
// No runtime module in the catalogue: refusing would stop every container's assignment until one // No runtime module in the catalogue: refusing would stop every container's assignment until one
// is written, with no remedy to name. // is written, with no remedy to name — so it is said, with the switch on as with it off.
web := withResources(mod("web", nil, nil, nil), res("container", "web")) web := withResources(mod("web", nil, nil, nil), res("container", "web"))
cat := shelf(web) cat := shelf(web)
for _, on := range []bool{false, true} {
enforceSeatDependencies = on
said, err := AssignRefusal(cat, "workstation", nil, []string{"web"}) said, err := AssignRefusal(cat, "workstation", nil, []string{"web"})
if err != nil { if err != nil {
t.Fatalf("a dependency nothing could meet was refused: %v", err) t.Fatalf("switch %v: a dependency nothing could meet was refused: %v", on, err)
} }
if len(said) != 1 || !strings.Contains(said[0], "no module in the catalogue claims it yet") { if len(said) != 1 || !strings.Contains(said[0], "no module in the catalogue claims it yet") {
t.Errorf("the assignment does not say what it depends on: %v", said) t.Errorf("switch %v: the assignment does not say what it depends on: %v", on, said)
}
if _, err := Resolve(cat, []string{"web"}, workstation(), World{}); err != nil {
t.Errorf("switch %v: a dependency nothing could meet refused the node: %v", on, err)
}
} }
enforceSeatDependencies = true enforceSeatDependencies = true
defer func() { enforceSeatDependencies = false }()
if _, err := AssignRefusal(cat, "workstation", nil, []string{"web"}); err == nil {
t.Error("with the switch on, a dependency nothing could meet was not refused")
}
} }
func TestTheHoldersMutualDependenceResolvesWhenAssignedTogether(t *testing.T) { func TestTheHoldersMutualDependenceResolvesWhenAssignedTogether(t *testing.T) {
@@ -165,6 +166,9 @@ func TestTheHoldersMutualDependenceResolvesWhenAssignedTogether(t *testing.T) {
} }
func TestStatusIsToldOfAnUnmetDependencyAndTheNodeStillResolves(t *testing.T) { func TestStatusIsToldOfAnUnmetDependencyAndTheNodeStillResolves(t *testing.T) {
// Before the switch (the state the mesh ran in until every node held the three seats).
enforceSeatDependencies = false
defer func() { enforceSeatDependencies = true }()
web := withResources(mod("web", nil, nil, nil), res("container", "web")) web := withResources(mod("web", nil, nil, nil), res("container", "web"))
cat := shelf(append(coreThree(), web)...) cat := shelf(append(coreThree(), web)...)
got, err := Resolve(cat, []string{"web"}, workstation(), World{}) got, err := Resolve(cat, []string{"web"}, workstation(), World{})
@@ -179,7 +183,7 @@ func TestStatusIsToldOfAnUnmetDependencyAndTheNodeStillResolves(t *testing.T) {
func TestWithTheSwitchFlippedAnUnmetDependencyRefusesTheNode(t *testing.T) { func TestWithTheSwitchFlippedAnUnmetDependencyRefusesTheNode(t *testing.T) {
enforceSeatDependencies = true enforceSeatDependencies = true
defer func() { enforceSeatDependencies = false }() defer func() { enforceSeatDependencies = true }()
web := withResources(mod("web", nil, nil, nil), res("container", "web")) web := withResources(mod("web", nil, nil, nil), res("container", "web"))
cat := shelf(append(coreThree(), web)...) cat := shelf(append(coreThree(), web)...)
_, err := Resolve(cat, []string{"web"}, workstation(), World{}) _, err := Resolve(cat, []string{"web"}, workstation(), World{})