§10 gate the dashboard + make "/" replaceable by a plugin (todo §10); "/" is now gated to a signed-in session (anonymous → /login via loginRedirect, query preserved as return_to) and fully replaceable via a new optional home?: RouteHandler on PluginManifest — a handler with the same signature as any route (the most ergonomic shape). The app.ts "/" branch gates first, then renders the single home plugin's handler against its own views/ with the native shell via ctx.chrome (HEAD / void-return / response-hook parity with a plugin route), else the built-in mock-data People list. home mounts at the root above the /<id> namespace, so it can't shadow or be shadowed by a built-in route. Single-slot + loud: findConflicts errors on >1 home (new "home" kind), discovery rejects a non-function home — never last-write-wins. Tests-first (338 → 344 units): app.test.ts gate + home-override; plugin.test.ts home conflict; discovery.test.ts home validation. Docs: plugin-contract.md (manifest table + "The dashboard (home)" section + conflict row), README. E2E: visual.spec plants a dev-signed session (the anonymous plugin-gate probe uses the cookie-free request fixture); all e2e web/gateway healthchecks repointed from the gated "/" to /public/css/styles.css. stability-reviewer: APPROVE, no Critical/High/Medium. typecheck + 344 units + visual(9) + full-flow(7) E2E green.

This commit is contained in:
2026-06-20 17:18:30 +02:00
parent df53106a5a
commit 2eb5b84ccf
14 changed files with 192 additions and 41 deletions

View File

@@ -99,3 +99,11 @@ test("findConflicts: duplicate nav id is an error, a shared permission token onl
]);
assert.ok(permDup.some((c) => c.kind === "permission" && c.level === "warn"));
});
test("findConflicts: only one plugin may claim the dashboard (`home`) — two is a loud error (§10)", () => {
const home = () => ({ html: "dash" });
const dup = findConflicts([p({ id: "a", home }), p({ id: "b", home })]);
assert.ok(dup.some((c) => c.kind === "home" && c.level === "error" && c.plugins.includes("a") && c.plugins.includes("b")));
// One home (or none) is fine.
assert.deepEqual(findConflicts([p({ id: "a", home }), p({ id: "b" })]).filter((c) => c.kind === "home"), []);
});