test(zddc): update internal/zddc suite for flat-peer layout
Repoint default-tool/history/canonical-folder/auto-own/virtual/declared, role-grant, and WORM-zone expectations to the top-level peer layout: archive is now blanket-WORM (DC = rc there), the workspace/register peers carry the DC grants directly, and incoming/working/staging/reviewing/mdl/rsk/ssr are physical peers. ensure_test repointed to top-level paths + the virtual-reject test inverted (peers are physical now). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
db110665f0
commit
7d462ab7a4
4 changed files with 209 additions and 447 deletions
|
|
@ -9,11 +9,10 @@ import (
|
||||||
|
|
||||||
func TestEnsureCanonicalAncestors_LazyCreation(t *testing.T) {
|
func TestEnsureCanonicalAncestors_LazyCreation(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
// Per-user homes now live under archive/<party>/working/<email>/
|
// working/ is a top-level peer; its <party> folder auto-owns the
|
||||||
// after the top-of-project reshape. The depth-3 working slot is
|
// creator (unfenced — party admins still cascade in). Per-user email
|
||||||
// the canonical-folder position; its auto-own .zddc is unfenced
|
// homes were abandoned in the reshape.
|
||||||
// and the depth-4 per-user home gets the fenced one.
|
target := filepath.Join(root, "Proj", "working", "ACME", "notes.md")
|
||||||
target := filepath.Join(root, "Proj", "archive", "ACME", "working", "alice@x.com", "notes.md")
|
|
||||||
|
|
||||||
resolved, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
|
resolved, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -23,13 +22,10 @@ func TestEnsureCanonicalAncestors_LazyCreation(t *testing.T) {
|
||||||
t.Errorf("resolved=%q, target=%q (no case variant exists, should be identical)", resolved, target)
|
t.Errorf("resolved=%q, target=%q (no case variant exists, should be identical)", resolved, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
// working/ is now created with auto-own .zddc (unfenced — party
|
autoZ := filepath.Join(root, "Proj", "working", "ACME", ".zddc")
|
||||||
// admins still cascade through, only the per-user home below is
|
|
||||||
// fenced).
|
|
||||||
autoZ := filepath.Join(root, "Proj", "archive", "ACME", "working", ".zddc")
|
|
||||||
data, err := os.ReadFile(autoZ)
|
data, err := os.ReadFile(autoZ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("auto-own .zddc not written at working/: %v", err)
|
t.Fatalf("auto-own .zddc not written at working/ACME/: %v", err)
|
||||||
}
|
}
|
||||||
body := string(data)
|
body := string(data)
|
||||||
if !strings.Contains(body, "alice@x.com: rwcda") {
|
if !strings.Contains(body, "alice@x.com: rwcda") {
|
||||||
|
|
@ -39,127 +35,74 @@ func TestEnsureCanonicalAncestors_LazyCreation(t *testing.T) {
|
||||||
t.Errorf("created_by missing: %s", body)
|
t.Errorf("created_by missing: %s", body)
|
||||||
}
|
}
|
||||||
if strings.Contains(body, "inherit: false") {
|
if strings.Contains(body, "inherit: false") {
|
||||||
t.Errorf("party working/ .zddc should be UNFENCED so party admins still reach inside; got: %s", body)
|
t.Errorf("working/<party>/ .zddc should be UNFENCED; got: %s", body)
|
||||||
}
|
}
|
||||||
|
// The working/ peer root itself does NOT auto-own (auto_own is at the
|
||||||
// alice@x.com/ subfolder gets a FENCED auto-own .zddc — private by
|
// <party> level).
|
||||||
// default so other users can't read alice's drafts via ancestor
|
if _, err := os.Stat(filepath.Join(root, "Proj", "working", ".zddc")); !os.IsNotExist(err) {
|
||||||
// cascade. alice can edit the file later to add collaborators.
|
t.Errorf("working/ peer root should not have auto-own .zddc; got err=%v", err)
|
||||||
homeZddc := filepath.Join(root, "Proj", "archive", "ACME", "working", "alice@x.com", ".zddc")
|
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "working", "alice@x.com")); err != nil {
|
|
||||||
t.Errorf("subfolder not created: %v", err)
|
|
||||||
}
|
|
||||||
homeData, err := os.ReadFile(homeZddc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("per-user-home auto-own .zddc not written: %v", err)
|
|
||||||
}
|
|
||||||
homeBody := string(homeData)
|
|
||||||
if !strings.Contains(homeBody, "alice@x.com: rwcda") {
|
|
||||||
t.Errorf("per-user-home grant missing: %s", homeBody)
|
|
||||||
}
|
|
||||||
if !strings.Contains(homeBody, "inherit: false") {
|
|
||||||
t.Errorf("per-user-home .zddc should have inherit: false; got: %s", homeBody)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// staging/<folder>/ is NOT fenced — staging is a shared lane (transmittal
|
// staging/<party>/<folder>/ is NOT auto-owned — only the <party> level is.
|
||||||
// folders are date+tracking-named, not per-user). Only per-user homes
|
|
||||||
// under working/ get the fence.
|
|
||||||
func TestEnsureCanonicalAncestors_StagingChildNotFenced(t *testing.T) {
|
func TestEnsureCanonicalAncestors_StagingChildNotFenced(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
target := filepath.Join(root, "Proj", "archive", "ACME", "staging",
|
target := filepath.Join(root, "Proj", "staging", "ACME",
|
||||||
"2025-10-31_proj-EM-TRN-0042 (RSA) - Outbound", "doc.pdf")
|
"2025-10-31_proj-EM-TRN-0042 (RSA) - Outbound", "doc.pdf")
|
||||||
if _, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755); err != nil {
|
if _, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755); err != nil {
|
||||||
t.Fatalf("ensure: %v", err)
|
t.Fatalf("ensure: %v", err)
|
||||||
}
|
}
|
||||||
// staging/<folder>/.zddc should not exist (only the parent staging/
|
childZddc := filepath.Join(root, "Proj", "staging", "ACME",
|
||||||
// gets an auto-own; the date-named child is plain).
|
|
||||||
childZddc := filepath.Join(root, "Proj", "archive", "ACME", "staging",
|
|
||||||
"2025-10-31_proj-EM-TRN-0042 (RSA) - Outbound", ".zddc")
|
"2025-10-31_proj-EM-TRN-0042 (RSA) - Outbound", ".zddc")
|
||||||
if _, err := os.Stat(childZddc); !os.IsNotExist(err) {
|
if _, err := os.Stat(childZddc); !os.IsNotExist(err) {
|
||||||
t.Errorf("staging child should NOT have auto-own .zddc; got err=%v", err)
|
t.Errorf("staging child should NOT have auto-own .zddc; got err=%v", err)
|
||||||
}
|
}
|
||||||
// And the staging/ slot itself gets the unfenced auto-own.
|
stagingZddc := filepath.Join(root, "Proj", "staging", "ACME", ".zddc")
|
||||||
stagingZddc := filepath.Join(root, "Proj", "archive", "ACME", "staging", ".zddc")
|
|
||||||
if _, err := os.Stat(stagingZddc); err != nil {
|
if _, err := os.Stat(stagingZddc); err != nil {
|
||||||
t.Errorf("party staging/ auto-own .zddc missing: %v", err)
|
t.Errorf("staging/<party> auto-own .zddc missing: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureCanonicalAncestors_CaseFoldReuse(t *testing.T) {
|
func TestEnsureCanonicalAncestors_CaseFoldReuse(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
// Pre-create Archive/ (PascalCase) — case-fold reuse applies to
|
// Pre-create Archive/ (PascalCase) — case-fold reuse applies to the
|
||||||
// the canonical project-root slot.
|
// canonical project-root peer + the received/issued slots.
|
||||||
if err := os.MkdirAll(filepath.Join(root, "Proj", "Archive", "ACME", "working"), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Join(root, "Proj", "Archive", "ACME", "received"), 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
target := filepath.Join(root, "Proj", "archive", "ACME", "working", "foo.md")
|
target := filepath.Join(root, "Proj", "archive", "ACME", "received", "foo.pdf")
|
||||||
resolved, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
|
resolved, err := EnsureCanonicalAncestors(root, target, "dc@x.com", 0o755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ensure: %v", err)
|
t.Fatalf("ensure: %v", err)
|
||||||
}
|
}
|
||||||
|
want := filepath.Join(root, "Proj", "Archive", "ACME", "received", "foo.pdf")
|
||||||
// Resolved path uses on-disk Archive/ casing.
|
|
||||||
want := filepath.Join(root, "Proj", "Archive", "ACME", "working", "foo.md")
|
|
||||||
if resolved != want {
|
if resolved != want {
|
||||||
t.Errorf("resolved=%q, want %q", resolved, want)
|
t.Errorf("resolved=%q, want %q", resolved, want)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No new lowercase archive/ sibling.
|
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(root, "Proj", "archive")); !os.IsNotExist(err) {
|
||||||
t.Errorf("lowercase sibling should not exist; got err=%v", err)
|
t.Errorf("lowercase sibling should not exist; got err=%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive/ already existed — no auto-own .zddc was retroactively written.
|
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "Archive", ".zddc")); !os.IsNotExist(err) {
|
|
||||||
t.Errorf("auto-own .zddc should not be written to a pre-existing folder; got err=%v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureCanonicalAncestors_PerPartyIncoming(t *testing.T) {
|
func TestEnsureCanonicalAncestors_PerPartyIncoming(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
target := filepath.Join(root, "Proj", "archive", "ACME", "incoming", "submission.pdf")
|
// incoming/ is a top-level peer; its <party> folder auto-owns.
|
||||||
|
target := filepath.Join(root, "Proj", "incoming", "ACME", "submission.pdf")
|
||||||
|
|
||||||
_, err := EnsureCanonicalAncestors(root, target, "rep@acme.com", 0o755)
|
if _, err := EnsureCanonicalAncestors(root, target, "rep@acme.com", 0o755); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ensure: %v", err)
|
t.Fatalf("ensure: %v", err)
|
||||||
}
|
}
|
||||||
|
autoZ := filepath.Join(root, "Proj", "incoming", "ACME", ".zddc")
|
||||||
// archive/ created (no auto-own — archive/ itself is a plain
|
|
||||||
// container; the cascade declares no auto_own there).
|
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive")); err != nil {
|
|
||||||
t.Errorf("archive/ not created: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", ".zddc")); !os.IsNotExist(err) {
|
|
||||||
t.Errorf("archive/ should not have auto-own .zddc; got err=%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// archive/ACME/ created WITH auto-own (the cascade declares
|
|
||||||
// auto_own on the party-folder level so whoever creates a party
|
|
||||||
// subtree owns it — used by the document controller to set up a
|
|
||||||
// new counterparty's .zddc). Unfenced, so ancestor grants still
|
|
||||||
// reach inside (project_team:r through to received/issued).
|
|
||||||
partyZ := filepath.Join(root, "Proj", "archive", "ACME", ".zddc")
|
|
||||||
pdata, err := os.ReadFile(partyZ)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("auto-own .zddc at ACME/ not written: %v", err)
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(pdata), "rep@acme.com: rwcda") {
|
|
||||||
t.Errorf("ACME/ auto-own missing rep grant: %s", pdata)
|
|
||||||
}
|
|
||||||
if strings.Contains(string(pdata), "inherit: false") {
|
|
||||||
t.Errorf("ACME/ auto-own should be UNFENCED; got: %s", pdata)
|
|
||||||
}
|
|
||||||
|
|
||||||
// archive/ACME/incoming/ created WITH auto-own.
|
|
||||||
autoZ := filepath.Join(root, "Proj", "archive", "ACME", "incoming", ".zddc")
|
|
||||||
data, err := os.ReadFile(autoZ)
|
data, err := os.ReadFile(autoZ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("auto-own .zddc at incoming/ not written: %v", err)
|
t.Fatalf("auto-own .zddc at incoming/ACME/ not written: %v", err)
|
||||||
}
|
}
|
||||||
if !strings.Contains(string(data), "rep@acme.com: rwcda") {
|
if !strings.Contains(string(data), "rep@acme.com: rwcda") {
|
||||||
t.Errorf("incoming/ auto-own missing rep grant: %s", data)
|
t.Errorf("incoming/ACME auto-own missing rep grant: %s", data)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(data), "inherit: false") {
|
||||||
|
t.Errorf("incoming/ACME auto-own should be UNFENCED; got: %s", data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,49 +110,47 @@ func TestEnsureCanonicalAncestors_WormFoldersNoAutoOwn(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
target := filepath.Join(root, "Proj", "archive", "ACME", "issued", "spec.pdf")
|
target := filepath.Join(root, "Proj", "archive", "ACME", "issued", "spec.pdf")
|
||||||
|
|
||||||
_, err := EnsureCanonicalAncestors(root, target, "dc@mycompany.com", 0o755)
|
if _, err := EnsureCanonicalAncestors(root, target, "dc@mycompany.com", 0o755); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ensure: %v", err)
|
t.Fatalf("ensure: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "issued")); err != nil {
|
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "issued")); err != nil {
|
||||||
t.Errorf("issued/ not created: %v", err)
|
t.Errorf("issued/ not created: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "issued", ".zddc")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "issued", ".zddc")); !os.IsNotExist(err) {
|
||||||
t.Errorf("issued/ should NOT have auto-own .zddc (WORM); got err=%v", err)
|
t.Errorf("issued/ should NOT have auto-own .zddc (WORM); got err=%v", err)
|
||||||
}
|
}
|
||||||
|
// archive/ and archive/<party>/ are plain containers — no auto-own.
|
||||||
|
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", ".zddc")); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("archive/ should not have auto-own .zddc; got err=%v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnsureCanonicalAncestors_NoPrincipalSkipsAutoOwn(t *testing.T) {
|
func TestEnsureCanonicalAncestors_NoPrincipalSkipsAutoOwn(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
target := filepath.Join(root, "Proj", "archive", "ACME", "working", "anon.md")
|
target := filepath.Join(root, "Proj", "working", "ACME", "anon.md")
|
||||||
|
|
||||||
_, err := EnsureCanonicalAncestors(root, target, "" /* no email */, 0o755)
|
if _, err := EnsureCanonicalAncestors(root, target, "" /* no email */, 0o755); err != nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("ensure: %v", err)
|
t.Fatalf("ensure: %v", err)
|
||||||
}
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "ACME")); err != nil {
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "working")); err != nil {
|
t.Errorf("working/ACME not created: %v", err)
|
||||||
t.Errorf("working/ not created: %v", err)
|
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "working", ".zddc")); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(root, "Proj", "working", "ACME", ".zddc")); !os.IsNotExist(err) {
|
||||||
t.Errorf("auto-own .zddc must not be written without principalEmail; got err=%v", err)
|
t.Errorf("auto-own .zddc must not be written without principalEmail; got err=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project-root virtual aggregator names are rejected — a write
|
// Top-level peers are physical now — a write under <project>/<peer>/<party>/
|
||||||
// targeting <project>/working/<...> bypasses the virtual resolver
|
// is created normally (no virtual-aggregator rejection).
|
||||||
// and must not materialise on disk.
|
func TestEnsureCanonicalAncestors_TopLevelPeersCreated(t *testing.T) {
|
||||||
func TestEnsureCanonicalAncestors_RejectsProjectRootVirtual(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
for _, slot := range []string{"working", "staging", "reviewing", "ssr", "mdl", "rsk"} {
|
for _, peer := range []string{"working", "staging", "reviewing", "incoming", "mdl", "rsk", "ssr"} {
|
||||||
target := filepath.Join(root, "Proj", slot, "x.md")
|
target := filepath.Join(root, "Proj", peer, "ACME", "x.md")
|
||||||
_, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
|
if _, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755); err != nil {
|
||||||
if err == nil {
|
t.Errorf("%s: unexpected error: %v", peer, err)
|
||||||
t.Errorf("%s: expected error for write under <project>/%s/, got nil", slot, slot)
|
|
||||||
}
|
}
|
||||||
if _, err := os.Stat(filepath.Join(root, "Proj", slot)); !os.IsNotExist(err) {
|
if _, err := os.Stat(filepath.Join(root, "Proj", peer, "ACME")); err != nil {
|
||||||
t.Errorf("%s: <project>/%s/ must NOT be created on disk; got err=%v", slot, slot, err)
|
t.Errorf("%s: <project>/%s/ACME/ should be created; got err=%v", peer, peer, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -218,8 +159,7 @@ func TestEnsureCanonicalAncestors_RejectsTraversal(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
other := t.TempDir()
|
other := t.TempDir()
|
||||||
target := filepath.Join(other, "evil.md")
|
target := filepath.Join(other, "evil.md")
|
||||||
_, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
|
if _, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755); err == nil {
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error for target outside fsRoot")
|
t.Errorf("expected error for target outside fsRoot")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,81 +6,69 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-
|
// TestDefaultToolAt_FromEmbeddedConvention — the canonical default-tool
|
||||||
// tool rules in defaults.zddc.yaml should resolve correctly for the
|
// rules in defaults.zddc.yaml resolve correctly for the flat top-level
|
||||||
// well-known paths without any on-disk .zddc.
|
// peers (and their per-party subdirs) without any on-disk .zddc.
|
||||||
//
|
|
||||||
// Layout reshape: lifecycle slots (working/staging/reviewing) now
|
|
||||||
// live under archive/<party>/. The project-level
|
|
||||||
// <project>/{working,staging,reviewing} URLs are virtual folder-nav
|
|
||||||
// aggregators (default_tool=browse).
|
|
||||||
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
|
func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{filepath.Join(root, "Project-X", "archive"), "archive"},
|
// The committed record (archive subtree → archive tool).
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), "tables"},
|
{j("archive"), "archive"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "rsk"), "tables"},
|
{j("archive", "Acme"), "archive"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "classifier"},
|
{j("archive", "Acme", "received"), "archive"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), "archive"},
|
{j("archive", "Acme", "issued"), "archive"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "archive"},
|
// Top-level peers.
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working"), "browse"},
|
{j("ssr"), "tables"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working", "alice@example.com"), "browse"},
|
{j("mdl"), "tables"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "staging"), "transmittal"},
|
{j("rsk"), "tables"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "reviewing"), "browse"},
|
{j("incoming"), "classifier"},
|
||||||
// Project-level virtual aggregators.
|
{j("working"), "browse"},
|
||||||
{filepath.Join(root, "Project-X", "ssr"), "tables"},
|
{j("staging"), "transmittal"},
|
||||||
{filepath.Join(root, "Project-X", "mdl"), "tables"},
|
{j("reviewing"), "browse"},
|
||||||
{filepath.Join(root, "Project-X", "rsk"), "tables"},
|
// Per-party subdirs inherit the peer's default_tool.
|
||||||
{filepath.Join(root, "Project-X", "working"), "browse"},
|
{j("mdl", "Acme"), "tables"},
|
||||||
{filepath.Join(root, "Project-X", "staging"), "browse"},
|
{j("rsk", "Acme"), "tables"},
|
||||||
{filepath.Join(root, "Project-X", "reviewing"), "browse"},
|
{j("incoming", "Acme"), "classifier"},
|
||||||
|
{j("working", "Acme"), "browse"},
|
||||||
|
{j("staging", "Acme"), "transmittal"},
|
||||||
|
{j("reviewing", "Acme"), "browse"},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got := DefaultToolAt(root, tc.path)
|
if got := DefaultToolAt(root, tc.path); got != tc.want {
|
||||||
if got != tc.want {
|
t.Errorf("DefaultToolAt(%q) = %q, want %q", tc.path[len(root):], got, tc.want)
|
||||||
t.Errorf("DefaultToolAt(%q) = %q, want %q",
|
|
||||||
tc.path[len(root):], got, tc.want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHistoryAt_Defaults — the embedded convention enables edit-history
|
// TestHistoryAt_Defaults — edit-history defaults on for the live-editing
|
||||||
// versioning on the per-party archive/<party>/working/ subtree (+ its
|
// peers working/mdl/rsk and the ssr registry (subtree-inheriting). The
|
||||||
// homes). History is subtree-inheriting and ignores the homes'
|
// other peers and the WORM archive do not get history.
|
||||||
// inherit:false fences. The project-level working/ aggregator is virtual
|
|
||||||
// (no direct content → no history), and sibling slots (staging,
|
|
||||||
// reviewing, mdl, incoming, received) do NOT get history.
|
|
||||||
func TestHistoryAt_Defaults(t *testing.T) {
|
func TestHistoryAt_Defaults(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
// Edit-history defaults on for the three live-editing slots:
|
{j("working"), true},
|
||||||
// working, mdl, rsk — at both the project-level virtual nodes and
|
{j("working", "Acme"), true},
|
||||||
// the per-party folders (subtree-inheriting).
|
{j("working", "Acme", "notes"), true},
|
||||||
{filepath.Join(root, "Project-X", "working"), true},
|
{j("mdl"), true},
|
||||||
{filepath.Join(root, "Project-X", "working", "alice@example.com"), true},
|
{j("mdl", "Acme"), true},
|
||||||
{filepath.Join(root, "Project-X", "mdl"), true},
|
{j("rsk"), true},
|
||||||
{filepath.Join(root, "Project-X", "rsk"), true},
|
{j("ssr"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working"), true},
|
// No history elsewhere.
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working", "alice@example.com"), true},
|
{j("staging"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working", "alice@example.com", "notes"), true},
|
{j("reviewing"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), true},
|
{j("incoming"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "rsk"), true},
|
{j("archive"), false},
|
||||||
// Other slots get no history.
|
{j("archive", "Acme", "received"), false},
|
||||||
{filepath.Join(root, "Project-X", "staging"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "reviewing"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "ssr"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "staging"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive"), false},
|
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
if got := HistoryAt(root, tc.path); got != tc.want {
|
if got := HistoryAt(root, tc.path); got != tc.want {
|
||||||
|
|
@ -89,25 +77,22 @@ func TestHistoryAt_Defaults(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDirToolAt — the trailing-slash form floors at "browse" for
|
// TestDirToolAt — the trailing-slash form floors at "browse" for every
|
||||||
// every path (the embedded convention sets dir_tool nowhere), and an
|
// path (the embedded convention sets dir_tool nowhere), and an on-disk
|
||||||
// on-disk .zddc can override it for a subtree.
|
// .zddc can override it for a subtree.
|
||||||
func TestDirToolAt(t *testing.T) {
|
func TestDirToolAt(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
// Nothing declares dir_tool → browse everywhere, including paths
|
|
||||||
// whose default_tool (no-slash form) is something else.
|
|
||||||
for _, p := range []string{
|
for _, p := range []string{
|
||||||
filepath.Join(root, "Project-X"),
|
filepath.Join(root, "Project-X"),
|
||||||
filepath.Join(root, "Project-X", "archive", "Acme", "working"),
|
filepath.Join(root, "Project-X", "working", "Acme"),
|
||||||
filepath.Join(root, "Project-X", "archive", "Acme", "mdl"),
|
filepath.Join(root, "Project-X", "mdl", "Acme"),
|
||||||
filepath.Join(root, "Project-X", "random", "deep", "folder"),
|
filepath.Join(root, "Project-X", "random", "deep", "folder"),
|
||||||
} {
|
} {
|
||||||
if got := DirToolAt(root, p); got != "browse" {
|
if got := DirToolAt(root, p); got != "browse" {
|
||||||
t.Errorf("DirToolAt(%q) = %q, want browse", p[len(root):], got)
|
t.Errorf("DirToolAt(%q) = %q, want browse", p[len(root):], got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Operator override on a subtree; cascades leaf→root.
|
|
||||||
specialDir := filepath.Join(root, "Special")
|
specialDir := filepath.Join(root, "Special")
|
||||||
if err := os.MkdirAll(specialDir, 0o755); err != nil {
|
if err := os.MkdirAll(specialDir, 0o755); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
@ -125,237 +110,116 @@ func TestDirToolAt(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCanonicalFolderAt — structural detection of the canonical
|
// TestCanonicalFolderAt — structural detection of the canonical slots the
|
||||||
// project-layout slots that the browse SPA scope-gates context-menu
|
// browse SPA scope-gates context-menu actions against, in the flat-peer
|
||||||
// actions against.
|
// layout: depth-2 peers, depth-3 <peer>/<party> (workspace/register peers
|
||||||
//
|
// report their peer), and depth-4 archive/<party>/{received,issued}.
|
||||||
// After the layout reshape:
|
|
||||||
// - <project>/archive is the only depth-2 canonical
|
|
||||||
// - <project>/archive/<party>/<slot> covers the eight per-party
|
|
||||||
// physical slots (incoming, received, issued, mdl, rsk, working,
|
|
||||||
// staging, reviewing)
|
|
||||||
// - everything else returns ""
|
|
||||||
func TestCanonicalFolderAt(t *testing.T) {
|
func TestCanonicalFolderAt(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
{filepath.Join(root, "Project-X", "archive"), "archive"},
|
{j("archive"), "archive"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "incoming"},
|
{j("ssr"), "ssr"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), "received"},
|
{j("mdl"), "mdl"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "issued"},
|
{j("rsk"), "rsk"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), "mdl"},
|
{j("incoming"), "incoming"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "rsk"), "rsk"},
|
{j("working"), "working"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working"), "working"},
|
{j("staging"), "staging"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "staging"), "staging"},
|
{j("reviewing"), "reviewing"},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "reviewing"), "reviewing"},
|
// <peer>/<party> reports the peer (not archive).
|
||||||
// Project-root virtuals are NOT canonical-folder slots.
|
{j("mdl", "Acme"), "mdl"},
|
||||||
{filepath.Join(root, "Project-X", "working"), ""},
|
{j("working", "Acme"), "working"},
|
||||||
{filepath.Join(root, "Project-X", "staging"), ""},
|
{j("incoming", "Acme"), "incoming"},
|
||||||
{filepath.Join(root, "Project-X", "reviewing"), ""},
|
// archive/<party> is not itself a slot; received/issued are.
|
||||||
|
{j("archive", "Acme"), ""},
|
||||||
|
{j("archive", "Acme", "received"), "received"},
|
||||||
|
{j("archive", "Acme", "issued"), "issued"},
|
||||||
|
// Non-slots.
|
||||||
{root, ""},
|
{root, ""},
|
||||||
{filepath.Join(root, "Project-X"), ""},
|
{j(), ""},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working", "alice@example.com"), ""},
|
{j("mdl", "Acme", "sub"), ""},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme"), ""},
|
{j("archive", "Acme", "received", "Acme-0042"), ""},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming", "2026-05-15_Acme-0042 (RFI) - Foundation"), ""},
|
{j("random", "dir"), ""},
|
||||||
{filepath.Join(root, "Project-X", "random", "dir"), ""},
|
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got := CanonicalFolderAt(root, tc.path)
|
if got := CanonicalFolderAt(root, tc.path); got != tc.want {
|
||||||
if got != tc.want {
|
t.Errorf("CanonicalFolderAt(%q) = %q, want %q", tc.path[len(root):], 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
|
// TestAutoOwnAt_FromEmbeddedConvention — auto_own is declared at the
|
||||||
// the per-party lifecycle slots (working/staging/reviewing/incoming)
|
// <party> level of the workspace peers (incoming/working/staging/
|
||||||
// and false for received/issued/mdl/rsk.
|
// reviewing); the registers (mdl/rsk/ssr) and the WORM archive don't
|
||||||
|
// auto-own.
|
||||||
func TestAutoOwnAt_FromEmbeddedConvention(t *testing.T) {
|
func TestAutoOwnAt_FromEmbeddedConvention(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working"), true},
|
{j("working", "Acme"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working", "alice@example.com"), true},
|
{j("staging", "Acme"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "staging"), true},
|
{j("reviewing", "Acme"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "reviewing"), true},
|
{j("incoming", "Acme"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
|
{j("mdl", "Acme"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
|
{j("rsk", "Acme"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "issued"), false},
|
{j("archive", "Acme", "received"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), false},
|
{j("archive", "Acme", "issued"), false},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "rsk"), false},
|
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got := AutoOwnAt(root, tc.path)
|
if got := AutoOwnAt(root, tc.path); got != tc.want {
|
||||||
if got != tc.want {
|
t.Errorf("AutoOwnAt(%q) = %v, want %v", tc.path[len(root):], got, tc.want)
|
||||||
t.Errorf("AutoOwnAt(%q) = %v, want %v",
|
|
||||||
tc.path[len(root):], got, tc.want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestVirtualAt_FromEmbeddedConvention — mdl/rsk under a party are
|
// TestVirtualAt_FromEmbeddedConvention — the flat-peer layout has no
|
||||||
// declared virtual, and the six project-level aggregators are virtual.
|
// virtual: directories (every peer is physical), so VirtualAt is false
|
||||||
// Other canonical slots materialise on disk.
|
// everywhere unless an operator sets it.
|
||||||
func TestVirtualAt_FromEmbeddedConvention(t *testing.T) {
|
func TestVirtualAt_FromEmbeddedConvention(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
cases := []struct {
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
|
||||||
path string
|
for _, p := range []string{
|
||||||
want bool
|
j("ssr"), j("mdl"), j("rsk"), j("working"), j("staging"), j("reviewing"), j("incoming"),
|
||||||
|
j("mdl", "Acme"), j("working", "Acme"), j("archive", "Acme", "received"),
|
||||||
} {
|
} {
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "mdl"), true},
|
if VirtualAt(root, p) {
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "rsk"), true},
|
t.Errorf("VirtualAt(%q) = true, want false (no virtual peers)", p[len(root):])
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "reviewing"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "staging"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), false},
|
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "received"), false},
|
|
||||||
// Project-level aggregators.
|
|
||||||
{filepath.Join(root, "Project-X", "ssr"), true},
|
|
||||||
{filepath.Join(root, "Project-X", "mdl"), true},
|
|
||||||
{filepath.Join(root, "Project-X", "rsk"), true},
|
|
||||||
{filepath.Join(root, "Project-X", "working"), true},
|
|
||||||
{filepath.Join(root, "Project-X", "staging"), true},
|
|
||||||
{filepath.Join(root, "Project-X", "reviewing"), true},
|
|
||||||
}
|
|
||||||
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
|
// TestIsDeclaredPath_FromEmbeddedConvention — the top-level peers are
|
||||||
// the convention are declared even on a fresh root; arbitrary paths
|
// cascade-declared even on a fresh root; arbitrary names are not.
|
||||||
// are not.
|
|
||||||
func TestIsDeclaredPath_FromEmbeddedConvention(t *testing.T) {
|
func TestIsDeclaredPath_FromEmbeddedConvention(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Project-X"}, p...)...) }
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
path string
|
path string
|
||||||
want bool
|
want bool
|
||||||
}{
|
}{
|
||||||
{filepath.Join(root, "Project-X", "archive"), true},
|
{j("archive"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), true},
|
{j("ssr"), true},
|
||||||
{filepath.Join(root, "Project-X", "archive", "Acme", "working"), true},
|
{j("incoming"), true},
|
||||||
// Project-root aggregators are also declared.
|
{j("working"), true},
|
||||||
{filepath.Join(root, "Project-X", "working"), true},
|
{j("staging"), true},
|
||||||
{filepath.Join(root, "Project-X", "reviewing"), true},
|
{j("reviewing"), true},
|
||||||
{filepath.Join(root, "Project-X", "ssr"), true},
|
{j("mdl"), true},
|
||||||
{filepath.Join(root, "Project-X", "junk"), false}, // not in convention
|
{j("rsk"), true},
|
||||||
|
{j("junk"), false},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
got := IsDeclaredPath(root, tc.path)
|
if got := IsDeclaredPath(root, tc.path); got != tc.want {
|
||||||
if got != tc.want {
|
t.Errorf("IsDeclaredPath(%q) = %v, want %v", tc.path[len(root):], got, tc.want)
|
||||||
t.Errorf("IsDeclaredPath(%q) = %v, want %v",
|
|
||||||
tc.path[len(root):], got, tc.want)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestChildrenDeclaredAt_FromEmbeddedConvention — at a project root
|
|
||||||
// the cascade declares archive/ plus the six top-level virtual
|
|
||||||
// aggregator slots (ssr, mdl, rsk, working, staging, reviewing).
|
|
||||||
func TestChildrenDeclaredAt_FromEmbeddedConvention(t *testing.T) {
|
|
||||||
resetCache()
|
|
||||||
root := t.TempDir()
|
|
||||||
got := ChildrenDeclaredAt(root, filepath.Join(root, "Project-X"))
|
|
||||||
want := map[string]bool{
|
|
||||||
"archive": true,
|
|
||||||
"ssr": true, "mdl": true, "rsk": true,
|
|
||||||
"working": true, "staging": true, "reviewing": 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", "archive", "Acme", "working"), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// Operator declares that Special/archive/Acme/working uses
|
|
||||||
// classifier instead of the embedded-default browse.
|
|
||||||
writeZddc(t, filepath.Join(root, "Special", "archive", "Acme", "working"),
|
|
||||||
"default_tool: classifier\n")
|
|
||||||
|
|
||||||
if got := DefaultToolAt(root, filepath.Join(root, "Special", "archive", "Acme", "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", "archive", "Acme", "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 archive/<party>/working/ — not explicitly
|
|
||||||
// mentioned in paths:.
|
|
||||||
deep := filepath.Join(root, "Project-X", "archive", "Acme", "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", "archive", "Acme", "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", "archive", "Acme", "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", "archive", "Acme", "working")) != "" {
|
|
||||||
t.Errorf("with inherit:false at root, default_tool should be empty for working")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -25,35 +25,21 @@ import (
|
||||||
func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
|
func TestStandardRoles_DocControllerScopedCreate(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
// DCs are typically internal employees and ARE in project_team
|
// DC authority comes PURELY from the cascade peer grants in
|
||||||
// (which is commonly defined as the *@example.com wildcard). The
|
// defaults.zddc.yaml — no auto-own / admins: list. DCs are typically
|
||||||
// embedded defaults restate document_controller:rwcda at every
|
// in project_team too (the *@example.com wildcard); the defaults
|
||||||
// slot that grants project_team a narrower verb set; the
|
// restate document_controller at each peer so the within-level union
|
||||||
// cascade's within-level union then gives the DC the higher
|
// gives the DC the higher grant.
|
||||||
// grant. This fixture mirrors the realistic deployment shape so
|
|
||||||
// the union behavior is actually exercised.
|
|
||||||
writeZddc(t, root, `roles:
|
writeZddc(t, root, `roles:
|
||||||
document_controller:
|
document_controller:
|
||||||
members: ["dc@example.com"]
|
members: ["dc@example.com"]
|
||||||
project_team:
|
project_team:
|
||||||
members: ["*@example.com"]
|
members: ["*@example.com"]
|
||||||
`)
|
|
||||||
// Simulate the auto-own .zddc the file API writes when DC mkdir's
|
|
||||||
// archive/Acme/. Carries the creator email + the document_controller
|
|
||||||
// role per the embedded defaults' auto_own_roles entry.
|
|
||||||
partyDir := filepath.Join(root, "Proj", "archive", "Acme")
|
|
||||||
if err := os.MkdirAll(partyDir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
writeZddc(t, partyDir, `acl:
|
|
||||||
permissions:
|
|
||||||
"dc@example.com": rwcda
|
|
||||||
document_controller: rwcda
|
|
||||||
created_by: dc@example.com
|
|
||||||
`)
|
`)
|
||||||
resetCache()
|
resetCache()
|
||||||
|
|
||||||
dc := "dc@example.com"
|
dc := "dc@example.com"
|
||||||
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Proj"}, p...)...) }
|
||||||
|
|
||||||
mustVerbs := func(dir string, want string) {
|
mustVerbs := func(dir string, want string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
@ -73,61 +59,29 @@ created_by: dc@example.com
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project level: rw (no c).
|
// Project level: rw (no c).
|
||||||
mustVerbs(filepath.Join(root, "Proj"), "rw")
|
mustVerbs(j(), "rw")
|
||||||
// A random subfolder under the project inherits rw (no c).
|
mustVerbs(j("random-folder"), "rw")
|
||||||
mustVerbs(filepath.Join(root, "Proj", "random-folder"), "rw")
|
// archive subtree is WORM: read + worm-create only (w/d/a stripped).
|
||||||
// archive/: rwc (can create party folders).
|
mustVerbs(j("archive"), "rc")
|
||||||
mustVerbs(filepath.Join(root, "Proj", "archive"), "rwc")
|
mustVerbs(j("archive", "Acme"), "rc")
|
||||||
// At the party folder itself: rwcda via the auto-own role grant.
|
mustVerbs(j("archive", "Acme", "received"), "rc")
|
||||||
mustVerbs(partyDir, "rwcda")
|
mustVerbs(j("archive", "Acme", "issued"), "rc")
|
||||||
// Lifecycle slots inside the party folder inherit rwcda from the
|
// Workspace peers: full authority via the peer-level DC grant.
|
||||||
// party-level role grant where no slot-local grant overrides.
|
mustVerbs(j("working", "Acme"), "rwcda")
|
||||||
mustVerbs(filepath.Join(partyDir, "working"), "rwcda")
|
mustVerbs(j("staging", "Acme"), "rwcda")
|
||||||
mustVerbs(filepath.Join(partyDir, "reviewing"), "rwcda")
|
mustVerbs(j("reviewing", "Acme"), "rwcda")
|
||||||
// incoming/ has explicit document_controller: rwcd
|
mustVerbs(j("incoming", "Acme"), "rwcd")
|
||||||
// — leaf-wins shadows the rwcda inherited from <party>/.
|
// Registers.
|
||||||
mustVerbs(filepath.Join(partyDir, "incoming"), "rwcd")
|
mustVerbs(j("mdl", "Acme"), "rwcd")
|
||||||
// staging/ has explicit document_controller: rwcda (rwcd for
|
mustVerbs(j("rsk", "Acme"), "rwcd")
|
||||||
// transfer + `a` for Plan Review's staging/<tracking>/.zddc).
|
mustVerbs(j("ssr"), "rwc")
|
||||||
mustVerbs(filepath.Join(partyDir, "staging"), "rwcda")
|
|
||||||
// received/ (WORM): inherited rwcda masked to r + worm-restored c.
|
|
||||||
mustVerbs(filepath.Join(partyDir, "received"), "rc")
|
|
||||||
mustVerbs(filepath.Join(partyDir, "issued"), "rc")
|
|
||||||
|
|
||||||
// NOT subtree-admin anywhere — even when notionally elevated,
|
// NOT subtree-admin anywhere — no admins: grant for the role.
|
||||||
// the role carries no admin: grant.
|
for _, p := range []string{j(), j("archive"), j("working", "Acme"), j("ssr"), j("mdl", "Acme")} {
|
||||||
for _, p := range []string{
|
|
||||||
filepath.Join(root, "Proj"),
|
|
||||||
filepath.Join(root, "Proj", "archive"),
|
|
||||||
partyDir,
|
|
||||||
filepath.Join(partyDir, "working"),
|
|
||||||
filepath.Join(partyDir, "staging"),
|
|
||||||
filepath.Join(partyDir, "reviewing"),
|
|
||||||
filepath.Join(partyDir, "received"),
|
|
||||||
filepath.Join(partyDir, "issued"),
|
|
||||||
} {
|
|
||||||
if IsSubtreeAdmin(root, p, Principal{Email: dc, Elevated: true}) {
|
if IsSubtreeAdmin(root, p, Principal{Email: dc, Elevated: true}) {
|
||||||
t.Errorf("doc controller should NOT be subtree-admin of %s (no admins: list anywhere)", p[len(root):])
|
t.Errorf("doc controller should NOT be subtree-admin of %s (no admins: list anywhere)", p[len(root):])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And specifically — they CAN'T reach inside a fenced per-user
|
|
||||||
// working home. The fence isolates team-member workspaces from
|
|
||||||
// every other role (including DC) by design.
|
|
||||||
homeDir := filepath.Join(partyDir, "working", "alice@example.com")
|
|
||||||
if err := os.MkdirAll(homeDir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
writeZddc(t, homeDir, `acl:
|
|
||||||
inherit: false
|
|
||||||
permissions:
|
|
||||||
"alice@example.com": rwcda
|
|
||||||
created_by: alice@example.com
|
|
||||||
`)
|
|
||||||
resetCache()
|
|
||||||
chain, _ := EffectivePolicy(root, homeDir)
|
|
||||||
if got := EffectiveVerbs(chain, dc); got != 0 {
|
|
||||||
t.Errorf("doc controller inside alice's fenced home = %q, want empty (fence isolates)", got.String())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestStandardRoles_DocControllerMultiDC — a second DC added to the
|
// TestStandardRoles_DocControllerMultiDC — a second DC added to the
|
||||||
|
|
@ -255,13 +209,13 @@ func TestStandardRoles_ProjectTeamInFlightRatchet(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
party := filepath.Join(root, "Proj", "archive", "Acme")
|
j := func(p ...string) string { return filepath.Join(append([]string{root, "Proj"}, p...)...) }
|
||||||
mustVerbs(filepath.Join(party, "working"), "rc") // create + read at slot
|
mustVerbs(j("working", "Acme"), "rc") // create + read at the workspace
|
||||||
mustVerbs(filepath.Join(party, "staging"), "rc") // drop + read, no modify
|
mustVerbs(j("staging", "Acme"), "rc") // drop + read, no modify
|
||||||
mustVerbs(filepath.Join(party, "reviewing"), "rc") // create iteration folders
|
mustVerbs(j("reviewing", "Acme"), "rc") // create iteration folders
|
||||||
mustVerbs(filepath.Join(party, "received"), "r") // WORM — read pass-through, no worm-create
|
mustVerbs(j("archive", "Acme", "received"), "r") // WORM — read pass-through
|
||||||
mustVerbs(filepath.Join(party, "issued"), "r") // WORM — same
|
mustVerbs(j("archive", "Acme", "issued"), "r") // WORM — same
|
||||||
mustVerbs(filepath.Join(party, "incoming"), "r") // counterparty drop zone — read only
|
mustVerbs(j("incoming", "Acme"), "r") // counterparty drop zone — read only
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestStandardRoles_DocControllerStagingDelete — DC needs `d` at
|
// TestStandardRoles_DocControllerStagingDelete — DC needs `d` at
|
||||||
|
|
@ -277,7 +231,7 @@ func TestStandardRoles_DocControllerStagingDelete(t *testing.T) {
|
||||||
members: ["dc@example.com"]
|
members: ["dc@example.com"]
|
||||||
`)
|
`)
|
||||||
dc := "dc@example.com"
|
dc := "dc@example.com"
|
||||||
chain, err := EffectivePolicy(root, filepath.Join(root, "Proj", "archive", "Acme", "staging"))
|
chain, err := EffectivePolicy(root, filepath.Join(root, "Proj", "staging", "Acme"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("EffectivePolicy: %v", err)
|
t.Fatalf("EffectivePolicy: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestWormZoneGrant_EmbeddedConvention — archive/<party>/received and
|
// TestWormZoneGrant_EmbeddedConvention — defaults.zddc.yaml declares
|
||||||
// issued carry `worm: []` in defaults.zddc.yaml, so any path under
|
// `worm: [document_controller]` on archive/, so the ENTIRE archive
|
||||||
// those folders is a WORM zone (inWorm=true) with no create-capable
|
// subtree is a WORM zone (inWorm=true). With no role members in this
|
||||||
// principals (grant=0). Other paths are not WORM zones.
|
// bare fixture the grant for an arbitrary principal is 0. The top-level
|
||||||
|
// workspace/register peers are NOT under archive and are not WORM.
|
||||||
func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
|
func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
|
||||||
resetCache()
|
resetCache()
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
|
|
@ -18,13 +19,16 @@ func TestWormZoneGrant_EmbeddedConvention(t *testing.T) {
|
||||||
path string
|
path string
|
||||||
wantInWorm bool
|
wantInWorm bool
|
||||||
}{
|
}{
|
||||||
|
{filepath.Join(root, "Proj", "archive"), true},
|
||||||
|
{filepath.Join(root, "Proj", "archive", "Acme"), true},
|
||||||
{filepath.Join(root, "Proj", "archive", "Acme", "received"), true},
|
{filepath.Join(root, "Proj", "archive", "Acme", "received"), true},
|
||||||
{filepath.Join(root, "Proj", "archive", "Acme", "issued"), true},
|
{filepath.Join(root, "Proj", "archive", "Acme", "issued"), true},
|
||||||
{filepath.Join(root, "Proj", "archive", "Acme", "received", "2025-Q1"), true}, // deeper still WORM
|
{filepath.Join(root, "Proj", "archive", "Acme", "received", "2025-Q1"), true},
|
||||||
{filepath.Join(root, "Proj", "archive", "Acme", "incoming"), false},
|
{filepath.Join(root, "Proj", "incoming"), false},
|
||||||
{filepath.Join(root, "Proj", "archive", "Acme", "mdl"), false},
|
{filepath.Join(root, "Proj", "mdl"), false},
|
||||||
{filepath.Join(root, "Proj", "working"), false},
|
{filepath.Join(root, "Proj", "working"), false},
|
||||||
{filepath.Join(root, "Proj", "staging"), false},
|
{filepath.Join(root, "Proj", "staging"), false},
|
||||||
|
{filepath.Join(root, "Proj", "ssr"), false},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
chain, err := EffectivePolicy(root, tc.path)
|
chain, err := EffectivePolicy(root, tc.path)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue