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); });