import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; // A bound that has to hold in two languages at once. The backend truncates a user-supplied display // name to `DISPLAY_TEXT_MAX` on its way into a varchar column of the same width (the columns are // declared with it); the upload dialog stops the user typing at the same number, so the truncation // is never a silent surprise. Neither side can see the other's copy, so the gate reads both. // // Widening one side alone is the failure this catches: the column and the Python constant move // together, the input keeps clipping at the old number, and the field simply stops accepting // keystrokes with no message — visible only by reading two files in two languages. // // Read as TEXT, like `i18n/backendIds.contract.test.ts` does, rather than by importing the module: // the api slice pulls in i18n, which touches localStorage at load and would drag a DOM into a check // that is pure string comparison. const src = (u: string) => readFileSync(new URL(u, import.meta.url), "utf-8"); describe("display-name bound", () => { it("matches the backend's DISPLAY_TEXT_MAX", () => { const backend = src("../../../../backend/app/models.py").match( /^DISPLAY_TEXT_MAX\s*=\s*(\d+)$/m, ); const frontend = src("./downloads.ts").match(/^export const DISPLAY_NAME_MAX = (\d+);$/m); expect(backend, "backend/app/models.py no longer defines DISPLAY_TEXT_MAX").toBeTruthy(); expect(frontend, "lib/api/downloads.ts no longer exports DISPLAY_NAME_MAX").toBeTruthy(); expect(Number(frontend![1])).toBe(Number(backend![1])); }); it("is the width the columns are actually declared with", () => { // The mutation guard for the check above: both constants could agree on a number neither // column uses. `String(DISPLAY_TEXT_MAX)` is how the three display columns are declared, so a // literal creeping back into one of them fails here. `User.display_name` is a core table; the // job's two are Capture's. const models = src("../../../../backend/app/models.py") + src("../../../../backend/app/downloads/models.py"); const declared = [...models.matchAll(/display_(?:name|uploader): Mapped.*?String\((.*?)\)/g)]; expect(declared).toHaveLength(3); for (const m of declared) expect(m[1]).toBe("DISPLAY_TEXT_MAX"); }); }); // The viewer reads what the server decides (backend downloads/preview.py): the preview kinds it may // be told and the header that says a text was cut. Each side names them in its own language, so the // gate reads both: a kind the server adds that the viewer does not know would fall to "no preview". describe("file preview", () => { it("knows the kinds the server hands out", () => { const backend = src("../../../../backend/app/downloads/preview.py").match( /^PreviewKind = Literal\[(.*)\]$/m, ); const frontend = src("./downloads.ts").match(/^export type FilePreviewKind = (.*);$/m); expect(backend, "downloads/preview.py no longer defines PreviewKind").toBeTruthy(); expect(frontend, "lib/api/downloads.ts no longer exports FilePreviewKind").toBeTruthy(); const kinds = (s: string, sep: string) => s .split(sep) .map((k) => k.trim().replace(/"/g, "")) .sort(); expect(kinds(frontend![1]!, "|")).toEqual(kinds(backend![1]!, ",")); }); it("reads the header the server cuts a text with", () => { const backend = src("../../../../backend/app/routes/files.py").match( /^TRUNCATED_HEADER = "(.*)"$/m, ); const frontend = src("./files.ts").match(/^const PREVIEW_TRUNCATED_HEADER = "(.*)";$/m); expect(backend, "routes/files.py no longer defines TRUNCATED_HEADER").toBeTruthy(); expect(frontend, "lib/api/files.ts no longer defines PREVIEW_TRUNCATED_HEADER").toBeTruthy(); expect(frontend![1]).toBe(backend![1]); }); });