Replaces the legacy Bureau CMS stack (@bureaudouble/bureau, deno-sqlite-blob, Fresh-style RSC) with the fleet layout: a defineProgram schema over the legacy tables (rows carry over by id), the timeline-selected blob database (autokit.sqlite), the operator CLI and mf predeploy, the package-shipped admin with a plugin per table, and the /api rpc/mcp/openapi door. The pages still render as server components, wired like wtfutures: one Autokit session per process, the request's db minted once in a createStart request middleware, read as context.db by each page's server function and handed to the server components as a db prop; the rsc graph never imports the session module (tests/one-session.test.ts). French stays at / and English at /en, now through Autokit's locales: the *_en columns became the translationsJson bag beside their French base text. The integer order columns of persons, roles and resources became order keys (drag-to-reorder in the admin); media columns are stored-media lists resolved through autokit-media. The age gate is the same shared page for every visitor: a head script marks a visitor who already answered, before the first paint.
109 lines
4.2 KiB
TypeScript
109 lines
4.2 KiB
TypeScript
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
|
|
import { readdirSync, readFileSync } from "node:fs";
|
|
import { createServer, type Server } from "node:http";
|
|
import { join } from "node:path";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
|
|
|
|
const root = join(import.meta.dirname, "..");
|
|
const scripts = (directory: string): string[] =>
|
|
readdirSync(join(root, directory), { recursive: true, encoding: "utf8" })
|
|
.filter((file) => file.endsWith(".js"))
|
|
.map((file) => readFileSync(join(root, directory, file), "utf8"));
|
|
|
|
// String literals that survive minification: the session module's own (`src/autokit/server.ts`)
|
|
// and its executor's.
|
|
const sessionModule = "sharp.elias.systems";
|
|
const sessionMarkers = [
|
|
sessionModule,
|
|
"createAutokitSession",
|
|
"createSqliteExecutor",
|
|
"compress:`zstd`",
|
|
];
|
|
|
|
describe("the built server (run after `vp build`)", () => {
|
|
it("boots the app's session once, in the SSR graph: the rsc graph reaches none of it", () => {
|
|
for (const text of scripts("dist/server/rsc")) {
|
|
for (const marker of sessionMarkers) expect(text).not.toContain(marker);
|
|
}
|
|
expect(
|
|
scripts("dist/server/assets").filter((text) => text.includes(sessionModule)),
|
|
).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
// The page cache, end to end, against a rehearsal database the test may write to (a `sites`
|
|
// commit that rewrites the title it holds): `ONE_SESSION_TIMELINE=<branch>` (`db branch create`).
|
|
const timeline = process.env["ONE_SESSION_TIMELINE"];
|
|
|
|
describe.runIf(timeline !== undefined)("a page rendered through server components", () => {
|
|
const purges: string[] = [];
|
|
let stub: Server;
|
|
let server: ChildProcess;
|
|
let origin: string;
|
|
|
|
beforeAll(async () => {
|
|
stub = createServer((request, response) => {
|
|
if (request.method === "PURGE") purges.push(String(request.headers["surrogate-key"]));
|
|
response.end();
|
|
});
|
|
await new Promise<void>((resolve) => stub.listen(0, "127.0.0.1", resolve));
|
|
const address = stub.address();
|
|
if (address === null || typeof address === "string") throw new Error("stub has no port");
|
|
execFileSync("vp", ["build", "--config", "tests/one-session.vite.config.ts"], {
|
|
cwd: root,
|
|
stdio: "ignore",
|
|
});
|
|
server = spawn(
|
|
"deno",
|
|
["serve", "-A", "--env", "--port", "0", ".output/one-session/server/one-session-entry.js"],
|
|
{
|
|
cwd: root,
|
|
env: {
|
|
...process.env,
|
|
MF_TIMELINE: timeline,
|
|
ONE_SESSION_PURGE_URL: `http://127.0.0.1:${address.port}/`,
|
|
},
|
|
stdio: ["ignore", "inherit", "pipe"],
|
|
},
|
|
);
|
|
origin = await new Promise<string>((resolve) => {
|
|
server.stderr?.on("data", (chunk: Buffer) => {
|
|
const listening = /Listening on http:\/\/[^:]+:(\d+)/.exec(String(chunk));
|
|
if (listening) resolve(`http://127.0.0.1:${listening[1]}`);
|
|
});
|
|
});
|
|
}, 300_000);
|
|
|
|
afterAll(() => {
|
|
server.kill();
|
|
stub.close();
|
|
});
|
|
|
|
it("is shared, tagged by the reads its server components made", async () => {
|
|
const response = await fetch(`${origin}/`);
|
|
await response.text();
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("cdn-cache-control")).toBeTruthy();
|
|
expect(response.headers.get("etag")).toBeTruthy();
|
|
const tags = response.headers.get("surrogate-key")?.split(/,\s*/) ?? [];
|
|
expect(tags).toEqual(expect.arrayContaining(["autokit", "t:sites"]));
|
|
expect(tags.some((tag) => tag.startsWith("u:"))).toBe(true);
|
|
}, 60_000);
|
|
|
|
it("is not shared with a signed-in request", async () => {
|
|
const response = await fetch(`${origin}/`, {
|
|
headers: { cookie: "autokit-auth.session_token=x" },
|
|
});
|
|
await response.text();
|
|
expect(response.status).toBe(200);
|
|
expect(response.headers.get("cdn-cache-control")).toBeNull();
|
|
expect(response.headers.get("surrogate-key")).toBeNull();
|
|
}, 60_000);
|
|
|
|
it("is purged by a commit on a table it read", async () => {
|
|
const response = await fetch(`${origin}/__one-session/commit`, { method: "POST" });
|
|
expect(response.status).toBe(200);
|
|
await response.text();
|
|
expect(purges.some((tags) => tags.split(/,\s*/).includes("t:sites"))).toBe(true);
|
|
}, 60_000);
|
|
});
|