A tab names the account it was claimed for on every request, beacon and upload (X-Siftlode-Expect-Account); current_user and /auth/logout refuse a mismatch with 401 account_changed and keep the session, so a tab left showing the account signed in before reloads instead of writing to the new one. /auth/login clears a connect marker left by an abandoned connect, and a link completes only on a session that still holds. The prefs merge reads the row under a lock. A tab's sessionStorage records its own owner. A page the account loses is given up, address bar and remembered page included. The panel layouts are preferences; the storage contract also refuses a key handed to the Storage API or to usePref outside its kind. A refused sign-in as someone else and a failed sign-out say so. The chromium-only e2e specs are in the gate register, the member walk waits for its requests, and devices.spec checks the save itself.
77 lines
3.6 KiB
TypeScript
77 lines
3.6 KiB
TypeScript
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, and test_migration_0098.py tests the removal itself, so both name 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",
|
|
"backend/tests/test_migration_0098.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", () => {
|
|
// Reads every source file of the repo: ~0.4 s idle, past 5 s on a busy machine's disk and CPU.
|
|
it("leaves no trace of the demo account", { timeout: 30_000 }, () => {
|
|
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([]);
|
|
});
|
|
|
|
// One account per browser session (2026-10-20): the per-tab account pin, the header and `?account=`
|
|
// that carried it, and the account list and switch went. A request naming an account would now
|
|
// be ignored by the server, so a pin left behind reads as working while it does nothing. Only the
|
|
// claim that clears the old pin keys (storage.ts) and its test may name them. (The tab's
|
|
// X-Siftlode-Expect-Account is not a pin: it never picks an account, the server refuses a request
|
|
// from a tab showing another one than the cookie's.)
|
|
it("leaves no per-tab account pin, header or switch", { timeout: 30_000 }, () => {
|
|
const PIN =
|
|
/X-Siftlode-Account|activeAccount|nextAccount|[?&]account=|switchAccount|\/api\/me\/(accounts|switch)/i;
|
|
const allowed = new Set([
|
|
"frontend/src/lib/storage.ts",
|
|
"frontend/src/lib/localData.dom.test.ts",
|
|
]);
|
|
const hits = ["frontend/src", "frontend/e2e"]
|
|
.flatMap(files)
|
|
.filter((f) => !allowed.has(f) && f !== "frontend/src/lib/removedFeatures.contract.test.ts")
|
|
.flatMap((f) =>
|
|
readFileSync(new URL(f, REPO), "utf-8")
|
|
.split("\n")
|
|
.flatMap((line, i) => (PIN.test(line) ? [`${f}:${i + 1}: ${line.trim()}`] : [])),
|
|
);
|
|
expect(hits).toEqual([]);
|
|
});
|
|
});
|