Partial: Astro Starlight config, sync-content.mjs + 6 passing tests. Missing: pages (index, disclaimer, about), harness page, lockfile, verified build. See SDD ledger. Co-Authored-By: Claude Opus 5.5 <[email protected]>
96 lines
4.6 KiB
JavaScript
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\}` <details><summary>S<\/summary> 1 < 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"]);
|
|
});
|