ZDDC/zddc/internal/zddc/virtualviews_test.go
ZDDC 59b5550872 refactor: nest lifecycle slots per-party + add virtual top-level aggregators
May 2026 reshape. archive/ is now the only physical project-root
directory; working/, staging/, reviewing/ move from the project root
into each archive/<party>/ folder. Six top-level URLs become virtual
aggregators served via the cascade rather than disk:

  ssr/mdl/rsk           tables rollups across parties with a
                        synthesised $party source-party column
  working/staging/      browse folder-nav listings of parties with
  reviewing             non-empty content in the slot; per-party
                        URLs 302-redirect to archive/<party>/<slot>/

Mkdir at the project root is restricted to `archive` and `_`/`.`-
prefixed system names — virtual aggregator names and ad-hoc folders
return 409.

Plan Review hardcodes the scaffold convention (archive/<party>/
{reviewing,staging}/<tracking>/); the pre-reshape
on_plan_review.{reviewing_root,staging_root} cascade keys are dropped.

document_controller is now subtree-admin of every archive/<party>/
(not of project-root working/staging/ as before), so per-party
lifecycle slots inherit admin authority through the cascade.

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

439 lines
14 KiB
Go

package zddc
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestResolveVirtualView_Roots(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
want VirtualViewKind
}{
{"/Project/ssr", VirtualViewSSRRoot},
{"/Project/ssr/", VirtualViewSSRRoot},
{"/Project/mdl", VirtualViewMDLRoot},
{"/Project/mdl/", VirtualViewMDLRoot},
{"/Project/rsk", VirtualViewRSKRoot},
{"/Project/rsk/", VirtualViewRSKRoot},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != tc.want {
t.Errorf("%s: kind=%d resolved=%v; want kind=%d resolved=true", tc.url, got.Kind, got.Resolved, tc.want)
}
if got.Project != "Project" {
t.Errorf("%s: project=%q want Project", tc.url, got.Project)
}
if !got.Kind.IsRootKind() {
t.Errorf("%s: IsRootKind=false", tc.url)
}
}
}
func TestResolveVirtualView_Specs(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
wantKind VirtualViewKind
wantBase string
}{
{"/Project/ssr/table.yaml", VirtualViewSSRSpec, "table.yaml"},
{"/Project/ssr/form.yaml", VirtualViewSSRSpec, "form.yaml"},
{"/Project/mdl/table.yaml", VirtualViewMDLSpec, "table.yaml"},
{"/Project/mdl/form.yaml", VirtualViewMDLSpec, "form.yaml"},
{"/Project/rsk/table.yaml", VirtualViewRSKSpec, "table.yaml"},
{"/Project/rsk/form.yaml", VirtualViewRSKSpec, "form.yaml"},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != tc.wantKind {
t.Errorf("%s: kind=%d resolved=%v; want kind=%d", tc.url, got.Kind, got.Resolved, tc.wantKind)
}
if got.SpecBase != tc.wantBase {
t.Errorf("%s: SpecBase=%q want %q", tc.url, got.SpecBase, tc.wantBase)
}
if !got.Kind.IsSpecKind() {
t.Errorf("%s: IsSpecKind=false", tc.url)
}
}
}
func TestResolveVirtualView_SSRRow(t *testing.T) {
root := t.TempDir()
got := ResolveVirtualView(root, "/Project/ssr/0330C1.yaml")
if !got.Resolved || got.Kind != VirtualViewSSRRow {
t.Fatalf("unexpected resolution: %+v", got)
}
if got.Party != "0330C1" {
t.Errorf("Party=%q want 0330C1", got.Party)
}
wantAbs := filepath.Join(root, "Project", "archive", "0330C1", "ssr.yaml")
if got.CanonicalAbs != wantAbs {
t.Errorf("CanonicalAbs=%q want %q", got.CanonicalAbs, wantAbs)
}
wantSchema := filepath.Join(root, "Project", "archive", "0330C1", "ssr.form.yaml")
if got.SchemaAbs != wantSchema {
t.Errorf("SchemaAbs=%q want %q", got.SchemaAbs, wantSchema)
}
if !got.Kind.IsRowKind() {
t.Errorf("IsRowKind=false")
}
}
func TestResolveVirtualView_RollupRow(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
wantKind VirtualViewKind
wantParty string
wantFilename string
wantSlot string
}{
{"/Project/mdl/0330C1__D-001.yaml", VirtualViewMDLRow, "0330C1", "D-001.yaml", "mdl"},
{"/Project/rsk/Acme__R-005.yaml", VirtualViewRSKRow, "Acme", "R-005.yaml", "rsk"},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != tc.wantKind {
t.Errorf("%s: kind=%d resolved=%v; want kind=%d", tc.url, got.Kind, got.Resolved, tc.wantKind)
continue
}
if got.Party != tc.wantParty {
t.Errorf("%s: Party=%q want %q", tc.url, got.Party, tc.wantParty)
}
if got.RowFilename != tc.wantFilename {
t.Errorf("%s: RowFilename=%q want %q", tc.url, got.RowFilename, tc.wantFilename)
}
wantAbs := filepath.Join(root, "Project", "archive", tc.wantParty, tc.wantSlot, tc.wantFilename)
if got.CanonicalAbs != wantAbs {
t.Errorf("%s: CanonicalAbs=%q want %q", tc.url, got.CanonicalAbs, wantAbs)
}
}
}
func TestResolveVirtualView_NonMatches(t *testing.T) {
root := t.TempDir()
cases := []string{
"/",
"/Project",
"/Project/",
"/Project/archive/Acme/mdl",
"/Project/ssr/invalid__name__double.yaml", // double-double underscore is rejected
"/Project/mdl/__leading.yaml", // empty party
"/Project/mdl/party__.yaml", // empty rowBase
"/Project/ssr/.hidden.yaml", // dotfile party name
"/Project/ssr/0330C1.yaml/sub", // sub-path under row file
"/Project/notaslot/table.yaml",
}
for _, url := range cases {
got := ResolveVirtualView(root, url)
if got.Resolved {
t.Errorf("%s: unexpectedly resolved as kind %d", url, got.Kind)
}
}
}
// TestResolveVirtualView_FolderNavRoot — the project-level virtual
// folder-nav aggregators resolve to VirtualViewFolderNavRoot for the
// bare slot URL (trailing slash optional).
func TestResolveVirtualView_FolderNavRoot(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
slot string
}{
{"/Project/working", "working"},
{"/Project/working/", "working"},
{"/Project/staging", "staging"},
{"/Project/staging/", "staging"},
{"/Project/reviewing", "reviewing"},
{"/Project/reviewing/", "reviewing"},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != VirtualViewFolderNavRoot {
t.Errorf("%s: kind=%d resolved=%v; want FolderNavRoot resolved=true", tc.url, got.Kind, got.Resolved)
}
if got.Slot != tc.slot {
t.Errorf("%s: Slot=%q want %q", tc.url, got.Slot, tc.slot)
}
if !got.Kind.IsRootKind() {
t.Errorf("%s: IsRootKind=false", tc.url)
}
if !got.Kind.IsFolderNavKind() {
t.Errorf("%s: IsFolderNavKind=false", tc.url)
}
}
}
// TestResolveVirtualView_FolderNavRedir — URLs deeper than the bare
// slot resolve to VirtualViewFolderNavRedir with Party + RedirRest
// populated; the dispatcher 302s these to the canonical
// archive/<party>/<slot>/<rest> path.
func TestResolveVirtualView_FolderNavRedir(t *testing.T) {
root := t.TempDir()
cases := []struct {
url string
wantParty string
wantRedirRest string
wantCanonical string
}{
{"/Project/working/Acme", "Acme", "", "/Project/archive/Acme/working/"},
{"/Project/working/Acme/", "Acme", "", "/Project/archive/Acme/working/"},
{"/Project/staging/Acme/2026-05-15_X (RFI) - T", "Acme", "2026-05-15_X (RFI) - T", "/Project/archive/Acme/staging/2026-05-15_X (RFI) - T"},
// Trailing slash is stripped at resolver entry; the dispatcher
// re-appends it before issuing the 302 to match the request shape.
{"/Project/reviewing/Acme/T-0042/", "Acme", "T-0042", "/Project/archive/Acme/reviewing/T-0042"},
}
for _, tc := range cases {
got := ResolveVirtualView(root, tc.url)
if !got.Resolved || got.Kind != VirtualViewFolderNavRedir {
t.Errorf("%s: kind=%d resolved=%v; want FolderNavRedir resolved=true", tc.url, got.Kind, got.Resolved)
continue
}
if got.Party != tc.wantParty {
t.Errorf("%s: Party=%q want %q", tc.url, got.Party, tc.wantParty)
}
if got.RedirRest != tc.wantRedirRest {
t.Errorf("%s: RedirRest=%q want %q", tc.url, got.RedirRest, tc.wantRedirRest)
}
if got.CanonicalURL != tc.wantCanonical {
t.Errorf("%s: CanonicalURL=%q want %q", tc.url, got.CanonicalURL, tc.wantCanonical)
}
if !got.Kind.IsFolderNavKind() {
t.Errorf("%s: IsFolderNavKind=false", tc.url)
}
}
}
// TestListPartyDirsInSlot — folder-nav listings include only parties
// whose archive/<party>/<slot>/ directory exists AND has non-empty
// content (the in-flight filter). Parties with an empty or absent
// slot directory are suppressed.
func TestListPartyDirsInSlot(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
// Acme has working content; Beta has only a .zddc system file
// (counts as empty); Gamma has the slot directory but it's
// completely empty; Delta doesn't have the slot at all.
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", "Acme", "working"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "Acme", "working", "draft.md"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", "Beta", "working"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "Beta", "working", ".zddc"), []byte(""), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", "Gamma", "working"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", "Delta"), 0o755); err != nil {
t.Fatal(err)
}
got, err := ListPartyDirsInSlot(root, projectAbs, "working")
if err != nil {
t.Fatal(err)
}
want := []string{"Acme"}
if strings.Join(got, ",") != strings.Join(want, ",") {
t.Errorf("ListPartyDirsInSlot(working) = %v, want %v", got, want)
}
}
// TestListPartyDirsInSlot_BadSlot — only the three folder-nav slots
// are valid.
func TestListPartyDirsInSlot_BadSlot(t *testing.T) {
root := t.TempDir()
for _, bad := range []string{"ssr", "mdl", "rsk", "received", "issued", "incoming", ""} {
if _, err := ListPartyDirsInSlot(root, root, bad); err == nil {
t.Errorf("expected error for slot=%q (only working/staging/reviewing valid)", bad)
}
}
}
// TestIsPlanReviewURL — the eligibility test surfaces the X-ZDDC-On-
// Plan-Review header. Matches /<project>/archive/<party>/received/
// <tracking>/ with or without trailing slash; everything else returns
// false.
func TestIsPlanReviewURL(t *testing.T) {
cases := []struct {
url string
want bool
}{
{"/Project/archive/Acme/received/Acme-0042", true},
{"/Project/archive/Acme/received/Acme-0042/", true},
{"/Project/archive/Acme/received", false},
{"/Project/archive/Acme/received/", false},
{"/Project/archive/Acme/received/Acme-0042/file.pdf", false},
{"/Project/archive/Acme/issued/Acme-0042/", false},
{"/Project/archive/Acme", false},
{"/Project/archive", false},
{"/Project", false},
{"/", false},
{"", false},
}
for _, tc := range cases {
if got := IsPlanReviewURL(tc.url); got != tc.want {
t.Errorf("IsPlanReviewURL(%q) = %v, want %v", tc.url, got, tc.want)
}
}
}
func TestIsSSRCreateURL(t *testing.T) {
cases := []struct {
url string
want string
wantOK bool
}{
{"/Project/ssr/form.html", "Project", true},
{"/Other-Project/ssr/form.html", "Other-Project", true},
{"/Project/ssr/", "", false},
{"/Project/ssr/Acme.yaml.html", "", false},
{"/Project/mdl/form.html", "", false},
{"/.hidden/ssr/form.html", "", false},
}
for _, tc := range cases {
got, ok := IsSSRCreateURL(tc.url)
if ok != tc.wantOK {
t.Errorf("%s: ok=%v want %v", tc.url, ok, tc.wantOK)
}
if got != tc.want {
t.Errorf("%s: project=%q want %q", tc.url, got, tc.want)
}
}
}
func TestStripYAMLHTML(t *testing.T) {
cases := []struct {
in string
want string
wantOK bool
}{
{"/Project/ssr/Acme.yaml.html", "/Project/ssr/Acme.yaml", true},
{"/Project/mdl/foo__bar.yaml.html", "/Project/mdl/foo__bar.yaml", true},
{"/Project/ssr/Acme.yaml", "/Project/ssr/Acme.yaml", false},
{"/Project/ssr/form.html", "/Project/ssr/form.html", false},
}
for _, tc := range cases {
got, ok := StripYAMLHTML(tc.in)
if got != tc.want || ok != tc.wantOK {
t.Errorf("%s: (%q, %v) want (%q, %v)", tc.in, got, ok, tc.want, tc.wantOK)
}
}
}
func TestValidPartyName(t *testing.T) {
ok := []string{"0330C1", "Acme", "Acme.Inc", "Acme-Subsidiary", "a", "0", "0330C1.draft", "X-Y-Z"}
bad := []string{"", ".hidden", "_underscore", "Acme/sub", "Acme Inc", "Acme(Inc)", "Acme,Inc", "..", "-leading-dash"}
for _, s := range ok {
if !ValidPartyName(s) {
t.Errorf("ValidPartyName(%q) = false, want true", s)
}
}
for _, s := range bad {
if ValidPartyName(s) {
t.Errorf("ValidPartyName(%q) = true, want false", s)
}
}
}
func TestListSSRParties(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
for _, party := range []string{"0330C1", "0440P2", "Acme"} {
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", party), 0o755); err != nil {
t.Fatal(err)
}
}
// A file (not a dir) and a hidden folder should be filtered out.
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "stray.txt"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(filepath.Join(projectAbs, "archive", ".hidden"), 0o755); err != nil {
t.Fatal(err)
}
parties, err := ListSSRParties(root, projectAbs)
if err != nil {
t.Fatal(err)
}
want := []string{"0330C1", "0440P2", "Acme"}
if strings.Join(parties, ",") != strings.Join(want, ",") {
t.Errorf("got %v, want %v", parties, want)
}
}
func TestListSSRParties_NoArchive(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
parties, err := ListSSRParties(root, projectAbs)
if err != nil {
t.Fatalf("err=%v want nil", err)
}
if len(parties) != 0 {
t.Errorf("got %v, want empty", parties)
}
}
func TestListRollupRows(t *testing.T) {
root := t.TempDir()
projectAbs := filepath.Join(root, "Project")
for _, party := range []string{"0330C1", "0440P2"} {
mdlDir := filepath.Join(projectAbs, "archive", party, "mdl")
if err := os.MkdirAll(mdlDir, 0o755); err != nil {
t.Fatal(err)
}
}
// Real rows.
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "D-001.yaml"), []byte("id: D-001\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "D-002.yaml"), []byte("id: D-002\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0440P2", "mdl", "D-010.yaml"), []byte("id: D-010\n"), 0o644); err != nil {
t.Fatal(err)
}
// Skipped: table.yaml, form.yaml, anything containing "__".
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "table.yaml"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "form.yaml"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(projectAbs, "archive", "0330C1", "mdl", "weird__name.yaml"), []byte("x"), 0o644); err != nil {
t.Fatal(err)
}
rows, err := ListRollupRows(root, projectAbs, "mdl")
if err != nil {
t.Fatal(err)
}
if len(rows) != 3 {
t.Fatalf("got %d rows, want 3; rows=%+v", len(rows), rows)
}
wantNames := []string{"0330C1__D-001.yaml", "0330C1__D-002.yaml", "0440P2__D-010.yaml"}
for i, want := range wantNames {
if rows[i].SyntheticName != want {
t.Errorf("row[%d].SyntheticName=%q want %q", i, rows[i].SyntheticName, want)
}
}
}
func TestListRollupRows_BadSlot(t *testing.T) {
root := t.TempDir()
if _, err := ListRollupRows(root, root, "ssr"); err == nil {
t.Error("expected error for slot=ssr (only mdl/rsk valid)")
}
}