feat!: remove the shared demo account
The demo account — one shared user that whitelisted emails entered from the login page, reset on a schedule — goes end to end (owner decision, E34): demo.py and the reset job, users.is_demo and demo_whitelist (migration 0098 deletes the demo user, its audit rows, the reset job's override and notices, then drops the column and table; the downgrade restores the empty schema), the /auth/demo entry, the admin whitelist and reset, the Demo tab, the banner, the login page's "Try the demo", and every is_demo branch in routes, userscope, scheduler and the UI. require_human only ever refused the demo account, so its 87 uses become current_user (owner-approved); require_write_scope keeps its own check. The login page's "or" divider now shows only beside the Google button: without the demo entry under it, an instance without Google would have said "or" to nothing. A contract test (removedFeatures) fails on the word anywhere in the backend, frontend, e2e or scripts — seen red on a planted hit. Size ceilings tightened for the modules that shrank; admin.py is back under the limit.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
// The shared demo account was removed (the owner's decision, 2026-10-04): its user, its whitelist, its
|
||||
// login entry, and every `is_demo` branch that kept it from quota, YouTube, downloads and the debug
|
||||
// recorder. A guard left behind is not harmless — it reads as a live rule, and the next reader keeps
|
||||
// it alive by copying it — so this fails on the word anywhere in the code a feature could live in:
|
||||
// the backend (app + tests), the frontend (src + e2e) and the repo scripts.
|
||||
//
|
||||
// Not scanned: the Alembic migrations (0014 added the account and 0098 removed it, and both stay true
|
||||
// as history) and the release notes, which record what shipped when. `audit_metrics.py` counts the
|
||||
// footprint by this same word, so it names it on purpose.
|
||||
|
||||
const REPO = new URL("../../../", import.meta.url);
|
||||
const ROOTS = ["backend/app", "backend/tests", "frontend/src", "frontend/e2e", "scripts"];
|
||||
const SKIP_DIRS = new Set(["node_modules", "__pycache__", "dist", ".speed", "release-notes"]);
|
||||
const EXTENSIONS = /\.(py|ts|tsx|js|mjs|json|css|md|sh)$/;
|
||||
const ALLOWED = new Set([
|
||||
"frontend/src/lib/removedFeatures.contract.test.ts",
|
||||
"scripts/audit_metrics.py",
|
||||
]);
|
||||
// "demo" as a word or an identifier part (`is_demo`, `DemoBanner`, `tryDemo`) — not "demote" (a role
|
||||
// change) and not "Demokratikus" (a country's Hungarian name in the vocab).
|
||||
const DEMO = /demo(?!t(e|ion)|kr)/i;
|
||||
|
||||
function files(dir: string): string[] {
|
||||
return readdirSync(new URL(dir, REPO), { withFileTypes: true }).flatMap((e) =>
|
||||
e.isDirectory()
|
||||
? SKIP_DIRS.has(e.name)
|
||||
? []
|
||||
: files(`${dir}/${e.name}`)
|
||||
: EXTENSIONS.test(e.name)
|
||||
? [`${dir}/${e.name}`]
|
||||
: [],
|
||||
);
|
||||
}
|
||||
|
||||
describe("removed features stay removed", () => {
|
||||
it("leaves no trace of the demo account", () => {
|
||||
const hits = ROOTS.flatMap(files)
|
||||
.filter((f) => !ALLOWED.has(f))
|
||||
.flatMap((f) =>
|
||||
readFileSync(new URL(f, REPO), "utf-8")
|
||||
.split("\n")
|
||||
.flatMap((line, i) => (DEMO.test(line) ? [`${f}:${i + 1}: ${line.trim()}`] : [])),
|
||||
);
|
||||
expect(hits).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user