ZDDC/zddc/internal/apps/availability_test.go
ZDDC 59b5550872 refactor: nest lifecycle slots per-party + add virtual top-level aggregators
May 2026 reshape. archive/ is now the only physical project-root
directory; working/, staging/, reviewing/ move from the project root
into each archive/<party>/ folder. Six top-level URLs become virtual
aggregators served via the cascade rather than disk:

  ssr/mdl/rsk           tables rollups across parties with a
                        synthesised $party source-party column
  working/staging/      browse folder-nav listings of parties with
  reviewing             non-empty content in the slot; per-party
                        URLs 302-redirect to archive/<party>/<slot>/

Mkdir at the project root is restricted to `archive` and `_`/`.`-
prefixed system names — virtual aggregator names and ad-hoc folders
return 409.

Plan Review hardcodes the scaffold convention (archive/<party>/
{reviewing,staging}/<tracking>/); the pre-reshape
on_plan_review.{reviewing_root,staging_root} cascade keys are dropped.

document_controller is now subtree-admin of every archive/<party>/
(not of project-root working/staging/ as before), so per-party
lifecycle slots inherit admin authority through the cascade.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 07:57:45 -05:00

123 lines
5 KiB
Go

package apps
import (
"path/filepath"
"testing"
)
func TestAppAvailableAt(t *testing.T) {
root := "/srv/zddc"
cases := []struct {
dir, app string
want bool
}{
// archive: everywhere
{root, "archive", true},
{root + "/Project-A", "archive", true},
{root + "/Project-A/working", "archive", true},
{root + "/Project-A/some-other-folder", "archive", true},
// landing: only at root
{root, "landing", true},
{root + "/Project-A", "landing", false},
// classifier: per-party working/, staging/, incoming/ subtrees
{root, "classifier", false},
{root + "/Project-A", "classifier", false},
{root + "/Project-A/archive/ACME/working", "classifier", true},
{root + "/Project-A/archive/ACME/working/deep/nested/path", "classifier", true},
{root + "/Project-A/archive/ACME/staging", "classifier", true},
{root + "/Project-A/archive/ACME/staging/2026-06-15_x (DFT) - y", "classifier", true},
{root + "/Project-A/archive/ACME/incoming", "classifier", true},
{root + "/Project-A/archive/ACME/incoming/sub", "classifier", true},
{root + "/Project-A/archive/ACME/received", "classifier", false},
{root + "/Project-A/archive/ACME/issued", "classifier", false},
{root + "/Project-A/archive/ACME/mdl", "classifier", false},
{root + "/Project-A/some-other-folder", "classifier", false},
// browse: universal — every directory has browse available
// (it's in the embedded-defaults baseline available_tools).
{root + "/Project-A/archive/ACME/working", "browse", true},
{root + "/Project-A/archive/ACME/working/sub", "browse", true},
{root + "/Project-A/archive/ACME/staging", "browse", true},
{root + "/Project-A/archive/ACME/incoming", "browse", true},
// transmittal: per-party staging/ only
{root + "/Project-A/archive/ACME/staging", "transmittal", true},
{root + "/Project-A/archive/ACME/staging/sub", "transmittal", true},
{root + "/Project-A/archive/ACME/working", "transmittal", false},
{root + "/Project-A/archive/ACME/issued", "transmittal", false},
// case-fold: any case of canonical names matches
{root + "/Project-A/archive/ACME/Staging", "transmittal", true},
{root + "/Project-A/archive/ACME/STAGING", "transmittal", true},
{root + "/Project-A/archive/ACME/Incoming", "classifier", true},
{root + "/Project-A/Archive/ACME/incoming", "classifier", true},
// unknown app
{root + "/Project-A", "weird", false},
}
for _, tc := range cases {
t.Run(tc.app+"@"+tc.dir, func(t *testing.T) {
got := AppAvailableAt(root, filepath.Clean(tc.dir), tc.app)
if got != tc.want {
t.Errorf("AppAvailableAt(%q, %q) = %v, want %v", tc.dir, tc.app, got, tc.want)
}
})
}
}
func TestDefaultAppAt(t *testing.T) {
root := "/srv/zddc"
cases := []struct {
dir string
want string
}{
// At the deployment root itself, no default tool — landing handles
// the project picker via a separate path.
{root, ""},
// Bare project root: no default. Trailing-slash URL serves browse;
// no-slash falls through to the redirect.
{root + "/Project-A", ""},
// Project-level virtual aggregators (sibling to archive/).
{root + "/Project-A/working", "browse"},
{root + "/Project-A/staging", "browse"},
{root + "/Project-A/reviewing", "browse"},
{root + "/Project-A/ssr", "tables"},
{root + "/Project-A/mdl", "tables"},
{root + "/Project-A/rsk", "tables"},
// Per-party lifecycle slots (the real physical homes).
{root + "/Project-A/archive/Acme/working", "browse"},
{root + "/Project-A/archive/Acme/working/alice@example.com", "browse"},
{root + "/Project-A/archive/Acme/working/2026-06-15_x (DFT) - y", "browse"},
{root + "/Project-A/archive/Acme/staging", "transmittal"},
{root + "/Project-A/archive/Acme/staging/2026-06-15_x (DFT) - y", "transmittal"},
{root + "/Project-A/archive/Acme/reviewing", "browse"},
// archive: at the archive root, party folders default to archive.
// Per-party subfolders override per their function:
// incoming → classifier (the bulk-rename workflow)
// received / issued → archive (WORM record browser)
{root + "/Project-A/archive", "archive"},
{root + "/Project-A/archive/Acme", "archive"},
{root + "/Project-A/archive/Acme/incoming", "classifier"},
{root + "/Project-A/archive/Acme/issued", "archive"},
{root + "/Project-A/archive/Acme/received", "archive"},
// mdl/rsk win over the broader archive rule.
{root + "/Project-A/archive/Acme/mdl", "tables"},
{root + "/Project-A/archive/Acme/mdl/anything-deeper", "tables"},
{root + "/Project-A/archive/Acme/rsk", "tables"},
// Random non-canonical folder names → no default.
{root + "/Project-A/scratch", ""},
// Case-fold on canonical names.
{root + "/Project-A/archive/Acme/Working", "browse"},
{root + "/Project-A/archive/Acme/STAGING", "transmittal"},
{root + "/Project-A/Archive/Acme/MDL", "tables"},
}
for _, tc := range cases {
t.Run(tc.dir, func(t *testing.T) {
if got := DefaultAppAt(root, tc.dir); got != tc.want {
t.Errorf("DefaultAppAt(%q) = %q, want %q", tc.dir, got, tc.want)
}
})
}
}