From 7fbe7867fd5319a637a44e32b2f61c908d623d65 Mon Sep 17 00:00:00 2001 From: ZDDC Date: Wed, 13 May 2026 10:34:06 -0500 Subject: [PATCH] =?UTF-8?q?feat(zddc):=20defaults=20=E2=80=94=20browse=20h?= =?UTF-8?q?osts=20the=20markdown=20editor=20for=20working/+reviewing/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flip default_tool from `mdedit` to `browse` (which now ships a Toast UI markdown editor plugin in its preview pane) at: • paths."*".paths.working • paths."*".paths.working.paths."*" (per-user homes) • paths."*".paths.reviewing available_tools at those levels drops `mdedit` and adds `browse` next to `classifier`. Operator overrides per .zddc cascade still work; only the embedded baseline changes. Test fixtures updated: • lookups_test.go — DefaultToolAt assertions for working/+reviewing/ • availability_test.go — AppAvailableAt + DefaultAppAt for working/+ reviewing/+per-user home • main_test.go — dispatch route asserts "ZDDC Browse" (was "ZDDC Markdown"); Apps cascade fixture swaps mdedit for browse so the live route fetches the right embedded HTML --- zddc/cmd/zddc-server/main_test.go | 32 ++++++++++++------------- zddc/internal/apps/availability_test.go | 32 ++++++++++++------------- zddc/internal/zddc/defaults.zddc.yaml | 31 ++++++++++++------------ zddc/internal/zddc/lookups_test.go | 16 ++++++------- 4 files changed, 56 insertions(+), 55 deletions(-) diff --git a/zddc/cmd/zddc-server/main_test.go b/zddc/cmd/zddc-server/main_test.go index 9a42a54..9a1c80d 100644 --- a/zddc/cmd/zddc-server/main_test.go +++ b/zddc/cmd/zddc-server/main_test.go @@ -138,14 +138,14 @@ func TestDispatchAppsResolution(t *testing.T) { "archive": upstream.URL + "/archive_stable.html", "transmittal": upstream.URL + "/transmittal_stable.html", "classifier": upstream.URL + "/classifier_stable.html", - "mdedit": upstream.URL + "/mdedit_stable.html", "landing": upstream.URL + "/landing_stable.html", + "browse": upstream.URL + "/browse_stable.html", }, } if err := zddc.WriteFile(root, zf); err != nil { t.Fatalf("WriteFile: %v", err) } - // Create folder convention dirs so classifier/mdedit/transmittal + // Create folder convention dirs so classifier/browse/transmittal // availability rules pass for the test paths used below. mustMkdir(t, filepath.Join(root, "Project-A", "Working")) @@ -380,10 +380,10 @@ func TestDispatchArchiveRedirect(t *testing.T) { func TestDispatchSlashRouting(t *testing.T) { // Convention: / → browse (directory view, via DirTool which // defaults to browse); → the directory's default_tool ("the - // specialized app": mdedit under working/, transmittal under - // staging/, archive under archive/, tables under archive//mdl). - // Without a default_tool, no-slash falls through to the trailing- - // slash redirect (302). + // specialized app": browse under working/+reviewing/, transmittal + // under staging/, archive under archive/, tables under + // archive//mdl). Without a default_tool, no-slash falls + // through to the trailing-slash redirect (302). // // The only trailing-slash redirect is for a directory that is the // rows-dir of a table declared via a REAL on-disk parent .zddc @@ -429,7 +429,7 @@ func TestDispatchSlashRouting(t *testing.T) { wantNoRedirect bool wantLoc string // checked when wantStatus is a redirect }{ - {"working no-slash → mdedit", "/Project/working", http.StatusOK, true, ""}, + {"working no-slash → browse", "/Project/working", http.StatusOK, true, ""}, {"working slash → browse", "/Project/working/", http.StatusOK, true, ""}, {"staging no-slash → transmittal", "/Project/staging", http.StatusOK, true, ""}, {"staging slash → browse", "/Project/staging/", http.StatusOK, true, ""}, @@ -525,21 +525,21 @@ func TestDispatchEmptyCanonicalProjectFolders(t *testing.T) { } // No-trailing-slash form on a canonical folder → default app - // (mdedit for working/, transmittal for staging/, archive for - // archive/). Mirror of the existing "no-slash → default app" - // behavior at the IsDir branch, extended to cover the case where - // the folder doesn't exist on disk yet. + // (browse for working/+reviewing/, transmittal for staging/, + // archive for archive/). Mirror of the existing "no-slash → + // default app" behavior at the IsDir branch, extended to cover + // the case where the folder doesn't exist on disk yet. noSlashDefaultApp := []struct { stage string expect string // substring that should appear in the response body }{ - {"working", "ZDDC Markdown"}, + {"working", "ZDDC Browse"}, {"staging", "ZDDC Transmittal"}, {"archive", "ZDDC Archive"}, - // reviewing/ also routes to mdedit; the polyfill follows the - // virtual aggregator's listing into canonical archive/+staging - // paths from there. - {"reviewing", "ZDDC Markdown"}, + // reviewing/ also routes to browse (markdown editor lives + // inside it now); the polyfill follows the virtual aggregator's + // listing into canonical archive/+staging paths from there. + {"reviewing", "ZDDC Browse"}, } for _, tc := range noSlashDefaultApp { t.Run("no-slash/"+tc.stage+" → default app", func(t *testing.T) { diff --git a/zddc/internal/apps/availability_test.go b/zddc/internal/apps/availability_test.go index 19788bb..5f055cd 100644 --- a/zddc/internal/apps/availability_test.go +++ b/zddc/internal/apps/availability_test.go @@ -35,11 +35,12 @@ func TestAppAvailableAt(t *testing.T) { {root + "/Project-A/archive/ACME/mdl", "classifier", false}, {root + "/Project-A/some-other-folder", "classifier", false}, - // mdedit: working/ only (review responses live in working//) - {root + "/Project-A/working", "mdedit", true}, - {root + "/Project-A/working/sub", "mdedit", true}, - {root + "/Project-A/staging", "mdedit", false}, - {root + "/Project-A/archive/ACME/incoming", "mdedit", false}, + // browse: universal — every directory has browse available + // (it's in the embedded-defaults baseline available_tools). + {root + "/Project-A/working", "browse", true}, + {root + "/Project-A/working/sub", "browse", true}, + {root + "/Project-A/staging", "browse", true}, + {root + "/Project-A/archive/ACME/incoming", "browse", true}, // transmittal: staging/ only {root + "/Project-A/staging", "transmittal", true}, @@ -48,8 +49,6 @@ func TestAppAvailableAt(t *testing.T) { {root + "/Project-A/archive/ACME/issued", "transmittal", false}, // case-fold: any case of canonical names matches - {root + "/Project-A/Working", "mdedit", true}, - {root + "/Project-A/WORKING", "mdedit", true}, {root + "/Project-A/Staging", "transmittal", true}, {root + "/Project-A/STAGING", "transmittal", true}, {root + "/Project-A/archive/ACME/Incoming", "classifier", true}, @@ -81,9 +80,9 @@ func TestDefaultAppAt(t *testing.T) { // no-slash falls through to the redirect. {root + "/Project-A", ""}, // Canonical project-root folders. - {root + "/Project-A/working", "mdedit"}, - {root + "/Project-A/working/alice@example.com", "mdedit"}, - {root + "/Project-A/working/2026-06-15_x (DFT) - y", "mdedit"}, + {root + "/Project-A/working", "browse"}, + {root + "/Project-A/working/alice@example.com", "browse"}, + {root + "/Project-A/working/2026-06-15_x (DFT) - y", "browse"}, {root + "/Project-A/staging", "transmittal"}, {root + "/Project-A/staging/2026-06-15_x (DFT) - y", "transmittal"}, // archive: at the archive root, party folders default to archive. @@ -98,15 +97,16 @@ func TestDefaultAppAt(t *testing.T) { // mdl wins over the broader archive rule. {root + "/Project-A/archive/Acme/mdl", "tables"}, {root + "/Project-A/archive/Acme/mdl/anything-deeper", "tables"}, - // reviewing/ is virtual but mdedit is wired as the default - // tool; the polyfill follows the listing's canonical URLs - // into archive/ and staging/ for the actual files. - {root + "/Project-A/reviewing", "mdedit"}, - {root + "/Project-A/reviewing/123-EM-SUB-0001", "mdedit"}, + // reviewing/ is virtual; browse hosts the markdown editor that + // renders responses (the polyfill follows the listing's + // canonical URLs into archive/ and staging/ for the actual + // files). + {root + "/Project-A/reviewing", "browse"}, + {root + "/Project-A/reviewing/123-EM-SUB-0001", "browse"}, // Random non-canonical folder names → no default. {root + "/Project-A/scratch", ""}, // Case-fold on canonical names. - {root + "/Project-A/Working", "mdedit"}, + {root + "/Project-A/Working", "browse"}, {root + "/Project-A/STAGING", "transmittal"}, {root + "/Project-A/Archive/Acme/MDL", "tables"}, } diff --git a/zddc/internal/zddc/defaults.zddc.yaml b/zddc/internal/zddc/defaults.zddc.yaml index 8690aa6..462a8a4 100644 --- a/zddc/internal/zddc/defaults.zddc.yaml +++ b/zddc/internal/zddc/defaults.zddc.yaml @@ -53,11 +53,12 @@ roles: members: [] # Universal tool baseline. archive (record browser), browse (file -# tree), and landing (project picker) work everywhere. Each canonical -# folder below adds its own context-specific tools (mdedit in -# working/, transmittal in staging/, etc.). The cascade unions -# available_tools across all levels — leaf restrictions don't drop -# ancestor entries — so this baseline propagates to every descendant. +# tree, hosts the in-place markdown editor), and landing (project +# picker) work everywhere. Each canonical folder below adds its own +# context-specific tools (transmittal in staging/, etc.). The cascade +# unions available_tools across all levels — leaf restrictions don't +# drop ancestor entries — so this baseline propagates to every +# descendant. available_tools: [archive, browse, landing] # ── The slash / no-slash routing convention ──────────────────────────────── @@ -71,9 +72,9 @@ available_tools: [archive, browse, landing] # default; you rarely set it. # (no slash) → `default_tool` — the "specialized # app" for this folder (e.g. archive, -# transmittal, mdedit, tables). If a -# folder declares no default_tool, the -# no-slash form just 302s to the slash +# transmittal, tables). If a folder +# declares no default_tool, the no- +# slash form just 302s to the slash # form, so you land on `dir_tool`. # # JSON listing requests are unaffected by either key — they always get @@ -81,7 +82,7 @@ available_tools: [archive, browse, landing] # can enumerate entries no matter what dir_tool/default_tool are. # # Both keys cascade leaf→root: a parent's default_tool applies to -# descendants unless a deeper level overrides it (mdedit set on +# descendants unless a deeper level overrides it (browse set on # working/ reaches working/alice/notes/ for free). The keys below set # default_tool on the canonical folders; dir_tool is left unset # everywhere, so the slash form is always `browse`. @@ -201,8 +202,8 @@ paths: default_tool: archive worm: [document_controller] working: - default_tool: mdedit - available_tools: [mdedit, classifier] + default_tool: browse + available_tools: [browse, classifier] # working/ auto-owns the first creator + the per-user homes # below. auto_own: true @@ -215,8 +216,8 @@ paths: admins: [document_controller] paths: "*": # per-user home dir - default_tool: mdedit - available_tools: [mdedit, classifier] + default_tool: browse + available_tools: [browse, classifier] auto_own: true # Per-user home is private by default: the generated # auto-own .zddc carries inherit:false so ancestor ACL @@ -233,8 +234,8 @@ paths: # rationale as working/. admins: [document_controller] reviewing: - default_tool: mdedit - available_tools: [mdedit] + default_tool: browse + available_tools: [browse] # reviewing/ is purely virtual — the aggregator handler # synthesises listings from received/ ↔ staging/ ↔ issued/. virtual: true diff --git a/zddc/internal/zddc/lookups_test.go b/zddc/internal/zddc/lookups_test.go index 71a7d83..c2770e0 100644 --- a/zddc/internal/zddc/lookups_test.go +++ b/zddc/internal/zddc/lookups_test.go @@ -21,10 +21,10 @@ func TestDefaultToolAt_FromEmbeddedConvention(t *testing.T) { {filepath.Join(root, "Project-X", "archive", "Acme", "incoming"), "classifier"}, {filepath.Join(root, "Project-X", "archive", "Acme", "received"), "archive"}, {filepath.Join(root, "Project-X", "archive", "Acme", "issued"), "archive"}, - {filepath.Join(root, "Project-X", "working"), "mdedit"}, - {filepath.Join(root, "Project-X", "working", "alice@example.com"), "mdedit"}, + {filepath.Join(root, "Project-X", "working"), "browse"}, + {filepath.Join(root, "Project-X", "working", "alice@example.com"), "browse"}, {filepath.Join(root, "Project-X", "staging"), "transmittal"}, - {filepath.Join(root, "Project-X", "reviewing"), "mdedit"}, + {filepath.Join(root, "Project-X", "reviewing"), "browse"}, } for _, tc := range cases { got := DefaultToolAt(root, tc.path) @@ -177,7 +177,7 @@ func TestOperatorOverride_DefaultsAreSurfaceable(t *testing.T) { t.Fatal(err) } // Operator declares that Special/working uses classifier - // instead of the embedded-default mdedit. + // instead of the embedded-default browse. writeZddc(t, filepath.Join(root, "Special", "working"), "default_tool: classifier\n") @@ -185,7 +185,7 @@ func TestOperatorOverride_DefaultsAreSurfaceable(t *testing.T) { t.Errorf("operator override should set default_tool=classifier, got %q", got) } // Default still applies at other projects. - if got := DefaultToolAt(root, filepath.Join(root, "Project-Y", "working")); got != "mdedit" { + if got := DefaultToolAt(root, filepath.Join(root, "Project-Y", "working")); got != "browse" { t.Errorf("default convention should hold at unchanged paths, got %q", got) } } @@ -193,14 +193,14 @@ func TestOperatorOverride_DefaultsAreSurfaceable(t *testing.T) { // 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 mdedit as its default tool. +// gets browse as its default tool. func TestDefaultToolAt_PropagatesToDescendants(t *testing.T) { resetCache() root := t.TempDir() // Deep path under working/ — not explicitly mentioned in paths:. deep := filepath.Join(root, "Project-X", "working", "alice@example.com", "notes", "sub", "deep") - if got := DefaultToolAt(root, deep); got != "mdedit" { - t.Errorf("DefaultToolAt(%q) = %q, want mdedit (cascade propagation)", + if got := DefaultToolAt(root, deep); got != "browse" { + t.Errorf("DefaultToolAt(%q) = %q, want browse (cascade propagation)", deep[len(root):], got) } }