Files
Lucas WintherandClaude Opus 5.5 699d038d1c feat: course website (Astro Starlight)
Pages (index, disclaimer, about), lockfile, root test:site script, fix for
Starlight 0.42 sidebar config and <details> rendering in generated MDX.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 18:35:29 +01:00

96 lines
4.6 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { syncContent, pairCodeTabs, mdxSafeProse, slugify, BLOB_BASE } from "./sync-content.mjs";
function fixture(lessons, extra = {}) {
const root = mkdtempSync(join(tmpdir(), "sync-"));
for (const [dir, md] of Object.entries(lessons)) {
mkdirSync(join(root, "course", dir), { recursive: true });
writeFileSync(join(root, "course", dir, "lesson.md"), md);
}
for (const [f, md] of Object.entries(extra)) {
mkdirSync(join(root, f, ".."), { recursive: true });
writeFileSync(join(root, f), md);
}
return root;
}
const run = (root) => {
const out = join(root, "out");
syncContent({ repoRoot: root, outDir: out });
return (f) => readFileSync(join(out, f), "utf8");
};
test("sync_rewrites_and_checks_links", () => {
const root = fixture(
{
"01-a": "# Module 1: A\n\nSee [two](../02-x/lesson.md) and [anchor](../02-x/lesson.md#expansion-drop), [tool](../../tools/t.mjs), [sc](./scenarios.md), [ext](https://example.com/a).\n",
"02-x": "# Module 2: X\n\n## Expansion Drop\n\nback to [one](../01-a/lesson.md).\n",
},
{ "tools/t.mjs": "//", "course/01-a/scenarios.md": "# s\n" },
);
const page = run(root);
const one = page("01-a.mdx");
assert.match(one, /title: "Module 1: A"/);
assert.match(one, /order: 1/);
assert.doesNotMatch(one, /^# Module/m);
assert.match(one, /\[two\]\(\/course\/02-x\/\)/);
assert.match(one, /\[anchor\]\(\/course\/02-x\/#expansion-drop\)/);
assert.match(one, new RegExp(`\\[tool\\]\\(${BLOB_BASE}tools/t\\.mjs\\)`));
assert.match(one, new RegExp(`\\[sc\\]\\(${BLOB_BASE}course/01-a/scenarios\\.md\\)`));
assert.match(one, /\(https:\/\/example\.com\/a\)/);
const dead = fixture({ "01-a": "# A\n\n[gone](../09-none/lesson.md)\n" });
assert.throws(() => run(dead), /dead link/);
const deadFile = fixture({ "01-a": "# A\n\n[gone](./missing.md)\n" });
assert.throws(() => run(deadFile), /dead link/);
});
test("sync_rejects_dead_anchors", () => {
const cross = fixture({ "01-a": "# A\n\n[x](../02-x/lesson.md#nope)\n", "02-x": "# X\n\n## Real\n" });
assert.throws(() => run(cross), /dead anchor/);
const own = fixture({ "01-a": "# A\n\n## Play it\n\n[ok](#play-it) [bad](#play-it-now)\n" });
assert.throws(() => run(own), /dead anchor #play-it-now/);
});
test("slugify_matches_heading_ids", () => {
assert.equal(slugify("Hint 1 — nudge"), "hint-1--nudge");
assert.equal(slugify("The `trap` in the wild"), "the-trap-in-the-wild");
});
const FENCE = "```";
test("sync_pairs_code_tabs", () => {
const md = `intro\n\n${FENCE}rust\nfn a() {}\n${FENCE}\n${FENCE}ts\nfunction a() {}\n${FENCE}\n\nlone:\n\n${FENCE}rust\nfn b() {}\n${FENCE}\n\ntext\n\n${FENCE}sh\nx\n${FENCE}\n${FENCE}sh\ny\n${FENCE}\n`;
const { md: out, used } = pairCodeTabs(md);
assert.ok(used);
assert.equal(out.match(/<Tabs syncKey="lang">/g).length, 1);
assert.match(out, /<TabItem label="Rust">\n\n```rust\nfn a\(\) \{\}\n```\n\n<\/TabItem>\n<TabItem label="TypeScript">\n\n```ts\nfunction a/);
assert.match(out, /lone:\n\n```rust\nfn b/); // lone block untouched
assert.match(out, /```sh\nx\n```\n```sh\ny\n```/); // adjacent non-rust/ts blocks untouched
const root = fixture({ "01-a": `# A\n\n${FENCE}rust\nr\n${FENCE}\n${FENCE}ts\nt\n${FENCE}\n` });
assert.match(run(root)("01-a.mdx"), /^import \{ Tabs, TabItem \} from "@astrojs\/starlight\/components";$/m);
const none = fixture({ "01-a": `# A\n\n${FENCE}rust\nr\n${FENCE}\n` });
assert.doesNotMatch(run(none)("01-a.mdx"), /import/);
});
test("sync_strips_comments_and_escapes_mdx", () => {
const out = mdxSafeProse("a <!-- Task 17: re-verify --> b {x} and `{kept}` <details><summary>S</summary> 1 < 2");
assert.doesNotMatch(out, /<!--/);
assert.match(out, /a b \\\{x\\\} and `\{kept\}`\n\n<details>\n\n<summary>S<\/summary>\n\n1 &lt; 2/);
const root = fixture({ "01-a": `# A\n\n<!-- hidden -->\n\n${FENCE}rust\nif a < b { <!-- keep --> }\n${FENCE}\n` });
const page = run(root)("01-a.mdx");
assert.doesNotMatch(page, /hidden/);
assert.match(page, /if a < b \{ <!-- keep --> \}/); // code untouched
});
test("sync_writes_harness_and_ignores_missing_dirs", () => {
const root = fixture({ "01-a": "# A\n\n[h](../harness.md)\n" }, { "course/harness.md": "# Harness contract\n" });
const page = run(root);
assert.match(page("01-a.mdx"), /\(\/course\/harness\/\)/);
assert.match(page("harness.mdx"), /title: "Harness contract"/);
assert.deepEqual(readdirSync(join(root, "out")).sort(), ["01-a.mdx", "harness.mdx"]);
});