diff --git a/internal/catalogue/licence_test.go b/internal/catalogue/licence_test.go index c0c33bc..3cc9581 100644 --- a/internal/catalogue/licence_test.go +++ b/internal/catalogue/licence_test.go @@ -148,3 +148,40 @@ func TestALicenceDeliversWhatIsPublicAndWhatIsSealed(t *testing.T) { t.Fatal("the key was written into the public file as well") } } + +// One machine's unresolvable module must not remove it from the private network. +// +// The pass that answers *what does this node offer* takes a failed resolution to mean it learned +// nothing — so refusing an unanswerable requirement there makes the machine disappear, and every +// other machine is then told, wrongly, that the two of them share no network. A wrong answer about +// a machine nobody asked about, caused by a fault on a third. +func TestAnUnanswerableRequirementDoesNotRemoveAMachineFromTheMesh(t *testing.T) { + shelf := map[string]Manifest{ + "assistant": aModelUser(), + "networking": {Module: "networking", + Provides: []Offer{{Name: "mesh-network", Scope: ScopeNode}}}, + } + // The first pass: what does this machine offer? It is assigned something nothing answers. + got, err := Resolve(shelf, []string{"assistant", "networking"}, + Node{Name: "laptop"}, World{Unchecked: true}) + if err != nil { + t.Fatalf("a machine with one unanswerable requirement was lost entirely: %v", err) + } + var offers bool + for _, m := range got.Modules { + for _, o := range m.Offers() { + if o == "mesh-network" { + offers = true + } + } + } + if !offers { + t.Fatal("the machine's own network module was not seen, so it looks off the network") + } + + // And the second pass, where the question is actually being asked, still refuses it. + if _, err := Resolve(shelf, []string{"assistant", "networking"}, + Node{Name: "laptop"}, World{}); err == nil { + t.Fatal("the requirement nothing answers was accepted when it was actually asked") + } +} diff --git a/internal/catalogue/resolve.go b/internal/catalogue/resolve.go index 6725691..4f6e41a 100644 --- a/internal/catalogue/resolve.go +++ b/internal/catalogue/resolve.go @@ -324,6 +324,20 @@ func Resolve(catalogue map[string]Manifest, assigned []string, node Node, world reported[want] = true continue } + if world.Unchecked { + // The first pass, which exists only to answer *what does this node offer*. + // + // **Refusing here makes the machine vanish** rather than reporting a problem: the + // caller takes a failed resolution to mean it learned nothing about this node, so + // one unanswerable requirement on one machine removes that machine from the + // private network as far as every OTHER machine is concerned — and they are then + // told, wrongly, that the two of them share no network. + // + // That is a wrong answer about a machine nobody asked about, caused by a fault on + // a third. The second pass refuses it properly, where the question is being asked. + reported[want] = true + continue + } reported[want] = true problems = append(problems, fmt.Sprintf( "nothing provides %q, wanted by %s", want, because[want]))