// node --test docs/docs.test.mjs (after `cargo wasm` and `cargo host`) // Every fenced block in docs/ is checked: // a block under a line that is only `path/in/repo` must appear in that file verbatim (lines of `…` // elide: each piece must appear, in order) // ```bash run runs (per page, one shell, in order) in a scratch // directory that links the repo's top level, with // atlibs on PATH and no ATPROTO_* in the environment // ```output right after a run block: its lines must appear in // that block's output, in order (`…` matches anything) // ```js run the same, as a module file run with node // ```js run browser the same, as a page module in headless Chrome (docs/browser.mjs: // imports atlibs/...); not run when there is no Chrome // ```html run browser [name.html] a page (written to name.html, default a scratch name), opened in // headless Chrome; its output is the text of its element // ```bash run=network runs only with ATLIBS_DOCS_NETWORK=1 // Each page's scratch directory is removed after it, unless ATLIBS_DOCS_KEEP=1 (debugging: the path is printed). // ```bash run=account | run=gate | run=container not run here: needs an account, is a gate command // | run=install (cargo …, node --test …) the full test suite runs, // needs podman/docker and the network (atlibs verify), // or installs (cargo install: into ~/.cargo/bin) // ```text help each line must appear in `atlibs help` // ``` norun config for another program (nginx, Caddyfile) // ```mermaid a diagram // Anything else fails, so no snippet goes unchecked. Also: relative links resolve, every page is in // docs/README.md, env.md lists every ATLIBS_* variable the code reads, lexicons.md every lexicon, // limits.md the run budget, and no code outside docs/ points into docs/. import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; import { existsSync, linkSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, relative } from "node:path"; import { test } from "node:test"; import { setTimeout as sleep } from "node:timers/promises"; import { BUDGET } from "../javascript/src/run-module/index.js"; import { hostBin } from "../rust/host/conformance.mjs"; import { CHROME } from "./browser.mjs"; const ROOT = new URL("../", import.meta.url).pathname; const DOCS = join(ROOT, "docs"); const NETWORK = process.env.ATLIBS_DOCS_NETWORK === "1"; const KEEP = process.env.ATLIBS_DOCS_KEEP === "1"; const walk = (dir) => readdirSync(dir, { withFileTypes: true }).flatMap((e) => (e.isDirectory() ? walk(join(dir, e.name)) : [join(dir, e.name)])); const pages = walk(DOCS).filter((f) => f.endsWith(".md")).sort(); const read = (file) => readFileSync(file, "utf8"); /** Fenced blocks: { info, lang, tags, body, line, path (the `path` line above it, if any) }. */ function blocks(text) { const lines = text.split("\n"); const out = []; for (let i = 0; i < lines.length; i++) { const open = /^```(\S*)\s*(.*)$/.exec(lines[i]); if (!open) continue; const start = i; const body = []; while (++i < lines.length && lines[i] !== "```") body.push(lines[i]); let j = start - 1; while (j >= 0 && lines[j].trim() === "") j--; const above = /^`([^`\s]+)`$/.exec(lines[j] ?? ""); out.push({ lang: open[1], tags: open[2].split(/\s+/).filter(Boolean), body: body.join("\n"), line: start + 1, path: above?.[1] }); } return out; } const runTag = (b) => b.tags.find((t) => t === "run" || t.startsWith("run=")); const elided = (body) => body.split(/^\s*(?:\/\/ |# )?…\s*$/m).map((s) => s.replace(/^\n+|\n+$/g, "")).filter(Boolean); /** Does `expected` (lines, `…` wildcards) appear in `actual`, line by line in order? */ function matches(expected, actual) { const have = actual.split("\n").map((l) => l.trimEnd()); let at = 0; for (const want of expected.split("\n").map((l) => l.trimEnd())) { if (want.trim() === "…" || want.trim() === "") continue; const re = new RegExp(`^${want.split("…").map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`); while (at < have.length && !re.test(have[at])) at++; if (at === have.length) return `missing: ${want}`; at++; } return null; } /** A scratch working directory: the repo's top level linked in, so paths in the docs work and writes stay out. */ function scratch() { const dir = mkdtempSync(join(tmpdir(), "atlibs-docs-")); // tracked top-level entries and the build output only, so nothing a page writes lands in the repo const tracked = execFileSync("git", ["ls-tree", "--name-only", "HEAD"], { cwd: ROOT, encoding: "utf8" }).split("\n").filter(Boolean); for (const name of new Set([...tracked, "target"])) if (name !== "docs" && existsSync(join(ROOT, name))) symlinkSync(join(ROOT, name), join(dir, name)); return dir; } /** The loopback ports a script names: `--port N`, `http.server N`, `localhost:N`, `127.0.0.1:N`. */ const portsIn = (text) => [...new Set([...text.matchAll(/(?:--port[ =]|http\.server |localhost:|127\.0\.0\.1:)(\d{2,5})\b/g)].map((m) => Number(m[1])))].sort((a, b) => a - b); /** * Hold `port` until the returned release: a lock file in the temp dir, so docs runs in other checkouts on this * machine take turns on the fixed ports the pages serve on instead of answering each other's curls. A lock whose * process is gone is taken over. */ async function lockPort(port) { const lock = join(tmpdir(), `atlibs-docs-port-${port}.lock`); const mine = `${lock}.${process.pid}`; writeFileSync(mine, String(process.pid)); try { for (;;) { try { linkSync(mine, lock); return () => rmSync(lock, { force: true }); } catch (e) { if (e.code !== "EEXIST") throw e; } try { process.kill(Number(readFileSync(lock, "utf8")), 0); } catch { // ponytail: two waiters on one dead holder can both take it; flock if that ever bites rmSync(lock, { force: true }); continue; } await sleep(200); } } finally { rmSync(mine, { force: true }); } } /** A bash loop: fail if any of `ports` already answers (another program has it), or wait (30 s at most) until all do. */ const probe = (ports, free) => `for p in ${ports.join(" ")}; do ${free ? `if (exec 3<>/dev/tcp/127.0.0.1/$p) 2>/dev/null; then echo "port $p is in use by another program"; exit 1; fi` : "for _ in $(seq 300); do (exec 3<>/dev/tcp/127.0.0.1/$p) 2>/dev/null && break; sleep 0.1; done"}; done`; function env(dir) { const clean = Object.fromEntries(Object.entries(process.env).filter(([k]) => !/^(ATPROTO|ATP)_/.test(k) && !k.startsWith("ATLIBS_") && k !== "NODE_TEST_CONTEXT")); return { ...clean, PATH: `${dirname(hostBin())}:${process.env.PATH}`, ATLIBS_SERVER_STATE: join(dir, ".state"), XDG_CACHE_HOME: join(dir, ".cache"), ATLIBS_REPO: ROOT }; } for (const page of pages) { const name = relative(DOCS, page); const text = read(page); test(name, async (t) => { const all = blocks(text); const script = []; const skipped = []; for (const [i, b] of all.entries()) { const where = `${name}:${b.line}`; if (b.path && existsSync(join(ROOT, b.path))) { const file = read(join(ROOT, b.path)); let at = 0; for (const piece of elided(b.body)) { const found = file.indexOf(piece, at); assert.ok(found >= 0, `${where}: not in ${b.path}:\n${piece.split("\n").find((l) => !file.includes(l)) ?? piece.slice(0, 200)}`); at = found + piece.length; } continue; } assert.ok(!b.path, `${where}: \`${b.path}\` above a block is not a file in the repo`); const run = runTag(b); if (b.lang === "output") { assert.ok(all[i - 1] && runTag(all[i - 1]), `${where}: an output block must follow a run block`); continue; } if (b.lang === "mermaid" || b.tags.includes("norun")) continue; if (b.lang === "text" && b.tags.includes("help")) { const help = execFileSync(hostBin(), ["help"], { encoding: "utf8" }); for (const line of b.body.split("\n").filter((l) => l.trim())) assert.ok(help.includes(line.trim()), `${where}: not in atlibs help: ${line.trim()}`); continue; } assert.ok(run, `${where}: a \`\`\`${b.lang} block that is not a file excerpt must be tagged run, run=network, run=account, run=gate, run=container, run=install, help or norun`); const browser = b.tags.includes("browser"); assert.ok(!browser || b.lang === "js" || b.lang === "html", `${where}: only a js or html block runs in the browser`); if (browser && !CHROME) skipped.push(`${where} ${run} browser (no Chrome)`); else if (run === "run" || (run === "run=network" && NETWORK)) { const next = all[i + 1]?.lang === "output" ? all[i + 1].body : null; // a js block runs as a module file in the scratch directory: with node (imports: ./javascript/src/...), // or as a page module in Chrome (imports: atlibs/...) const runner = browser ? `node ${join(DOCS, "browser.mjs")}` : "node"; const file = b.lang === "html" ? (b.tags.find((t) => t.endsWith(".html")) ?? `.snippet-${i}.html`) : `.snippet-${i}.mjs`; const body = b.lang === "js" || b.lang === "html" ? `cat > ${file} <<'__SNIPPET__'\n${b.body}\n__SNIPPET__\n${runner} ${file}` : b.body; script.push({ where, body, output: next }); } else skipped.push(`${where} ${run}`); } if (skipped.length) t.diagnostic(`not run: ${skipped.join(", ")}`); if (!script.length) return; const dir = scratch(); // a page's own builds (atlibs new my-module && cargo wasm: ~460 MB) go with it, failed or not t.after(() => KEEP ? t.diagnostic(`kept ${dir}`) : rmSync(dir, { recursive: true, force: true })); // a server started in the background (`… &`): its port (named in it, or else in the next block) must be free, // and the next block waits until it answers const serve = (s, n) => { const ports = portsIn(s.body).length ? portsIn(s.body) : portsIn(script[n + 1]?.body ?? ""); return ports.length ? [probe(ports, true), s.body, probe(ports, false)] : [s.body, "sleep 1"]; }; const source = ["set -e", "exec 2>&1", 'trap \'kill $(jobs -p) 2>/dev/null || true\' EXIT'] .concat(script.flatMap((s, n) => [`printf '\n@@%s\n' ${n}`, ...(s.body.trimEnd().endsWith("&") ? serve(s, n) : [s.body])])) .join("\n"); const releases = []; let ran; try { for (const port of portsIn(script.map((s) => s.body).join("\n"))) releases.push(await lockPort(port)); ran = spawnSync("bash", ["-c", source], { cwd: dir, env: env(dir), encoding: "utf8", timeout: 180_000 }); } finally { for (const release of releases) release(); } const parts = ran.stdout.split(/^@@(\d+)$/m); const outputs = new Map(); for (let k = 1; k < parts.length; k += 2) outputs.set(Number(parts[k]), parts[k + 1] ?? ""); const last = Math.max(-1, ...outputs.keys()); assert.equal(ran.status, 0, `${script[last]?.where ?? name}: exited ${ran.status}${ran.error ? ` (${ran.error.message})` : ""}\n${script[last]?.body}\n--- output\n${(outputs.get(last) ?? ran.stdout).slice(-3000)}`); for (const [n, s] of script.entries()) { if (s.output === null) continue; const missing = matches(s.output, outputs.get(n) ?? ""); assert.equal(missing, null, `${s.where}: ${missing}\n--- output\n${(outputs.get(n) ?? "").slice(-3000)}`); } }); } test("links resolve", () => { for (const page of [...pages, join(ROOT, "README.md")]) { for (const [, target] of read(page).matchAll(/\]\(([^)\s]+)\)/g)) { if (/^[a-z]+:/.test(target) || target.startsWith("#")) continue; const path = join(dirname(page), target.split("#")[0]); assert.ok(existsSync(path), `${relative(ROOT, page)}: broken link ${target}`); } } }); test("every page is in docs/README.md", () => { const index = read(join(DOCS, "README.md")); for (const page of pages) if (!page.endsWith("/docs/README.md")) assert.ok(index.includes(`(${relative(DOCS, page)})`), `docs/README.md does not link ${relative(DOCS, page)}`); }); test("no code outside docs/ points into docs/", () => { const found = spawnSync("git", ["grep", "-nE", "docs/[A-Za-z0-9_/.-]+\\.md", "--", ".", ":!docs", ":!README.md"], { cwd: ROOT, encoding: "utf8" }); assert.equal(found.stdout, ""); }); test("reference/env.md lists every ATLIBS_* variable the code reads, and no other", () => { const doc = read(join(DOCS, "reference/env.md")); const sources = ["javascript/src", "rust/host/src", "rust/cli/src", "examples/node-hono/server.mjs"]; const code = spawnSync("git", ["grep", "-hoE", "ATLIBS_[A-Z_]+[A-Z]", "--", ...sources, ":!*.test.mjs", ":!**/fake.rs"], { cwd: ROOT, encoding: "utf8" }).stdout; const used = new Set(code.split("\n").filter(Boolean)); for (const v of used) assert.ok(doc.includes(`\`${v}\``), `env.md misses ${v}`); for (const [, v] of doc.matchAll(/`(ATLIBS_[A-Z_]+)`/g)) assert.ok(used.has(v), `env.md lists ${v}, which no code reads`); }); test("reference/lexicons.md lists every lexicon", () => { const doc = read(join(DOCS, "reference/lexicons.md")); for (const file of walk(join(ROOT, "lexicons")).filter((f) => f.endsWith(".json"))) { const { id } = JSON.parse(read(file)); assert.ok(doc.includes(`\`${id}\``), `lexicons.md misses ${id}`); } }); test("reference/limits.md states the run budget", () => { const doc = read(join(DOCS, "reference/limits.md")); for (const [key, value] of Object.entries(BUDGET)) { const shown = value === Infinity ? "unlimited" : value; assert.match(doc, new RegExp(`\`${key}\`[^\\n]*\\b${shown}\\b`), `limits.md: ${key} ${shown}`); } });