ZDDC/zddc/internal/zddc/ensure_test.go
ZDDC 56c3353f7b feat(browse): party picker for New folder/file in virtual aggregators
Creating a folder/file at a project-level folder-nav aggregator root
(working/staging/reviewing) used to error or silently shadow — the slots
are virtual and content is party-scoped. Now browse opens a party picker
that targets archive/<party>/<slot>/<name>, with a "+ New party…" option
(server-gated to the document_controller via the existing archive/ ACL).

- events.js: aggregatorRoot detection + openPartyPicker modal (mirrors the
  stage.js modal), createInAggregator routes the create to the canonical
  archive path; rewriteAggregatorPath handles right-clicking a party row
  shown in an aggregator listing so it never re-prompts.
- server: serveFileMkdir now 409s a mkdir inside an aggregator
  (rejectProjectAggregatorMkdir) with a pointer at archive/<party>/<slot>/,
  instead of letting the write fall through to an unreachable shadow dir.

Reverts the prior session's project-level creator-owned working/ folders
(per the design decision to make all three folder-nav slots uniformly
party-scoped): working/ is a pure virtual aggregator again like
staging/reviewing — drops the working/ history+auto_own+acl defaults, the
EnsureCanonicalAncestors working exception, the working-root document-
controller file gate (serveFilePut/Move) and zddc.IsRoleMemberAt. Per-party
archive/<party>/working/ keeps its own history + auto-own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 10:39:49 -05:00

225 lines
8.7 KiB
Go

package zddc
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestEnsureCanonicalAncestors_LazyCreation(t *testing.T) {
root := t.TempDir()
// Per-user homes now live under archive/<party>/working/<email>/
// after the top-of-project reshape. The depth-3 working slot is
// the canonical-folder position; its auto-own .zddc is unfenced
// and the depth-4 per-user home gets the fenced one.
target := filepath.Join(root, "Proj", "archive", "ACME", "working", "alice@x.com", "notes.md")
resolved, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
if err != nil {
t.Fatalf("ensure: %v", err)
}
if 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
// 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)
if err != nil {
t.Fatalf("auto-own .zddc not written at working/: %v", err)
}
body := string(data)
if !strings.Contains(body, "alice@x.com: rwcda") {
t.Errorf("auto-own grant missing: %s", body)
}
if !strings.Contains(body, "created_by: alice@x.com") {
t.Errorf("created_by missing: %s", body)
}
if strings.Contains(body, "inherit: false") {
t.Errorf("party working/ .zddc should be UNFENCED so party admins still reach inside; got: %s", body)
}
// alice@x.com/ subfolder gets a FENCED auto-own .zddc — private by
// default so other users can't read alice's drafts via ancestor
// cascade. alice can edit the file later to add collaborators.
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
// folders are date+tracking-named, not per-user). Only per-user homes
// under working/ get the fence.
func TestEnsureCanonicalAncestors_StagingChildNotFenced(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "Proj", "archive", "ACME", "staging",
"2025-10-31_proj-EM-TRN-0042 (RSA) - Outbound", "doc.pdf")
if _, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755); err != nil {
t.Fatalf("ensure: %v", err)
}
// staging/<folder>/.zddc should not exist (only the parent staging/
// 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")
if _, err := os.Stat(childZddc); !os.IsNotExist(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", "archive", "ACME", "staging", ".zddc")
if _, err := os.Stat(stagingZddc); err != nil {
t.Errorf("party staging/ auto-own .zddc missing: %v", err)
}
}
func TestEnsureCanonicalAncestors_CaseFoldReuse(t *testing.T) {
root := t.TempDir()
// Pre-create Archive/ (PascalCase) — case-fold reuse applies to
// the canonical project-root slot.
if err := os.MkdirAll(filepath.Join(root, "Proj", "Archive", "ACME", "working"), 0o755); err != nil {
t.Fatal(err)
}
target := filepath.Join(root, "Proj", "archive", "ACME", "working", "foo.md")
resolved, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
if err != nil {
t.Fatalf("ensure: %v", err)
}
// Resolved path uses on-disk Archive/ casing.
want := filepath.Join(root, "Proj", "Archive", "ACME", "working", "foo.md")
if 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) {
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) {
root := t.TempDir()
target := filepath.Join(root, "Proj", "archive", "ACME", "incoming", "submission.pdf")
_, err := EnsureCanonicalAncestors(root, target, "rep@acme.com", 0o755)
if err != nil {
t.Fatalf("ensure: %v", err)
}
// 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)
if err != nil {
t.Fatalf("auto-own .zddc at incoming/ not written: %v", err)
}
if !strings.Contains(string(data), "rep@acme.com: rwcda") {
t.Errorf("incoming/ auto-own missing rep grant: %s", data)
}
}
func TestEnsureCanonicalAncestors_WormFoldersNoAutoOwn(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "Proj", "archive", "ACME", "issued", "spec.pdf")
_, err := EnsureCanonicalAncestors(root, target, "dc@mycompany.com", 0o755)
if err != nil {
t.Fatalf("ensure: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "issued")); err != nil {
t.Errorf("issued/ not created: %v", 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)
}
}
func TestEnsureCanonicalAncestors_NoPrincipalSkipsAutoOwn(t *testing.T) {
root := t.TempDir()
target := filepath.Join(root, "Proj", "archive", "ACME", "working", "anon.md")
_, err := EnsureCanonicalAncestors(root, target, "" /* no email */, 0o755)
if err != nil {
t.Fatalf("ensure: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "working")); err != nil {
t.Errorf("working/ not created: %v", err)
}
if _, err := os.Stat(filepath.Join(root, "Proj", "archive", "ACME", "working", ".zddc")); !os.IsNotExist(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
// targeting <project>/working/<...> bypasses the virtual resolver
// and must not materialise on disk.
func TestEnsureCanonicalAncestors_RejectsProjectRootVirtual(t *testing.T) {
root := t.TempDir()
for _, slot := range []string{"working", "staging", "reviewing", "ssr", "mdl", "rsk"} {
target := filepath.Join(root, "Proj", slot, "x.md")
_, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
if err == nil {
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) {
t.Errorf("%s: <project>/%s/ must NOT be created on disk; got err=%v", slot, slot, err)
}
}
}
func TestEnsureCanonicalAncestors_RejectsTraversal(t *testing.T) {
root := t.TempDir()
other := t.TempDir()
target := filepath.Join(other, "evil.md")
_, err := EnsureCanonicalAncestors(root, target, "alice@x.com", 0o755)
if err == nil {
t.Errorf("expected error for target outside fsRoot")
}
}