1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
// 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 <output> 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`
// ```<lang> 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}`);
}
});