feat(zddc): defaults — browse hosts the markdown editor for working/+reviewing/
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
This commit is contained in:
parent
b5aab81d31
commit
7fbe7867fd
4 changed files with 56 additions and 55 deletions
|
|
@ -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: <dir>/ → browse (directory view, via DirTool which
|
||||
// defaults to browse); <dir> → the directory's default_tool ("the
|
||||
// specialized app": mdedit under working/, transmittal under
|
||||
// staging/, archive under archive/, tables under archive/<party>/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/<party>/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) {
|
||||
|
|
|
|||
|
|
@ -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/<rs-name>/)
|
||||
{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"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
# <dir> (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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue