package apply import ( "context" "path/filepath" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/declaration" "github.com/novox/mesh-host/internal/store" ) // R6-A AC3: given two modules declaring one path, when one module's want changes, the shared path is neither removed // nor taken from the other module (novox/hq ADR 0539 ยง9). func TestASharedPathIsNeitherRemovedNorTakenFromTheOtherModule(t *testing.T) { dir := t.TempDir() shared := filepath.Join(dir, "shared.conf") own := filepath.Join(dir, "a-own.conf") a := declare(t, `{"id":"a.shared","type":"file","path":"`+shared+`","content":"x\n","module":"a"}, {"id":"a.own","type":"file","path":"`+own+`","content":"a\n","module":"a"}`) b := declare(t, `{"id":"b.shared","type":"file","path":"`+shared+`","content":"x\n","module":"b"}`) state := store.State{} var err error _, state, err = ApplyModule(context.Background(), archHost(t), a, Scope{Module: "a", Others: b.Resources}, state, store.OriginDeclared, noServices, nil, nil, nil, nil) if err != nil { t.Fatal(err) } _, state, err = ApplyModule(context.Background(), archHost(t), b, Scope{Module: "b", Others: a.Resources}, state, store.OriginDeclared, noServices, nil, nil, nil, nil) if err != nil { t.Fatal(err) } // A's want changes: it no longer declares the shared path, and b still does. a2 := declare(t, `{"id":"a.own","type":"file","path":"`+own+`","content":"a\n","module":"a"}`) report, state, err := ApplyModule(context.Background(), archHost(t), a2, Scope{Module: "a", Others: b.Resources}, state, store.OriginDeclared, noServices, nil, nil, nil, nil) if err != nil { t.Fatal(err) } if !exists(shared) { t.Fatal("the path another module still declares was removed") } if _, still := state.Find("b.shared"); !still { t.Fatal("the other module's record was taken away") } if _, still := state.Find("a.shared"); still { t.Fatal("the module's own record of the shared path was kept") } if o := outcomeOf(report, "a.shared"); o.Action != "forgotten" { t.Fatalf("the shared path: %+v", o) } } // R6-A AC4: given modules A and B on a node, when A's want is removed, only A's resources are removed and B is // untouched. func TestARemovedModuleTakesOnlyItsOwnResources(t *testing.T) { dir := t.TempDir() fa, fb := filepath.Join(dir, "a.conf"), filepath.Join(dir, "b.conf") d := declare(t, `{"id":"a.conf","type":"file","path":"`+fa+`","content":"a\n","module":"a"}, {"id":"b.conf","type":"file","path":"`+fb+`","content":"b\n","module":"b"}`) _, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } if rec, _ := state.Find("a.conf"); rec.Module != "a" { t.Fatalf("the owner was not recorded: %+v", rec) } gone := &declaration.Declaration{} b := declare(t, `{"id":"b.conf","type":"file","path":"`+fb+`","content":"b\n","module":"b"}`) report, state, err := ApplyModule(context.Background(), archHost(t), gone, Scope{Module: "a", Others: b.Resources}, state, store.OriginDeclared, noServices, nil, nil, nil, nil) if err != nil { t.Fatal(err) } if exists(fa) || !exists(fb) { t.Fatalf("a's file exists %v, b's file exists %v", exists(fa), exists(fb)) } if o := outcomeOf(report, "a.conf"); o.Action != "removed" { t.Fatalf("a's file: %+v", o) } if _, still := state.Find("b.conf"); !still { t.Fatal("b's record was removed") } if _, still := state.Find("a.conf"); still { t.Fatal("a's record is still there") } } // A record from before the owner was kept is read by its id, so the first removal after the switch is the module's too. func TestARecordWithoutAnOwnerBelongsToTheModuleItsIdNames(t *testing.T) { dir := t.TempDir() fa, fb := filepath.Join(dir, "a.conf"), filepath.Join(dir, "b.conf") d := declare(t, `{"id":"a.conf","type":"file","path":"`+fa+`","content":"a\n"}, {"id":"b.conf","type":"file","path":"`+fb+`","content":"b\n"}`) _, state, err := Apply(context.Background(), archHost(t), d, store.State{}, store.OriginDeclared, noServices, nil, nil) if err != nil { t.Fatal(err) } for i := range state.Resources { state.Resources[i].Module = "" } _, state, err = ApplyModule(context.Background(), archHost(t), &declaration.Declaration{}, Scope{Module: "a"}, state, store.OriginDeclared, noServices, nil, nil, nil, nil) if err != nil { t.Fatal(err) } if exists(fa) || !exists(fb) { t.Fatalf("a's file exists %v, b's file exists %v", exists(fa), exists(fb)) } } // A resource that leaves the node's own composition for its module's own is that module's now: the node's own apply // leaves it as it is, whatever its type, until the module's own apply takes it (issue 1945, live on 2026-10-25 at 02:32: // shanks and g14 lost systemd-resolved's guard, its resolver files and sudo's operator file, ace qbittorrent's server, // because the node's own apply removed every record no longer in it, the others holding the very same resources or not). // // And a resource the node's own composition carried for a module a want still places here (a folded module of the old // form, held) is never removed by the node's own apply when a composition leaves it out: the apply is refused whole and // says why (issue 774, live on 2026-10-29 at 07:54 UTC on the bus node: the node's own composition, composed without what // it ran once the kept declaration was read no more, removed the bus server, the proxy and three held modules). Only its // want's removal lets it go. func TestAResourceThatMovedToItsModulesCompositionIsLeftByTheNodesOwnApply(t *testing.T) { for _, tc := range []struct { name string id string // the resource the node's own composition carried, and no longer does moved bool // its module's own composition now declares it (Others) placed bool // a want still places its module here refused bool // the node's own apply is refused whole stays bool // the resource is still on the machine and recorded // wants names the modules whose want says removed (Scope.RemovedByWant); nil leaves the guard to Placed alone. wants []string }{ {name: "moved to its module's own composition", id: "systemd-resolved.fact-suffix", moved: true, placed: true, stays: true}, {name: "a held folded module, in no composition", id: "nats.server-conf", placed: true, refused: true, stays: true}, {name: "its module's want removed", id: "nats.server-conf"}, // Issue 774, review of the re-land, round 1: a module folded in the node's own has no want, so Placed does not name // it; in the node's own apply only a module a want removed loses its resources. {name: "a folded module with no want", id: "nats.server-conf", wants: []string{}, refused: true, stays: true}, {name: "a folded module a want removed", id: "nats.server-conf", wants: []string{"nats", "old"}}, } { t.Run(tc.name, func(t *testing.T) { dir := t.TempDir() fact := filepath.Join(dir, "carried") gone := filepath.Join(dir, "gone.conf") own := declare(t, `{"id":"`+tc.id+`","type":"file","path":"`+fact+`","content":"mesh\n"}, {"id":"old.gone","type":"file","path":"`+gone+`","content":"g\n"}`) _, state, err := ApplyModule(context.Background(), archHost(t), own, Scope{Module: store.NodeModule}, state0(), store.OriginDeclared, noServices, nil, nil, nil, nil) if err != nil { t.Fatal(err) } module, _ := declaration.ModuleGuess(tc.id) scope := Scope{Module: store.NodeModule} if tc.moved { moved := declare(t, `{"id":"`+tc.id+`","type":"file","path":"`+fact+`","content":"mesh\n","module":"`+module+`"}`) scope.Others = moved.Resources } if tc.placed { scope.Placed = map[string]bool{module: true} } if tc.wants != nil { scope.RemovedByWant = map[string]bool{} for _, m := range tc.wants { scope.RemovedByWant[m] = true } } report, after, err := ApplyModule(context.Background(), archHost(t), &declaration.Declaration{}, scope, state, store.OriginDeclared, noServices, nil, nil, nil, nil) if tc.refused != (err != nil) { t.Fatalf("refused %v, want %v: %v", err != nil, tc.refused, err) } if tc.refused { if !strings.Contains(err.Error(), tc.id+" of "+module) { t.Fatalf("the refusal does not name what it kept: %v", err) } after = state } if exists(fact) != tc.stays { t.Fatalf("the carried resource exists %v, want %v: %+v", exists(fact), tc.stays, outcomeOf(report, tc.id)) } if _, still := after.Find(tc.id); still != tc.stays { t.Fatalf("the carried resource's record kept %v, want %v", still, tc.stays) } if exists(gone) == !tc.refused { t.Fatalf("a resource no composition holds any more exists %v, with the apply refused %v", exists(gone), tc.refused) } }) } } func state0() store.State { return store.State{} }