Files
siftlode/frontend/src/lib/pythonEngine.contract.test.ts
T
peter 378b7559e4 build(frontend): finish the React 19 move, and hold the versions that were left in prose
react-i18next was still on ^14.1.2. The E27 S1 plan named it as "the one peer that
actually needs moving — React 19 support is the 15 line", and then it did not
move. The 14 line's <Trans> reads `element.ref`, removed in React 19, and its
withTranslation types are built on the GLOBAL JSX namespace @types/react 19 no
longer declares — both latent only because this app imports neither. 15.7.4 fixes
both at the source (`c.props.ref ?? c.ref`, `React.JSX.LibraryManagedAttributes`).

`reactEngine.contract.test.ts` joins its Node and Python twins and holds what was
only a comment: the compiler's `target` against the installed React major, react
and react-dom together, the types against the runtime, and react-i18next's floor.
A stale target is not a corrupt bundle — the plugin validates the option — but it
is a silent one, since tsc never sees Babel's output.

pythonEngine gains the seventh spelling nobody held: `requirements.lock`'s own
regeneration recipe names a python image, and wheels resolve against THAT
interpreter, so a recipe left behind yields a lock that does not match the image
installing it.

The type lane now covers `alembic` too, for the reason the ruff lane was widened
to it — migrations run on every container start, so one that raises crash-loops
prod. It came back at zero new errors, so the baseline is untouched. `tests/` is
deliberately still out, and the config says why.

vitest.config's docblock claimed the React transform is "a no-op for the plain .ts
node tests". It is not: the compiler rewrites src/lib's hooks in that lane too,
which is the point of sharing one factory with the build — and worth knowing
before ruling the compiler out of a divergence.

Tests: useCardPager had none, in either the hook or its two callers, though its
page clamp moved to render in S2 — the transformation where a non-convergent
condition is a crash rather than a glitch. Four cases now, including termination
on an empty list. Combobox's and Avatar's close/failed-URL cases are relabelled
CHARACTERIZATION: both pass against the pre-sweep code, and the tests beside them
are the ones that discriminate.
2026-09-21 23:33:02 +02:00

101 lines
4.3 KiB
TypeScript

import { readFileSync } from "node:fs";
import { expect, it } from "vitest";
// The Python major is a contract, not a coincidence — the host half. The twin of
// `nodeEngine.contract.test.ts`, and written the day after that one was: `0.93.2` shipped because
// four machines disagreed about Node, and the Python side turned out to have the identical shape
// with nobody watching. Prod (`Dockerfile`), the test image (`backend/Dockerfile.test`) and CI
// (`check.yml`) each spelled 3.12 as its own literal, there was no `.python-version`, and the dev
// machine had only 3.13 and 3.14 installed — so every ad-hoc host script ran two majors ahead of
// production.
//
// It lives in the FRONTEND suite for one boring reason: this is the only runner that can see the
// whole repository. The backend suite runs inside the test image, which mounts `backend/` alone, so
// it cannot read the repo-root Dockerfile or the workflow. It holds the other half of the chain —
// including "the interpreter running these tests IS the pinned one", which only it can assert:
//
// backend/tests/test_python_engine_contract.py : running interpreter == pyrightconfig == ruff.toml
// here : .python-version == pyrightconfig == ruff.toml
// == Dockerfile == Dockerfile.test == check.yml
//
// To move the version: edit `.python-version`, then follow the failures in both files.
const readRoot = (rel: string) => readFileSync(new URL(`../../../${rel}`, import.meta.url), "utf8");
function versionIn(source: string, pattern: RegExp, where: string): string {
const m = pattern.exec(source);
// A rename or a moved line lands here. Re-point the test — the contract did not go away with it.
expect(m, `${pattern} not found in ${where}`).not.toBeNull();
return `${m![1]}.${m![2]}`;
}
const pinned = versionIn(readRoot(".python-version"), /^(\d+)\.(\d+)\s*$/m, ".python-version");
it("pins a plausible Python version", () => {
// Guards against a typo'd `.python-version` silently agreeing with nothing.
expect(Number(pinned.split(".")[1])).toBeGreaterThanOrEqual(12);
expect(pinned.split(".")[0]).toBe("3");
});
it("runs the shipped backend on the pinned version", () => {
// The image our users' instance actually runs.
expect(versionIn(readRoot("Dockerfile"), /^FROM python:(\d+)\.(\d+)-slim/m, "Dockerfile")).toBe(
pinned,
);
});
it("tests on the pinned version", () => {
// The image the whole pytest suite runs in — including the backend half of this contract, which
// asserts its own `sys.version_info` against the pin.
expect(
versionIn(
readRoot("backend/Dockerfile.test"),
/^FROM python:(\d+)\.(\d+)-slim/m,
"Dockerfile.test",
),
).toBe(pinned);
});
it("runs the server gate on the pinned version", () => {
// The backend job's container. Its NODE is deliberately outside the Node contract (it only runs
// the checkout action), but its PYTHON runs our code and our tests, so it is inside this one.
expect(
versionIn(
readRoot(".gitea/workflows/check.yml"),
/^\s*container: nikolaik\/python-nodejs:python(\d+)\.(\d+)-nodejs\d+$/m,
"check.yml",
),
).toBe(pinned);
});
it("type-checks against the pinned version", () => {
// A type checker aimed at another version green-lights APIs the shipped interpreter does not have
// — the same defect as the image split, moved to check time.
expect(
versionIn(
readRoot("backend/pyrightconfig.json"),
/"pythonVersion"\s*:\s*"(\d+)\.(\d+)"/,
"pyrightconfig.json",
),
).toBe(pinned);
});
it("lints against the pinned version", () => {
expect(
versionIn(readRoot("backend/ruff.toml"), /^target-version = "py(\d)(\d+)"$/m, "ruff.toml"),
).toBe(pinned);
});
it("resolves the lock on the pinned version", () => {
// The seventh spelling, and the sneakiest: `requirements.lock`'s own regeneration recipe names an
// image. Wheels and environment markers are resolved by THAT interpreter, so a recipe left behind
// produces a lock that does not match the image installing it — the same split as the others, one
// indirection further out, and with nothing red to show for it.
expect(
versionIn(
readRoot("backend/requirements.lock"),
/docker run [^\n]*python:(\d+)\.(\d+)-slim/,
"requirements.lock",
),
).toBe(pinned);
});