The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
92 lines
3.9 KiB
TypeScript
92 lines
3.9 KiB
TypeScript
import { test as setup, expect } from "@playwright/test";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdirSync, readFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
/**
|
|
* One-time setup for the characterization net (the `setup` project the tests depend on):
|
|
* 1. preflight — the localdev :8080 stack must be up (owned by `siftlode dev`);
|
|
* 2. seed — reset the dedicated E2E account to a known baseline, in-container (idempotent);
|
|
* 3. authenticate ONCE via password-login and persist the session + `en` locale to
|
|
* storageState, so the real specs never re-auth (auth endpoints are rate-limited).
|
|
*/
|
|
|
|
const STORAGE = "e2e/.auth/state.json";
|
|
const EMAIL = process.env.E2E_EMAIL || "e2e@siftlode.local";
|
|
// No default: a password committed to a public repo is not a credential. The ops wrapper
|
|
// (`siftlode e2e` / `check-full`) exports it from the secret store.
|
|
const PASSWORD = process.env.E2E_PASSWORD ?? "";
|
|
const CONTAINER = process.env.E2E_API_CONTAINER || "siftlode-api-1";
|
|
const BASE_URL = process.env.PW_BASE_URL || "http://127.0.0.1:8080";
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const SEED_SCRIPT = resolve(HERE, "../../scripts/e2e_seed.py");
|
|
|
|
// The in-container seed only makes sense when the target is the local stack.
|
|
const isLocalTarget = /\/\/(127\.0\.0\.1|localhost)[:/]/.test(BASE_URL);
|
|
|
|
setup("seed + authenticate", async ({ page }) => {
|
|
expect(
|
|
PASSWORD,
|
|
"E2E_PASSWORD is not set — run the suite through `siftlode e2e` (it reads the secret store)",
|
|
).not.toBe("");
|
|
|
|
// 1. Preflight: fail fast (with a useful message) if the stack isn't running.
|
|
const health = await page.request.get("/healthz");
|
|
expect(
|
|
health.ok(),
|
|
`localdev stack not reachable at ${BASE_URL} — start it with \`siftlode dev\``,
|
|
).toBeTruthy();
|
|
|
|
// 2. Idempotent seed inside the api container (skippable / local-only).
|
|
if (!process.env.E2E_SKIP_SEED && isLocalTarget) {
|
|
try {
|
|
const out = execFileSync(
|
|
"docker",
|
|
[
|
|
"exec",
|
|
"-i",
|
|
"-e",
|
|
`E2E_EMAIL=${EMAIL}`,
|
|
"-e",
|
|
`E2E_PASSWORD=${PASSWORD}`,
|
|
CONTAINER,
|
|
"python",
|
|
"-",
|
|
],
|
|
{ input: readFileSync(SEED_SCRIPT) },
|
|
);
|
|
console.log(`[e2e seed] ${out.toString().trim()}`);
|
|
} catch (err: unknown) {
|
|
const e = err as { stderr?: Buffer; message?: string };
|
|
throw new Error(`E2E seed failed: ${e.stderr?.toString() || e.message}`);
|
|
}
|
|
}
|
|
|
|
// 3. Authenticate. page.request shares the browser context's cookie jar, so the `session`
|
|
// cookie it receives is captured by storageState below.
|
|
const login = await page.request.post("/auth/password-login", {
|
|
data: { email: EMAIL, password: PASSWORD },
|
|
});
|
|
expect(login.ok(), "password-login failed — is the E2E account seeded?").toBeTruthy();
|
|
|
|
const me = await (await page.request.get("/api/me")).json();
|
|
expect(me.authenticated, "session not established after login").toBeTruthy();
|
|
|
|
// 4. Pin the UI locale, suppress the first-login onboarding wizard (a full-screen overlay
|
|
// that would intercept every click — the test account has no YouTube connection, so it
|
|
// auto-opens), and opt the account out of starting on Home. A bare URL lands on
|
|
// Home, and the feed, channel and player flows this net locks all open "/" expecting the
|
|
// feed; the opt-out is the one Settings → Appearance → Startup writes. Both flags are
|
|
// per-account: `siftlode.onboarding.dismissed.<id>`, `siftlode.startOnHome.<id>`.
|
|
await page.goto("/");
|
|
await page.evaluate((id: number) => {
|
|
localStorage.setItem("siftlode.lang", "en");
|
|
localStorage.setItem(`siftlode.onboarding.dismissed.${id}`, "1");
|
|
localStorage.setItem(`siftlode.startOnHome.${id}`, "0");
|
|
}, me.id);
|
|
mkdirSync(dirname(STORAGE), { recursive: true });
|
|
await page.context().storageState({ path: STORAGE });
|
|
});
|