From 52c18f7a45663ea1e9fea53ae5dde32fb27ae591 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 1 Oct 2026 00:01:41 +0200 Subject: [PATCH] The path-preservation proof resolves access ids to their default paths too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An access named by id (issue 153) resolves to the path the definition still carries when the assignment says nothing, and the proof compares that — the same rule as a placed directory. --- internal/catalogue/placed_paths_test.go | 50 +++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/internal/catalogue/placed_paths_test.go b/internal/catalogue/placed_paths_test.go index 2852546..779c151 100644 --- a/internal/catalogue/placed_paths_test.go +++ b/internal/catalogue/placed_paths_test.go @@ -60,6 +60,11 @@ func TestPlacedDirectoriesKeepTheirPaths(t *testing.T) { // (issue 119's conversion), and what must not move is the path a machine sees. was = resolvedTree(was, dirsFor(earlier, Rendering{}), module, t) resolved := resolvedTree(is, dirsFor(m, Rendering{}), module, t) + // An access named by id resolves to the path the definition still carries as its default + // (issue 153) — the same rule as a placed directory: an assignment that says nothing moves + // nothing. + was = accessesResolved(was, earlier) + resolved = accessesResolved(resolved, m) if !reflect.DeepEqual(was, resolved) { wasJSON, _ := json.MarshalIndent(was, "", " ") isJSON, _ := json.MarshalIndent(resolved, "", " ") @@ -128,3 +133,48 @@ func firstDifference(a, b string) string { } return "(the difference is beyond the shorter document)" } + +// accessesResolved fills every ${access:} with the default path the definition carries for that +// access, and drops the id, so a manifest that names its accesses is compared by the paths a machine +// with no placement receives. +func accessesResolved(node any, m Manifest) any { + defaults := map[string]string{} + for _, a := range m.Accesses { + if a.ID != "" && a.Path != "" { + defaults[a.ID] = a.Path + } + } + var walk func(any) any + walk = func(n any) any { + switch v := n.(type) { + case map[string]any: + out := map[string]any{} + for k, child := range v { + if k == "id" { + if _, isAccess := v["mode"]; isAccess && v["type"] == nil { + if _, hasPath := v["path"]; hasPath { + continue + } + } + } + out[k] = walk(child) + } + return out + case []any: + out := make([]any, len(v)) + for i, child := range v { + out[i] = walk(child) + } + return out + case string: + return accessRef.ReplaceAllStringFunc(v, func(ref string) string { + if p, ok := defaults[accessRef.FindStringSubmatch(ref)[1]]; ok { + return p + } + return ref + }) + } + return n + } + return walk(node) +}