Final consumer migration. The Go-coded lists that previously encoded
the ZDDC convention all defer to the .zddc cascade now.
Schema added:
available_tools: [tool1, tool2, ...] concat-union across cascade;
tools not in the union are
denied auto-route at that path
auto_own_fenced: true|false generated auto-own .zddc
carries inherit:false (private
to creator)
Lookups added:
AvailableToolsAt(root, dir) union of available_tools across cascade
IsToolAvailableAt(root, dir, tool)
AutoOwnFencedAt(root, dir) leaf-only
Cascade semantics finalised (per field):
default_tool → leaf→root walk (parent applies to descendants)
available_tools → leaf→root union (each level adds; baseline at root)
auto_own → leaf-only (creating THIS dir specifically)
auto_own_fenced → leaf-only (same)
virtual → leaf-only (THIS dir is virtual, not subtree)
Consumers migrated:
apps.DefaultAppAt → zddc.DefaultToolAt
apps.AppAvailableAt → zddc.IsToolAvailableAt (+ landing special)
EnsureCanonicalAncestors → AutoOwnAt + AutoOwnFencedAt
fs.ListDirectory empty-list fallback → zddc.IsDeclaredPath
fs.virtualCanonicalFolders → zddc.ChildrenDeclaredAt
dispatcher canonical-folder branches → unified into one
cascade-declared block
Hardcoded helpers REMOVED (dead code):
apps.inAncestorWithName
zddc.autoOwnDepthMatch / isAutoOwnDepthMatch
Hardcoded lists kept as data sources for the cascade walker but
no longer drive routing logic:
ProjectRootFolders / PartyFolders / AutoOwnCanonicalNames /
VirtualOnlyCanonicalNames / IsProjectRootFolder / IsArchivePartyFolder /
IsArchivePartyMdlDir — all still defined; only `ProjectRootFolders`
is used by special.go's IsProjectRootFolder. The rest are dead.
Dispatcher unified: the previously-two branches (per-party folder vs
project-root folder) collapse into one cascade-declared-path block
that handles the slash/no-slash convention uniformly:
- no-slash, default_tool=tables → ServeTable (default-MDL fallback)
- no-slash, default_tool set → apps.Serve(tool)
- no-slash, no default_tool → 302 to slash form
- slash, any → ServeDirectory empty-list fallback
The IsDir branch's switch also un-hardcoded — any cascade tool is
served (not just the legacy 3 names), so e.g. /Project/archive/<party>
/incoming (no slash) now serves classifier directly rather than 302'ing
to the slash form.
defaults.zddc.yaml populated with the canonical convention as the
recipe. Operators edit it (or override per-directory on disk) to
change any behaviour — no Go code changes required.
Browse drag-drop scope (working/staging/incoming) is the one remaining
client-side hardcoded regex; cascading that requires the cascade JSON
to be served to the client, which is its own Phase 4 piece.
Tests updated for the new no-slash mdl URL convention (landing MDL
card test) and no-slash stage URLs (nav strip test). All 248
Playwright + all Go tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
3.8 KiB
JavaScript
91 lines
3.8 KiB
JavaScript
// Tests for shared/nav.js — the lateral project-stage strip.
|
|
//
|
|
// The strip's render decision depends on location.protocol and
|
|
// location.pathname. file:// won't render at all (online-only). To
|
|
// exercise online behavior we spin up a tiny in-process HTTP server
|
|
// for this spec so the page can be served from http://127.0.0.1:<port>
|
|
// at arbitrary paths.
|
|
|
|
import { test, expect } from '@playwright/test';
|
|
import * as http from 'http';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
const HTML_PATH = path.resolve('classifier/dist/classifier.html');
|
|
|
|
let server;
|
|
let baseUrl;
|
|
|
|
test.beforeAll(async () => {
|
|
const html = fs.readFileSync(HTML_PATH, 'utf8');
|
|
server = http.createServer((req, res) => {
|
|
// Serve the same classifier HTML at every path. The strip's
|
|
// detection logic uses location.pathname; the bytes don't have
|
|
// to vary.
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
res.end(html);
|
|
});
|
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
const port = server.address().port;
|
|
baseUrl = `http://127.0.0.1:${port}`;
|
|
});
|
|
|
|
test.afterAll(async () => {
|
|
if (server) await new Promise(resolve => server.close(resolve));
|
|
});
|
|
|
|
test.describe('shared/nav.js stage strip', () => {
|
|
|
|
test('does NOT render at the deployment root', async ({ page }) => {
|
|
await page.goto(`${baseUrl}/index.html`, { waitUntil: 'load' });
|
|
await page.waitForSelector('.app-header', { timeout: 5000 });
|
|
await expect(page.locator('.zddc-stage-strip')).toHaveCount(0);
|
|
});
|
|
|
|
test('renders for <project>/archive.html with archive active', async ({ page }) => {
|
|
await page.goto(`${baseUrl}/projA/archive.html`, { waitUntil: 'load' });
|
|
const strip = page.locator('.zddc-stage-strip');
|
|
await expect(strip).toHaveCount(1);
|
|
await expect(strip.locator('.zddc-stage-strip__project')).toHaveText('projA');
|
|
|
|
const stages = await strip.locator('.zddc-stage').allTextContents();
|
|
expect(stages).toEqual(['Archive', 'Working', 'Staging', 'Reviewing']);
|
|
|
|
const active = strip.locator('.zddc-stage--active');
|
|
await expect(active).toHaveCount(1);
|
|
await expect(active).toHaveText('Archive');
|
|
await expect(active).toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
test('renders for <project>/working/foo/mdedit.html with working active', async ({ page }) => {
|
|
await page.goto(`${baseUrl}/projA/working/casey/mdedit.html`, { waitUntil: 'load' });
|
|
const active = page.locator('.zddc-stage-strip .zddc-stage--active');
|
|
await expect(active).toHaveText('Working');
|
|
});
|
|
|
|
test('stage links point to the canonical <project>/<stage>/ URLs', async ({ page }) => {
|
|
await page.goto(`${baseUrl}/projA/staging/`, { waitUntil: 'load' });
|
|
await page.waitForSelector('.zddc-stage-strip');
|
|
|
|
const links = await page.evaluate(() => {
|
|
const xs = document.querySelectorAll('.zddc-stage-strip .zddc-stage');
|
|
return Array.from(xs).map(a => ({ text: a.textContent, href: a.getAttribute('href') }));
|
|
});
|
|
expect(links).toEqual([
|
|
{ text: 'Archive', href: '/projA/archive' },
|
|
{ text: 'Working', href: '/projA/working' },
|
|
{ text: 'Staging', href: '/projA/staging' },
|
|
{ text: 'Reviewing', href: '/projA/reviewing' },
|
|
]);
|
|
});
|
|
|
|
test('mounts immediately above the app-header', async ({ page }) => {
|
|
await page.goto(`${baseUrl}/projA/archive.html`, { waitUntil: 'load' });
|
|
const prev = await page.evaluate(() => {
|
|
const h = document.querySelector('.app-header');
|
|
return h && h.previousElementSibling && h.previousElementSibling.className;
|
|
});
|
|
expect(prev).toContain('zddc-stage-strip');
|
|
});
|
|
|
|
});
|