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>
439 lines
14 KiB
Go
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)")
|
|
}
|
|
}
|