ZDDC/zddc/internal/zddc/lookups_test.go
ZDDC 73e34bed5e feat: per-party RSK + project-level SSR/MDL/RSK rollup tables
Adds the risk register as a sibling of MDL under archive/<party>/, and
three project-level virtual aggregations at <project>/{ssr,mdl,rsk}:

  - SSR aggregates archive/<party>/ssr.yaml; "+ Add row" materializes a
    new party folder (mkdir + auto-own .zddc + ssr.yaml). Renames go
    through X-ZDDC-Op: ssr-rename, which os.Rename's the party
    directory so every row inside follows. Party name doubles as the
    folder name (no opaque IDs) and is path-derived on read.

  - MDL/RSK rollups list every deliverable / every risk across all
    parties with a derived `party` column; "+ Add row" is suppressed
    because party affiliation is ambiguous in the aggregate view.

All four virtual roots are declared `virtual: true` in
defaults.zddc.yaml. Spec/form bytes come from six new embedded
defaults (default-rsk.*, default-ssr.*, default-project-{mdl,rsk}.*)
served via a generalized IsDefaultSpec/IsDefaultSpecAbs that replaces
the MDL-only recognizer. Listing synthesis lives in fs/tree.go;
ACL on each synthetic row evaluates against the canonical
archive/<party>/ chain so non-owners see rows read-only. PUT/DELETE
through virtual URLs rewrite to canonical paths in fileapi.go via
sibling-shape blocks that don't touch the ACL gate. SSR row DELETE
returns 405 (delete the party folder via the archive view).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:47:56 -05:00

281 lines
10 KiB
Go

package zddc
import (
"os"
"path/filepath"
"testing"
)
// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-
// tool rules in defaults.zddc.yaml should resolve correctly for the
// well-known paths without any on-disk .zddc.
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
resetCache()
root := t.TempDir()
cases := []struct {
path string
want string
}{
{filepath.Join(root, "Project-X", "archive"), "archive"},
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), "tables"},
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "classifier"},
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), "archive"},
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "archive"},
{filepath.Join(root, "Project-X", "working"), "browse"},
{filepath.Join(root, "Project-X", "working", "alice@example.com"), "browse"},
{filepath.Join(root, "Project-X", "staging"), "transmittal"},
{filepath.Join(root, "Project-X", "reviewing"), "browse"},
}
for _, tc := range cases {
got := DefaultToolAt(root, tc.path)
if got != tc.want {
t.Errorf("DefaultToolAt(%q) = %q, want %q",
tc.path[len(root):], got, tc.want)
}
}
}
// TestDirToolAt — the trailing-slash form floors at "browse" for
// every path (the embedded convention sets dir_tool nowhere), and an
// on-disk .zddc can override it for a subtree.
func TestDirToolAt(t *testing.T) {
resetCache()
root := t.TempDir()
// Nothing declares dir_tool → browse everywhere, including paths
// whose default_tool (no-slash form) is something else.
for _, p := range []string{
filepath.Join(root, "Project-X"),
filepath.Join(root, "Project-X", "working"),
filepath.Join(root, "Project-X", "archive", "Acme", "mdl"),
filepath.Join(root, "Project-X", "random", "deep", "folder"),
} {
if got := DirToolAt(root, p); got != "browse" {
t.Errorf("DirToolAt(%q) = %q, want browse", p[len(root):], got)
}
}
// Operator override on a subtree; cascades leaf→root.
specialDir := filepath.Join(root, "Special")
if err := os.MkdirAll(specialDir, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, specialDir, "dir_tool: tables\n")
resetCache()
if got := DirToolAt(root, filepath.Join(root, "Special")); got != "tables" {
t.Errorf("DirToolAt(Special) = %q, want tables", got)
}
if got := DirToolAt(root, filepath.Join(root, "Special", "deep")); got != "tables" {
t.Errorf("DirToolAt(Special/deep) = %q, want tables (cascade)", got)
}
if got := DirToolAt(root, filepath.Join(root, "Other")); got != "browse" {
t.Errorf("DirToolAt(Other) = %q, want browse (override scoped to Special)", got)
}
}
// TestCanonicalFolderAt — structural detection of the canonical
// project-layout slots that the browse SPA scope-gates context-menu
// actions against. Top-level <project>/<folder> and second-level
// <project>/archive/<party>/<folder>; everything else returns "".
func TestCanonicalFolderAt(t *testing.T) {
resetCache()
root := t.TempDir()
cases := []struct {
path string
want string
}{
{filepath.Join(root, "Project-X", "archive"), "archive"},
{filepath.Join(root, "Project-X", "working"), "working"},
{filepath.Join(root, "Project-X", "staging"), "staging"},
{filepath.Join(root, "Project-X", "reviewing"), "reviewing"},
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "incoming"},
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), "received"},
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "issued"},
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), "mdl"},
{root, ""},
{filepath.Join(root, "Project-X"), ""},
{filepath.Join(root, "Project-X", "working", "alice@example.com"), ""},
{filepath.Join(root, "Project-X", "archive", "Acme"), ""},
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming", "2026-05-15_Acme-0042 (RFI) - Foundation"), ""},
{filepath.Join(root, "Project-X", "random", "dir"), ""},
}
for _, tc := range cases {
got := CanonicalFolderAt(root, tc.path)
if got != tc.want {
t.Errorf("CanonicalFolderAt(%q) = %q, want %q",
tc.path[len(root):], got, tc.want)
}
}
}
// TestAutoOwnAt_FromEmbeddedConvention — auto_own should be true for
// working/incoming/staging (per the convention) and false elsewhere.
func TestAutoOwnAt_FromEmbeddedConvention(t *testing.T) {
resetCache()
root := t.TempDir()
cases := []struct {
path string
want bool
}{
{filepath.Join(root, "Project-X", "working"), true},
{filepath.Join(root, "Project-X", "working", "alice@example.com"), true},
{filepath.Join(root, "Project-X", "staging"), true},
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), false},
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), false},
}
for _, tc := range cases {
got := AutoOwnAt(root, tc.path)
if got != tc.want {
t.Errorf("AutoOwnAt(%q) = %v, want %v",
tc.path[len(root):], got, tc.want)
}
}
}
// TestVirtualAt_FromEmbeddedConvention — mdl/ is declared virtual;
// everything else (including reviewing/, which is now Plan-Review-
// managed with physical workflow folders) materialises on disk.
func TestVirtualAt_FromEmbeddedConvention(t *testing.T) {
resetCache()
root := t.TempDir()
cases := []struct {
path string
want bool
}{
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), true},
{filepath.Join(root, "Project-X", "reviewing"), false},
{filepath.Join(root, "Project-X", "working"), false},
{filepath.Join(root, "Project-X", "staging"), false},
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), false},
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
}
for _, tc := range cases {
got := VirtualAt(root, tc.path)
if got != tc.want {
t.Errorf("VirtualAt(%q) = %v, want %v",
tc.path[len(root):], got, tc.want)
}
}
}
// TestIsDeclaredPath_FromEmbeddedConvention — canonical paths under
// the convention are declared even on a fresh root; arbitrary paths
// are not.
func TestIsDeclaredPath_FromEmbeddedConvention(t *testing.T) {
resetCache()
root := t.TempDir()
cases := []struct {
path string
want bool
}{
{filepath.Join(root, "Project-X", "archive"), true},
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
{filepath.Join(root, "Project-X", "working"), true},
{filepath.Join(root, "Project-X", "reviewing"), true},
{filepath.Join(root, "Project-X", "junk"), false}, // not in convention
}
for _, tc := range cases {
got := IsDeclaredPath(root, tc.path)
if got != tc.want {
t.Errorf("IsDeclaredPath(%q) = %v, want %v",
tc.path[len(root):], got, tc.want)
}
}
}
// TestChildrenDeclaredAt_FromEmbeddedConvention — at a project
// root, the canonical children should be enumerated: the four
// physical folders (archive, working, staging, reviewing) plus the
// three project-level virtual aggregator slots (ssr, mdl, rsk).
func TestChildrenDeclaredAt_FromEmbeddedConvention(t *testing.T) {
resetCache()
root := t.TempDir()
got := ChildrenDeclaredAt(root, filepath.Join(root, "Project-X"))
want := map[string]bool{
"archive": true, "working": true, "staging": true, "reviewing": true,
"ssr": true, "mdl": true, "rsk": true,
}
if len(got) != len(want) {
t.Errorf("ChildrenDeclaredAt = %v, want exactly %v keys", got, want)
}
for _, n := range got {
if !want[n] {
t.Errorf("unexpected child %q", n)
}
}
}
// TestOperatorOverride_DefaultsAreSurfaceable — operator can override
// any of the canonical tool defaults by mirroring the structure in an
// on-disk .zddc. The override wins.
func TestOperatorOverride_DefaultsAreSurfaceable(t *testing.T) {
resetCache()
root := t.TempDir()
if err := os.MkdirAll(filepath.Join(root, "Special", "working"), 0o755); err != nil {
t.Fatal(err)
}
// Operator declares that Special/working uses classifier
// instead of the embedded-default browse.
writeZddc(t, filepath.Join(root, "Special", "working"),
"default_tool: classifier\n")
if got := DefaultToolAt(root, filepath.Join(root, "Special", "working")); got != "classifier" {
t.Errorf("operator override should set default_tool=classifier, got %q", got)
}
// Default still applies at other projects.
if got := DefaultToolAt(root, filepath.Join(root, "Project-Y", "working")); got != "browse" {
t.Errorf("default convention should hold at unchanged paths, got %q", got)
}
}
// TestDefaultToolAt_PropagatesToDescendants — once an ancestor sets
// default_tool, descendants inherit it unless they override. So a
// path under working/ that isn't explicitly declared in paths: still
// gets browse as its default tool.
func TestDefaultToolAt_PropagatesToDescendants(t *testing.T) {
resetCache()
root := t.TempDir()
// Deep path under working/ — not explicitly mentioned in paths:.
deep := filepath.Join(root, "Project-X", "working", "alice@example.com", "notes", "sub", "deep")
if got := DefaultToolAt(root, deep); got != "browse" {
t.Errorf("DefaultToolAt(%q) = %q, want browse (cascade propagation)",
deep[len(root):], got)
}
}
// TestAutoOwnAt_DescendantCanDisable — explicit auto_own:false at a
// descendant overrides an ancestor's auto_own:true.
func TestAutoOwnAt_DescendantCanDisable(t *testing.T) {
resetCache()
root := t.TempDir()
deepDir := filepath.Join(root, "Project-X", "working", "alice@example.com")
if err := os.MkdirAll(deepDir, 0o755); err != nil {
t.Fatal(err)
}
writeZddc(t, deepDir, "auto_own: false\n")
if got := AutoOwnAt(root, deepDir); got != false {
t.Errorf("AutoOwnAt(%q) = %v, want false (descendant override)", deepDir, got)
}
// Ancestor still has it true.
ancestor := filepath.Join(root, "Project-X", "working")
if got := AutoOwnAt(root, ancestor); got != true {
t.Errorf("AutoOwnAt(%q) = %v, want true (ancestor untouched)", ancestor, got)
}
}
// TestInheritFalse_BlocksEmbeddedDefaults — at the on-disk root,
// inherit:false stops the embedded layer from contributing. The
// canonical paths are then no longer declared.
func TestInheritFalse_BlocksEmbeddedDefaults(t *testing.T) {
resetCache()
root := t.TempDir()
writeZddc(t, root, "inherit: false\n")
// Without the embedded defaults' paths: tree, IsDeclaredPath
// returns false for previously-canonical paths.
if IsDeclaredPath(root, filepath.Join(root, "Project-X", "archive")) {
t.Errorf("with inherit:false at root, archive should not be a declared path")
}
if DefaultToolAt(root, filepath.Join(root, "Project-X", "working")) != "" {
t.Errorf("with inherit:false at root, default_tool should be empty for working")
}
}