ZDDC/zddc/internal/zddc/walker_test.go
ZDDC 2f08418fb0 feat(zddc): Phase 2 — paths: walker, recursive cascade
Adds the recursive paths: schema and the cascade walker that threads
ancestor virtual contributions through to descendant levels.

Schema:
  paths:
    "*":              # literal-segment or "*" segment-wildcard key
      paths:          # recursive — each step matches one segment
        archive:
          paths:
            "*":
              paths:
                incoming:
                  title: "demo"

Each on-disk .zddc and the embedded defaults can declare paths:; the
walker collects every matching subtree and merges its contributions
into chain.Levels[depth] using mergeOverlay (per-field overlay with
on-disk most specific). The matched glob descends one segment at a
time; the value's own paths: becomes a new virtual source for deeper
matches.

Semantics:
  - matchGlob: literal key first (case-insensitive on segment),
    "*" wildcard fallback.
  - mergeOverlay: top wins per-field on scalars; maps merge key-by-
    key with top overriding; lists concat-dedupe; Paths replaces
    (recursive walker threads it through naturally).
  - inherit:false at any on-disk level drops accumulated ancestor
    virtual sources AND zeroes chain.Embedded — the operator owns
    every rule from that level outward.
  - Behaviour is bit-identical when no .zddc declares paths:; the
    walker reduces to the prior linear cascade.

Eight new tests cover the glob match table, ancestor-paths
contribution, on-disk-wins override, paths-absent bit-identical
behaviour, and inherit:false dropping ancestor paths: contributions.
All existing tests still pass.

Phase 3 next: populate defaults.zddc.yaml with the canonical
ZDDC convention via paths:, and replace apps.DefaultAppAt /
AppAvailableAt / AutoOwnCanonicalNames / VirtualOnlyCanonicalNames /
IsProjectRootFolder / IsArchivePartyFolder with cascade lookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:55:12 -05:00

190 lines
5.9 KiB
Go

package zddc
import (
"os"
"path/filepath"
"testing"
)
// TestMatchGlob_LiteralWinsOverWildcard — a literal key always beats
// the "*" fallback even when both are present.
func TestMatchGlob_LiteralWinsOverWildcard(t *testing.T) {
m := map[string]ZddcFile{
"archive": {Title: "literal"},
"*": {Title: "wildcard"},
}
got := matchGlob(m, "archive")
if got == nil || got.Title != "literal" {
t.Errorf("matchGlob(archive) = %+v, want literal", got)
}
}
// TestMatchGlob_CaseInsensitiveLiteral — operator writes lowercase,
// on-disk segment is PascalCase. Match should still hit.
func TestMatchGlob_CaseInsensitiveLiteral(t *testing.T) {
m := map[string]ZddcFile{"archive": {Title: "ok"}}
got := matchGlob(m, "Archive")
if got == nil || got.Title != "ok" {
t.Errorf("matchGlob(Archive) = %+v, want case-insensitive match", got)
}
}
// TestMatchGlob_WildcardFallback — no literal key, the * key matches
// any segment.
func TestMatchGlob_WildcardFallback(t *testing.T) {
m := map[string]ZddcFile{"*": {Title: "any"}}
got := matchGlob(m, "Project-1")
if got == nil || got.Title != "any" {
t.Errorf("matchGlob(Project-1) under * = %+v, want wildcard match", got)
}
}
// TestMatchGlob_NoMatch — no literal, no wildcard → nil.
func TestMatchGlob_NoMatch(t *testing.T) {
m := map[string]ZddcFile{"archive": {}}
if got := matchGlob(m, "working"); got != nil {
t.Errorf("matchGlob(working) = %+v, want nil", got)
}
}
// TestEffectivePolicy_PathsContributesViaWildcard — a root-level
// paths: tree with "*" / archive / incoming applies to a deep path
// even when none of those directories have a real .zddc.
func TestEffectivePolicy_PathsContributesViaWildcard(t *testing.T) {
resetCache()
root := t.TempDir()
// Root .zddc declares behaviour for any-project / archive /
// any-party / incoming via a nested paths: tree. The actual
// directories may not exist; the walker fires regardless.
writeZddc(t, root, `title: root
paths:
"*":
paths:
archive:
display:
incoming: "Inbox"
paths:
"*":
paths:
incoming:
title: "incoming-leaf"
`)
leaf := filepath.Join(root, "Project-1", "archive", "PartyA", "incoming")
if err := os.MkdirAll(leaf, 0o755); err != nil {
t.Fatal(err)
}
chain, err := EffectivePolicy(root, leaf)
if err != nil {
t.Fatal(err)
}
// chain.Levels: [root, Project-1, archive, PartyA, incoming] → 5
if got, want := len(chain.Levels), 5; got != want {
t.Fatalf("len(chain.Levels) = %d, want %d", got, want)
}
// At /Project-1/archive/ the display override should now be
// merged in via the ancestor paths.
if got := chain.Levels[2].Display["incoming"]; got != "Inbox" {
t.Errorf("Levels[2].Display[incoming] = %q, want Inbox", got)
}
// At the leaf, the deepest paths: contribution sets title.
if got := chain.Levels[4].Title; got != "incoming-leaf" {
t.Errorf("Levels[4].Title = %q, want incoming-leaf", got)
}
}
// TestEffectivePolicy_PathsOnDiskWins — virtual contributions are
// lower specificity than on-disk; on-disk overrides per-field.
func TestEffectivePolicy_PathsOnDiskWins(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `paths:
archive:
title: "from-virtual"
`)
archive := filepath.Join(root, "archive")
if err := os.MkdirAll(archive, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, archive, `title: "from-on-disk"`)
chain, err := EffectivePolicy(root, archive)
if err != nil {
t.Fatal(err)
}
// chain.Levels[1] is /archive — should reflect on-disk wins.
if got := chain.Levels[1].Title; got != "from-on-disk" {
t.Errorf("Levels[1].Title = %q, want %q (on-disk overrides virtual)",
got, "from-on-disk")
}
}
// TestEffectivePolicy_PathsAbsentIsBitIdentical — without a paths:
// declaration anywhere, the walker reduces to the prior linear
// cascade behaviour.
func TestEffectivePolicy_PathsAbsentIsBitIdentical(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `title: "root"
admins:
- admin@example.com
`)
leaf := filepath.Join(root, "Project-1", "archive")
if err := os.MkdirAll(leaf, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, leaf, `title: "leaf"`)
chain, err := EffectivePolicy(root, leaf)
if err != nil {
t.Fatal(err)
}
if got := chain.Levels[0].Title; got != "root" {
t.Errorf("Levels[0].Title = %q, want %q", got, "root")
}
if got := chain.Levels[2].Title; got != "leaf" {
t.Errorf("Levels[2].Title = %q, want %q", got, "leaf")
}
// Mid level has no .zddc and no paths: contribution → empty.
if got := chain.Levels[1].Title; got != "" {
t.Errorf("Levels[1].Title = %q, want empty", got)
}
}
// TestEffectivePolicy_InheritFalseDropsAncestorPaths — inherit:false
// at the on-disk root drops contributions from the embedded layer
// (already covered in defaults_test.go) AND drops contributions
// from ancestor paths: that hadn't yet matched. Here we set it on
// the project level: an embedded paths: contribution that would
// have applied to deeper levels is suppressed.
func TestEffectivePolicy_InheritFalseDropsAncestorPaths(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, `paths:
Project-1:
title: "ancestor-virtual"
paths:
sub:
title: "ancestor-deep"
`)
sub := filepath.Join(root, "Project-1", "sub")
if err := os.MkdirAll(sub, 0o755); err != nil {
t.Fatal(err)
}
// Project-1/.zddc sets inherit:false. Effect: the ancestor's
// /Project-1/sub/ contribution does NOT reach the sub level.
writeZddc(t, filepath.Join(root, "Project-1"),
`title: "standalone"
inherit: false
`)
chain, err := EffectivePolicy(root, sub)
if err != nil {
t.Fatal(err)
}
if got := chain.Levels[1].Title; got != "standalone" {
t.Errorf("Levels[1].Title = %q, want standalone (inherit:false should drop ancestor virtual contribution)",
got)
}
if got := chain.Levels[2].Title; got != "" {
t.Errorf("Levels[2].Title = %q, want empty (ancestor paths: blocked by inherit:false)",
got)
}
}