import test from "node:test";
import assert from "node:assert/strict";
import { readFile, mkdtemp, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createRequire } from "node:module";
import ts from "typescript";
async function registry() {
const temp = await mkdtemp(path.join(os.tmpdir(), "lichen-canonical-registry-"));
const source = await readFile("lib/playground/experiments.ts", "utf8");
const output = ts.transpileModule(source, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 },
}).outputText;
const registryPath = path.join(temp, "experiments.cjs");
await writeFile(registryPath, output);
return createRequire(import.meta.url)(registryPath);
}
const routeFiles = [
"app/playground/[slug]/page.tsx",
"app/playground/[slug]/launch/page.tsx",
"app/playground/lichen/v2/page.tsx",
"app/playground/lichen/v2/launch/page.tsx",
"components/playground/LichenArticle.tsx",
"components/playground/StudyAlias.tsx",
"components/playground/ExperimentLaunch.tsx",
"components/playground/v2/StudyShell.tsx",
"components/playground/v2/LichenExperience.tsx",
"components/playground/v2/study.ts",
"content/playground/lichen.mdx",
];
test("registry keeps all nine experiments and points Lichen at the canonical launch", async () => {
const { experiments, getExperiment } = await registry();
assert.deepEqual(
experiments.map(({ slug }) => slug),
["small-weather", "fall-creek-cairn", "lichen-forest", "balloon", "kaleidoscope", "blue-calx", "lichen", "stone-in-focus", "midi-playground"],
);
assert.equal(getExperiment("lichen")?.launchUrl, "/playground/lichen/launch/");
assert.equal(getExperiment("lichen")?.number, "007");
});
test("canonical routes branch Lichen to the shared article and direct experience", async () => {
const [article, launch] = await Promise.all([
readFile("app/playground/[slug]/page.tsx", "utf8"),
readFile("app/playground/[slug]/launch/page.tsx", "utf8"),
]);
assert.match(article, /import LichenArticle from "\.\.\/\.\.\/\.\.\/components\/playground\/LichenArticle"/u);
assert.match(article, /path: "\/playground\/lichen\/"/u);
assert.match(article, /if \(slug === "lichen"\) return /u);
assert.match(launch, /import LichenExperience from "\.\.\/\.\.\/\.\.\/\.\.\/components\/playground\/v2\/LichenExperience"/u);
assert.match(launch, /if \(slug === "lichen"\) return /u);
// The direct experience must not fall back to the legacy iframe launcher.
assert.doesNotMatch(launch, /ExperimentLaunch[^;]*lichen/u);
});
test("canonical article keeps the gallery transition hooks and a native return anchor", async () => {
const article = await readFile("components/playground/LichenArticle.tsx", "utf8");
assert.match(article, /data-playground-experiment="lichen"/u);
assert.doesNotMatch(article, /data-playground-return/u);
const history = await readFile("lib/playground/transitions.mjs", "utf8");
assert.match(history, /nav\[aria-label=Breadcrumb\]/u);
assert.doesNotMatch(article, / {
const [article, launch, alias] = await Promise.all([
readFile("app/playground/lichen/v2/page.tsx", "utf8"),
readFile("app/playground/lichen/v2/launch/page.tsx", "utf8"),
readFile("components/playground/StudyAlias.tsx", "utf8"),
]);
assert.match(article, /aliasMetadata\('Lichen — Source Code', '\/playground\/lichen\/'\)/u);
assert.match(article, //u);
assert.match(launch, /aliasMetadata\('Lichen — Interactive study', '\/playground\/lichen\/launch\/'\)/u);
assert.match(launch, //u);
// Static-export-compatible: meta refresh plus an immediate JS replace that
// preserves the query and hash, and an accessible link fallback.
assert.match(alias, /httpEquiv="refresh"/u);
assert.match(alias, /robots: \{ index: false, follow: false \}/u);
assert.match(alias, /alternates: \{ canonical \}/u);
assert.match(alias, /location\.replace/u);
assert.match(alias, /location\.search\+location\.hash/u);
assert.match(alias, //u);
});
test("study identity is version-free and links to the canonical article", async () => {
const [experience, study, shell] = await Promise.all([
readFile("components/playground/v2/LichenExperience.tsx", "utf8"),
readFile("components/playground/v2/study.ts", "utf8"),
readFile("components/playground/v2/StudyShell.tsx", "utf8"),
]);
assert.match(experience, /number: '005'/u);
assert.doesNotMatch(experience, /001 \/ V2|originalUrl|\/playground\/lichen\/v2\//u);
assert.match(experience, /articleUrl: '\/playground\/lichen\/'/u);
assert.doesNotMatch(study, /originalUrl/u);
assert.doesNotMatch(shell, /originalUrl|Compare the original|Original ↗/u);
assert.match(shell, /href=\{study\.articleUrl\}>Source Code & field notes ↗/u);
});
test("no rendered playground surface carries version or comparison identity", async () => {
const surfaces = await Promise.all(routeFiles.map((file) => readFile(file, "utf8")));
const joined = surfaces.join("\n");
assert.doesNotMatch(joined, /\bV2\b/u, "no V2 wording should remain in rendered surfaces");
assert.doesNotMatch(joined, /Compare the original/u);
assert.doesNotMatch(joined, /Original ↗|Open the original/u);
assert.doesNotMatch(joined, /second version/iu);
assert.doesNotMatch(joined, /\/playground\/lichen\/v2\/(?:launch\/)?(?:"|')/u);
});
test("article copy describes the current study rather than a comparison", async () => {
const mdx = await readFile("content/playground/lichen.mdx", "utf8");
assert.doesNotMatch(mdx, /second version|original remains beside it|for comparison/iu);
assert.match(mdx, /now lives in the Blue Bucket application/u);
});