pi-mono/packages/chord/test/node/reload/base.test.ts
Mario Zechner edc9c69892 feat(chord): the reload suite, kit timeouts and base marks, four reload fixes
An end-to-end reload suite (a kit app, a worker, a real Chromium page; one test per row of design's reload table).
The kit gains timeouts and base (base bundles' inputs and worker entries mark base). Fixes: a call crossing a failed
or a removal gets the cause, not not_ready; a call sent with an old method index is decoded by the names its consumer
had until the new bound is acked; a page retries a failed script fetch; applyChanges runs one at a time.
2026-11-04 16:47:07 +00:00

83 lines
3.7 KiB
TypeScript

// Reload suite, row "base code (the app's client entry, Chord, React)" (design §21 "What a user sees during a
// reload"): the page reloads, the only case losing what is on screen; calls in flight end with the link; page state is
// lost, server state unchanged. The base bundle's entry changes on disk, the watcher flags base code, and the app
// restarts its hosts (the kit's answer to base code) on the same data directory and port.
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { BACKGROUND_CONTEXT as context } from "../../../src/context/index.ts";
import { latches, ok, ReloadPage, startReloadApp, versionedMain } from "../fixtures/reload/app.ts";
import { counterPlugin, LATCH_PLUGIN, webPlugin } from "../fixtures/reload/plugins.ts";
import { Browser, CHROMIUM, poll } from "../web/browser.ts";
/** The counter as the web plugins show it. */
const COUNTER = { placement: "worker", contract: "v1" } as const;
describe.skipIf(CHROMIUM === undefined)("a change of base code", () => {
let browser: Browser;
let page: ReloadPage;
beforeAll(async () => {
browser = await Browser.start();
page = new ReloadPage(browser);
}, 120_000);
afterAll(async () => {
await browser?.close();
});
it("reloads the page, ends calls in flight with the link, loses page state and keeps server state", async () => {
const entry = await versionedMain("1");
const first = await startReloadApp(
{
latch: LATCH_PLUGIN,
counter: counterPlugin({ code: "v1", placement: "worker" }),
alpha: webPlugin({ name: "alpha", code: "1", counter: COUNTER }),
},
{ entry },
);
let second: Awaited<ReturnType<typeof startReloadApp>> | undefined;
try {
await page.signIn(first, ["alpha"]);
await page.waitText("alpha", ".state", "live");
expect(await page.result(await page.call("alpha", "add", 2))).toEqual(ok("v1:2"));
await page.waitText("alpha", ".label", "v1:2");
await page.type("alpha", ".kept", "kept");
await browser.evaluate("globalThis.marked = true");
const hand = await latches(first, "worker");
const running = await page.call("alpha", "slowAdd", "running");
await hand.entered("running");
// Base code changes; the app restarts its hosts.
await versionedMain("2", join(entry, ".."));
await first.app.close(context);
// The call in flight ends with the link.
expect(await page.result(running)).toMatchObject({ status: "error" });
second = await startReloadApp({}, { entry, port: first.port, dataDir: first.dataDir, root: first.root });
expect(second.app.web!.base).not.toBe(first.app.web!.base);
// The page reloads for the new base bundle: what was on screen and in page state is gone.
await browser.wait('globalThis.version === "2" && globalThis.page?.connection === "connected"', 60_000);
expect(await browser.evaluate("globalThis.marked ?? false")).toBe(false);
await page.waitText("alpha", ".state", "live");
expect(await page.value("alpha", ".kept")).toBe("");
// Server state unchanged: the count as committed before (the cut call never committed).
expect(await page.text("alpha", ".label")).toBe("v1:2");
expect(await page.result(await page.call("alpha", "visits"))).toEqual(ok(1));
} finally {
await first.app.close(context);
await second?.app.close(context);
}
}, 120_000);
it("flags a change of the web entry as base code (`plugins.state.base`)", async () => {
const entry = await versionedMain("1");
const app = await startReloadApp({ alpha: webPlugin({ name: "alpha", code: "1" }) }, { entry });
try {
await versionedMain("2", join(entry, ".."));
await poll(() => app.app.plugins.state.value.base, 5000, "base code flagged");
} finally {
await app.app.close(context);
}
}, 60_000);
});