Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

416 lines
11 KiB
JavaScript

/**
* Docs site integrity tests for Mintlify navigation and markdown links.
*
* Runs against the real files on disk so docs.json, redirects, frontmatter,
* local assets, and internal links stay deployable together.
*/
import assert from "node:assert";
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { describe, it } from "node:test";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const DOCS_DIR = join(__dirname, "..");
const DOCS_JSON_PATH = join(DOCS_DIR, "docs.json");
function readDocsConfig() {
return JSON.parse(readFileSync(DOCS_JSON_PATH, "utf-8"));
}
function normalizeRoute(route) {
const cleanRoute = route
.split("#")[0]
.split("?")[0]
.replace(/^\/+/, "")
.replace(/\/$/, "")
.replace(/\.mdx?$/, "");
return cleanRoute;
}
function collectRedirects(config = readDocsConfig()) {
const redirects = new Map();
for (const redirect of config.redirects ?? []) {
redirects.set(normalizeRoute(redirect.source), redirect.destination);
}
return redirects;
}
function collectPages(obj) {
if (Array.isArray(obj)) {
return obj.flatMap((item) =>
typeof item === "string" ? [item] : collectPages(item),
);
}
if (!obj || typeof obj !== "object") {
return [];
}
return [
...collectPages(obj.pages),
...collectPages(obj.groups),
...collectPages(obj.tabs),
];
}
function findDuplicates(values) {
const seen = new Set();
const duplicates = new Set();
for (const value of values) {
if (seen.has(value)) {
duplicates.add(value);
} else {
seen.add(value);
}
}
return [...duplicates];
}
function collectMarkdownFiles(dir = DOCS_DIR) {
const entries = readdirSync(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
if (entry.name === "node_modules" || entry.name.startsWith(".")) {
continue;
}
const entryPath = join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectMarkdownFiles(entryPath));
} else if (entry.name.endsWith(".md") || entry.name.endsWith(".mdx")) {
files.push(entryPath);
}
}
return files;
}
function internalTargetExists(target) {
const cleanTarget = normalizeRoute(target);
if (!cleanTarget) return true;
const candidates = [
join(DOCS_DIR, cleanTarget),
join(DOCS_DIR, `${cleanTarget}.md`),
join(DOCS_DIR, `${cleanTarget}.mdx`),
join(DOCS_DIR, cleanTarget, "index.md"),
join(DOCS_DIR, cleanTarget, "index.mdx"),
];
return candidates.some(existsSync);
}
function localAssetExists(target) {
const cleanTarget = normalizeRoute(target);
if (!cleanTarget) return true;
return [
join(DOCS_DIR, cleanTarget),
join(DOCS_DIR, "public", cleanTarget),
].some(existsSync);
}
function internalTargetOrRedirectExists(
target,
redirects = collectRedirects(),
) {
if (internalTargetExists(target)) {
return true;
}
const destination = redirects.get(normalizeRoute(target));
return destination ? internalTargetExists(destination) : false;
}
function resolveInternalTarget(sourceFile, href) {
if (
!href ||
href.startsWith("#") ||
href.startsWith("mailto:") ||
/^[a-z][a-z0-9+.-]*:/i.test(href)
) {
return null;
}
if (href.startsWith("/")) {
return href;
}
const target = relative(DOCS_DIR, resolve(dirname(sourceFile), href));
return target.startsWith("..") ? null : target;
}
function extractMarkdownFrontmatter(content) {
if (!content.startsWith("---\n")) {
return null;
}
const end = content.indexOf("\n---", 4);
if (end === -1) {
return { closed: false, body: content.slice(4) };
}
return { closed: true, body: content.slice(4, end) };
}
describe("docs.json configuration", () => {
it("docs.json exists and is valid JSON", () => {
assert.ok(existsSync(DOCS_JSON_PATH), "docs.json should exist");
const config = readDocsConfig();
assert.ok(
typeof config === "object" && config !== null,
"should be a valid object",
);
});
it("has required Mintlify configuration fields", () => {
const config = readDocsConfig();
assert.ok(config.name, "should have name");
assert.ok(config.colors, "should have colors");
assert.ok(config.navigation, "should have navigation");
});
it("has valid theme", () => {
const config = readDocsConfig();
assert.ok(config.theme, "should have theme");
const validThemes = ["mint", "quill", "venus", "prism"];
assert.ok(
validThemes.includes(config.theme),
`theme "${config.theme}" should be a valid Mintlify theme`,
);
});
it("has valid color configuration", () => {
const config = readDocsConfig();
assert.ok(config.colors.primary, "should have primary color");
assert.match(
config.colors.primary,
/^#[0-9A-Fa-f]{6}$/,
"primary color should be valid hex",
);
});
it("navigation tabs are defined", () => {
const config = readDocsConfig();
assert.ok(config.navigation, "should have navigation");
assert.ok(
config.navigation.tabs || config.navigation.global,
"should have tabs or global navigation",
);
});
it("navigation tabs and groups do not duplicate labels", () => {
const config = readDocsConfig();
const tabs = config.navigation.tabs ?? [];
const duplicateTabs = findDuplicates(tabs.map((tab) => tab.tab));
assert.deepStrictEqual(duplicateTabs, []);
for (const tab of tabs) {
const groups = tab.groups ?? [];
const duplicateGroups = findDuplicates(
groups.map((group) => group.group),
);
assert.deepStrictEqual(
duplicateGroups,
[],
`Duplicate group labels in tab "${tab.tab}"`,
);
}
});
it("navigation groups do not list the same page twice", () => {
const config = readDocsConfig();
for (const tab of config.navigation.tabs ?? []) {
for (const group of tab.groups ?? []) {
const pages = collectPages(group.pages ?? []);
const duplicatePages = findDuplicates(pages);
assert.deepStrictEqual(
duplicatePages,
[],
`Duplicate pages in ${tab.tab} / ${group.group}`,
);
}
}
});
it("redirects resolve to real pages without loops or fragments", () => {
const config = readDocsConfig();
const redirects = config.redirects ?? [];
for (const redirect of redirects) {
assert.ok(redirect.source, "redirect should include a source");
assert.ok(redirect.destination, "redirect should include a destination");
assert.ok(
!/[?#]/.test(redirect.source),
`redirect source cannot include query or hash: ${redirect.source}`,
);
assert.ok(
!/[?#]/.test(redirect.destination),
`redirect destination cannot include query or hash: ${redirect.destination}`,
);
assert.notStrictEqual(
normalizeRoute(redirect.source),
normalizeRoute(redirect.destination),
`redirect loops to itself: ${redirect.source}`,
);
assert.ok(
internalTargetExists(redirect.destination),
`redirect destination should exist: ${redirect.source} -> ${redirect.destination}`,
);
}
});
it("configured local assets exist", () => {
const config = readDocsConfig();
const assetPaths = [
config.favicon,
config.logo?.light,
config.logo?.dark,
config.seo?.metadata?.["og:image"],
config.seo?.metadata?.["x:image"],
].filter(Boolean);
for (const assetPath of assetPaths) {
assert.ok(
localAssetExists(assetPath),
`Missing configured asset: ${assetPath}`,
);
}
});
});
describe("documentation files", () => {
it("core documentation pages referenced in navigation exist", () => {
const config = readDocsConfig();
const pages = collectPages(config.navigation);
const missingPages = [];
for (const page of pages) {
if (page.startsWith("http")) continue;
const mdxPath = join(DOCS_DIR, `${page}.mdx`);
const mdPath = join(DOCS_DIR, `${page}.md`);
if (!existsSync(mdxPath) && !existsSync(mdPath)) {
missingPages.push(page);
}
}
assert.strictEqual(
missingPages.length,
0,
`Missing documentation pages: ${missingPages.join(", ")}`,
);
});
it("documentation directories exist", () => {
const expectedDirs = ["tracks", "runtime", "plugins", "cli", "connectors"];
for (const dir of expectedDirs) {
assert.ok(
existsSync(join(DOCS_DIR, dir)),
`${dir}/ directory should exist`,
);
}
});
it("markdown files have content", () => {
const markdownFiles = collectMarkdownFiles();
for (const file of markdownFiles) {
const content = readFileSync(file, "utf-8");
const label = relative(DOCS_DIR, file);
assert.ok(content.trim().length > 0, `${label} should not be empty`);
}
});
it("frontmatter blocks are closed and do not duplicate keys", () => {
const markdownFiles = collectMarkdownFiles();
for (const file of markdownFiles) {
const content = readFileSync(file, "utf-8");
const frontmatter = extractMarkdownFrontmatter(content);
const label = relative(DOCS_DIR, file);
if (!frontmatter) {
continue;
}
assert.ok(frontmatter.closed, `${label} frontmatter should be closed`);
const keys = [];
for (const line of frontmatter.body.split("\n")) {
const match = /^([A-Za-z0-9_-]+):/.exec(line);
if (match) {
keys.push(match[1]);
}
}
assert.deepStrictEqual(
findDuplicates(keys),
[],
`${label} frontmatter should not duplicate keys`,
);
}
});
it("internal documentation links resolve", () => {
const markdownFiles = collectMarkdownFiles();
const redirects = collectRedirects();
const missingLinks = [];
const linkPattern =
/\[[^\]]+\]\(([^)\s]+)(?:\s+"[^"]*")?\)|href=["']([^"']+)["']/g;
for (const file of markdownFiles) {
const content = readFileSync(file, "utf-8");
let match = linkPattern.exec(content);
while (match !== null) {
const href = match[1] || match[2];
const target = resolveInternalTarget(file, href);
if (target && !internalTargetOrRedirectExists(target, redirects)) {
missingLinks.push(`${relative(DOCS_DIR, file)} -> ${href}`);
}
match = linkPattern.exec(content);
}
}
assert.deepStrictEqual(missingLinks, []);
});
it("local image and source assets referenced from markdown exist", () => {
const markdownFiles = collectMarkdownFiles();
const missingAssets = [];
const assetPattern =
/!\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(?:src|poster)=["']([^"']+)["']/g;
for (const file of markdownFiles) {
const content = readFileSync(file, "utf-8");
let match = assetPattern.exec(content);
while (match !== null) {
const href = match[1] || match[2];
const target = resolveInternalTarget(file, href);
if (target && !localAssetExists(target)) {
missingAssets.push(`${relative(DOCS_DIR, file)} -> ${href}`);
}
match = assetPattern.exec(content);
}
}
assert.deepStrictEqual(missingAssets, []);
});
});