mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-29 20:05:25 +08:00
466 lines
21 KiB
JavaScript
466 lines
21 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { access, readdir, readFile, stat } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { collectMdxDocuments } from "../scripts/prepare-fumapress.mjs";
|
|
|
|
const root = path.resolve(import.meta.dirname, "..");
|
|
const publicRoot = path.join(root, "dist/public");
|
|
|
|
test("the documentation keeps a neutral base with custom section and TOC treatments", async () => {
|
|
const css = await readFile(path.join(root, "src/app.css"), "utf8");
|
|
assert.match(css, /fumadocs-ui\/css\/neutral\.css/);
|
|
assert.doesNotMatch(css, /fumadocs-ui\/css\/ocean\.css/);
|
|
assert.match(css, /#nd-toc > div > div/);
|
|
assert.match(css, /#nd-toc > div > div > a > svg/);
|
|
assert.match(css, /#nd-toc > div > div > div\.absolute/);
|
|
assert.match(css, /\[data-toc-popover-content\].*> div\.absolute/s);
|
|
assert.match(css, /a\[data-active="true"\]::before/);
|
|
assert.match(css, /\[data-toc-popover-content\] a\[href\^="#"\]\[data-active="true"\]::before/);
|
|
assert.match(css, /inset-inline-start: 0\.5rem/);
|
|
assert.match(css, /border-radius: 0/);
|
|
});
|
|
|
|
async function collectHtmlRoutes(directory) {
|
|
const routes = [];
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
const target = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) routes.push(...await collectHtmlRoutes(target));
|
|
else if (entry.name === "index.html") routes.push(target);
|
|
}
|
|
return routes;
|
|
}
|
|
|
|
function staticHtmlForCanonicalDocument(document) {
|
|
const parts = document.replace(/\.mdx$/, "").split("/");
|
|
if (parts.at(-1) === "index") parts.pop();
|
|
return path.join(publicRoot, ...parts, "index.html");
|
|
}
|
|
|
|
function staticHtmlForUrlPath(urlPath) {
|
|
const parts = urlPath.replace(/^\/docs(?:\/|$)/, "").replace(/^\//, "").replace(/\/$/, "").split("/")
|
|
.map((segment) => decodeURIComponent(segment));
|
|
return path.join(publicRoot, ...parts, "index.html");
|
|
}
|
|
|
|
test("all 302 canonical localized documents have static HTML", async () => {
|
|
const documents = await collectMdxDocuments(root);
|
|
assert.equal(documents.length, 302);
|
|
const missing = [];
|
|
for (const document of documents) {
|
|
try {
|
|
await access(staticHtmlForCanonicalDocument(document));
|
|
} catch {
|
|
missing.push(document);
|
|
}
|
|
}
|
|
assert.deepEqual(missing, []);
|
|
});
|
|
|
|
test("every fixed redirect destination resolves to generated output", async () => {
|
|
const redirects = (await readFile(path.join(publicRoot, "_redirects"), "utf8"))
|
|
.trim().split("\n").map((line) => line.split(/\s+/));
|
|
for (const [source, destination] of redirects) {
|
|
if (destination.includes(":splat")) continue;
|
|
await access(staticHtmlForUrlPath(destination), undefined, `${source} -> ${destination}`);
|
|
}
|
|
});
|
|
|
|
test("static output uses decoded filesystem paths for edge hosting", async () => {
|
|
const encodedComponents = [];
|
|
async function walk(directory) {
|
|
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
|
if (/%[0-9A-Fa-f]{2}/.test(entry.name)) encodedComponents.push(path.join(directory, entry.name));
|
|
if (entry.isDirectory()) await walk(path.join(directory, entry.name));
|
|
}
|
|
}
|
|
await walk(publicRoot);
|
|
assert.deepEqual(encodedComponents, []);
|
|
});
|
|
|
|
test("static routes never repeat their locale prefix", async () => {
|
|
for (const locale of ["en", "zh", "ja"]) {
|
|
await assert.rejects(access(path.join(publicRoot, locale, locale)), { code: "ENOENT" });
|
|
}
|
|
});
|
|
|
|
test("representative localized guides and canonical host are emitted", async () => {
|
|
for (const locale of ["en", "zh", "ja"]) {
|
|
await access(path.join(publicRoot, locale, "insight/guide/index.html"));
|
|
}
|
|
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
|
|
const robots = await readFile(path.join(publicRoot, "robots.txt"), "utf8");
|
|
assert.match(sitemap, /https:\/\/langbot\.app\/docs\//);
|
|
assert.doesNotMatch(sitemap, /https:\/\/docs\.langbot\.app\//);
|
|
assert.match(robots, /https:\/\/langbot\.app\/docs\/sitemap\.xml/);
|
|
assert.doesNotMatch(robots, /https:\/\/docs\.langbot\.app\//);
|
|
});
|
|
|
|
test("platform navigation renders logo assets instead of icon path text", async () => {
|
|
for (const locale of ["en", "zh", "ja"]) {
|
|
const html = await readFile(
|
|
path.join(publicRoot, locale, "usage/platforms/discord/index.html"),
|
|
"utf8",
|
|
);
|
|
const asideStart = html.indexOf('<aside id="nd-sidebar"');
|
|
const asideEnd = html.indexOf("</aside>", asideStart);
|
|
const sidebar = html.slice(asideStart, asideEnd);
|
|
assert.match(
|
|
sidebar,
|
|
/<img[^>]+src="\/docs\/images\/platforms\/discord\.svg"[^>]*>/,
|
|
`${locale} sidebar is missing the Discord logo`,
|
|
);
|
|
assert.match(sidebar, />Discord</, `${locale} sidebar is missing the platform title`);
|
|
assert.doesNotMatch(
|
|
sidebar,
|
|
/>\/images\/platforms\/discord\.svg(?:<!-- -->)?Discord</,
|
|
`${locale} sidebar exposes the icon path as text`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("nested bot groups preserve the logos declared in docs.json", async () => {
|
|
const html = await readFile(
|
|
path.join(publicRoot, "zh/usage/platforms/discord/index.html"),
|
|
"utf8",
|
|
);
|
|
const asideStart = html.indexOf('<aside id="nd-sidebar"');
|
|
const asideEnd = html.indexOf("</aside>", asideStart);
|
|
const sidebar = html.slice(asideStart, asideEnd);
|
|
for (const [title, logo] of [
|
|
["企业微信", "wecom.svg"],
|
|
["微信", "wechat.svg"],
|
|
["QQ 官方机器人", "qq.svg"],
|
|
["QQ (OneBot v11)", "qq.svg"],
|
|
]) {
|
|
const escapedTitle = title.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
const escapedLogo = logo.replace(".", "\\.");
|
|
assert.match(
|
|
sidebar,
|
|
new RegExp(`<img[^>]+src="/docs/images/platforms/${escapedLogo}"[^>]*>\\s*${escapedTitle}<svg`),
|
|
`zh sidebar is missing the ${logo} logo for ${title}`,
|
|
);
|
|
}
|
|
assert.doesNotMatch(
|
|
sidebar,
|
|
/>robot(?:<!-- -->)?Satori/,
|
|
"zh sidebar exposes the named robot icon as text",
|
|
);
|
|
assert.match(
|
|
sidebar,
|
|
/<svg[^>]+class="[^"]*lucide-bot[^"]*"[^>]*>[\s\S]*?<\/svg>Satori/,
|
|
"zh sidebar is missing the Satori robot icon",
|
|
);
|
|
});
|
|
|
|
test("all locales emit the OpenAPI surface with endpoint semantics", async () => {
|
|
const representatives = {
|
|
en: ["system/get-system-information", "Get system information", "GET", "/api/v1/system/info"],
|
|
zh: ["系统/获取系统信息", "获取系统信息", "GET", "/api/v1/system/info"],
|
|
ja: ["system/获取系统信息", "获取系统信息", "GET", "/api/v1/system/info"],
|
|
};
|
|
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
|
|
|
|
for (const [locale, [representativePath, ...semantics]] of Object.entries(representatives)) {
|
|
const apiRoot = path.join(publicRoot, locale, "api-reference");
|
|
const routes = await collectHtmlRoutes(apiRoot);
|
|
assert.ok(routes.length >= 50, `${locale} emitted only ${routes.length} API routes`);
|
|
|
|
const encodedRepresentativePath = representativePath.split("/").map((segment) => encodeURIComponent(segment)).join("/");
|
|
const representative = path.join(apiRoot, representativePath, "index.html");
|
|
const html = await readFile(representative, "utf8");
|
|
for (const semantic of semantics) assert.ok(html.includes(semantic), `${locale} page is missing ${semantic}`);
|
|
assert.ok(sitemap.includes(`https://langbot.app/docs/${locale}/api-reference/${encodedRepresentativePath}`));
|
|
|
|
const spec = JSON.parse(await readFile(path.join(root, `openapi/service-api-${locale}.json`), "utf8"));
|
|
const tagOrder = [];
|
|
for (const pathItem of Object.values(spec.paths ?? {})) {
|
|
for (const operation of Object.values(pathItem)) {
|
|
if (!operation || typeof operation !== "object") continue;
|
|
for (const tag of operation.tags ?? []) if (!tagOrder.includes(tag)) tagOrder.push(tag);
|
|
}
|
|
}
|
|
const asideStart = html.indexOf('<aside id="nd-sidebar"');
|
|
const asideEnd = html.indexOf("</aside>", asideStart);
|
|
const sidebar = html.slice(asideStart, asideEnd);
|
|
assert.match(sidebar, /<p[^>]*>HTTP API Reference<\/p>/, `${locale} OpenAPI section is missing`);
|
|
let previousTag = -1;
|
|
for (const tag of tagOrder) {
|
|
const position = sidebar.indexOf(`>${tag}<`);
|
|
assert.ok(position > previousTag, `${locale} OpenAPI tag order changed at ${tag}`);
|
|
previousTag = position;
|
|
}
|
|
}
|
|
});
|
|
|
|
test("canonical sitemap routes match the legacy Mintlify directory structure", async () => {
|
|
const legacy = new Set((await readFile(path.join(root, "tests/fixtures/legacy-sitemap-routes.txt"), "utf8"))
|
|
.trim().split("\n").map((route) => route.replace(/\/$/, "")));
|
|
// Mintlify accidentally indexed this repository maintenance note; preserve
|
|
// its URL as a redirect without treating it as public documentation.
|
|
legacy.delete("/scripts/README-blog-articles");
|
|
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
|
|
const actual = new Set([...sitemap.matchAll(/<loc>https:\/\/langbot\.app\/docs(\/[^<]+)<\/loc>/g)]
|
|
.map((match) => decodeURIComponent(match[1]).replace(/\/$/, "")));
|
|
assert.deepEqual([...actual].sort(), [...legacy].sort());
|
|
});
|
|
|
|
const locales = ["en", "zh", "ja"];
|
|
const hreflangByLocale = { en: "en", zh: "zh-CN", ja: "ja" };
|
|
const zhOnlyRoutes = [
|
|
"develop/adapter/discord/README",
|
|
"develop/adapter/discord/api_reference",
|
|
"develop/adapter/discord/design",
|
|
"develop/adapter/discord/quick_start",
|
|
"develop/adapter/discord/troubleshooting",
|
|
"tob/contact",
|
|
"tob/index",
|
|
"tob/platform",
|
|
"tob/provider",
|
|
"usage/pipelines/tbox",
|
|
"usage/platforms/qq/official",
|
|
"usage/platforms/wechat/wechatpad",
|
|
"workshop/shengsuanyun-integration",
|
|
"workshop/tokenpony-integration",
|
|
];
|
|
|
|
function routeFromHtml(file) {
|
|
const relative = path.relative(publicRoot, path.dirname(file)).split(path.sep).join("/");
|
|
return `/${relative}`.replace(/\/$/, "");
|
|
}
|
|
|
|
function extractHeadLinks(html, rel) {
|
|
return [...html.matchAll(/<link\s+([^>]+)>/g)]
|
|
.map((match) => match[1])
|
|
.filter((attributes) => new RegExp(`(?:^|\\s)rel="${rel}"(?:\\s|$)`).test(attributes))
|
|
.map((attributes) => ({
|
|
href: attributes.match(/(?:^|\s)href="([^"]+)"/)?.[1]?.replaceAll("&", "&"),
|
|
hreflang: attributes.match(/(?:^|\s)hrefLang="([^"]+)"/i)?.[1],
|
|
}));
|
|
}
|
|
|
|
test("rendered navbar and documentation tree use each locale's docs.json labels", async () => {
|
|
const expected = {
|
|
en: ["Roadmap", "https://langbot.app/en/roadmap", "Guides", "Quick Start", "Installation", "Developers", "Articles", "API Reference"],
|
|
zh: ["路线图", "https://langbot.app/zh/roadmap", "指南", "快速开始", "安装部署", "开发者", "文章", "API 参考"],
|
|
ja: ["ロードマップ", "https://langbot.app/ja/roadmap", "ガイド", "クイックスタート", "インストール", "開発者", "記事", "API リファレンス"],
|
|
};
|
|
const removedHomeLinks = {
|
|
en: "https://langbot.app/en",
|
|
zh: "https://langbot.app/zh",
|
|
ja: "https://langbot.app/ja",
|
|
};
|
|
for (const locale of locales) {
|
|
const html = await readFile(path.join(publicRoot, locale, "insight/guide/index.html"), "utf8");
|
|
for (const value of expected[locale]) assert.ok(html.includes(value), `${locale} navbar missing ${value}`);
|
|
assert.match(html, /<img[^>]+src="\/docs\/langbot-logo\.png"[^>]*>/, `${locale} docs brand logo is not base-path scoped`);
|
|
assert.doesNotMatch(html, /<img[^>]+src="\/langbot-logo\.png"[^>]*>/, `${locale} docs brand logo points outside /docs`);
|
|
assert.ok(!html.includes(`href="${removedHomeLinks[locale]}"`), `${locale} navbar still includes Home`);
|
|
for (const languageName of ["English", "简体中文", "日本語"]) {
|
|
assert.ok(html.includes(languageName), `${locale} language selector missing ${languageName}`);
|
|
}
|
|
for (const other of locales.filter((item) => item !== locale)) {
|
|
assert.ok(!html.includes(expected[other][1]), `${locale} navbar includes ${other} roadmap link`);
|
|
}
|
|
}
|
|
});
|
|
|
|
function collectNavigationPages(value, output = []) {
|
|
if (typeof value === "string") output.push(value);
|
|
else if (Array.isArray(value)) value.forEach((item) => collectNavigationPages(item, output));
|
|
else if (value && typeof value === "object") {
|
|
if (value.hidden) return output;
|
|
for (const key of ["tabs", "groups", "pages"]) {
|
|
if (Array.isArray(value[key])) collectNavigationPages(value[key], output);
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function firstNavigationPage(value) {
|
|
if (typeof value === "string") return value;
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) {
|
|
const page = firstNavigationPage(item);
|
|
if (page) return page;
|
|
}
|
|
} else if (value && typeof value === "object" && !value.hidden) {
|
|
return firstNavigationPage(value.pages ?? value.groups ?? []);
|
|
}
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
}
|
|
|
|
test("docs.json tabs and groups render as root selectors and non-collapsible sections", async () => {
|
|
const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8"));
|
|
const localeMap = { en: "en", cn: "zh", jp: "ja" };
|
|
for (const language of docs.navigation.languages) {
|
|
const locale = localeMap[language.language];
|
|
for (const tab of language.tabs.filter((item) => !item.hidden)) {
|
|
const sourcePage = firstNavigationPage(tab.groups);
|
|
assert.ok(sourcePage, `${locale} ${tab.tab} has no representative page`);
|
|
const route = sourcePage.replace(/\/index$/, "");
|
|
const html = await readFile(path.join(publicRoot, route, "index.html"), "utf8");
|
|
const asideStart = html.indexOf('<aside id="nd-sidebar"');
|
|
const asideEnd = html.indexOf("</aside>", asideStart);
|
|
const sidebar = html.slice(asideStart, asideEnd);
|
|
assert.ok(sidebar.includes(`>${escapeHtml(tab.tab)}</p>`), `${locale} ${tab.tab} is not the root selector`);
|
|
for (const group of tab.groups.filter((item) => !item.hidden)) {
|
|
assert.ok(sidebar.includes(`>${escapeHtml(group.group)}</p>`), `${locale} ${tab.tab}/${group.group} is not a section label`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("every legacy navigation entry resolves to its exact sidebar URL", async () => {
|
|
const docs = JSON.parse(await readFile(path.join(root, "docs.json"), "utf8"));
|
|
const localeMap = { en: "en", cn: "zh", jp: "ja" };
|
|
const failures = [];
|
|
for (const language of docs.navigation.languages) {
|
|
const locale = localeMap[language.language];
|
|
const pages = collectNavigationPages(language.tabs ?? []);
|
|
for (const page of pages) {
|
|
if (!page.startsWith(`${locale}/`)) continue;
|
|
const route = page.replace(/\/index$/, "");
|
|
const htmlPath = path.join(publicRoot, route, "index.html");
|
|
const html = await readFile(htmlPath, "utf8");
|
|
const asideStart = html.indexOf('<aside id="nd-sidebar"');
|
|
const asideEnd = html.indexOf("</aside>", asideStart);
|
|
const sidebar = html.slice(asideStart, asideEnd);
|
|
if (!sidebar.includes(`href="/docs/${route}"`)) failures.push({ page, htmlPath });
|
|
}
|
|
}
|
|
assert.deepEqual(failures, []);
|
|
});
|
|
|
|
test("the Chinese quick-start navigation keeps the incumbent troubleshooting page", async () => {
|
|
const html = await readFile(path.join(publicRoot, "zh/insight/guide/index.html"), "utf8");
|
|
const asideStart = html.indexOf('<aside id="nd-sidebar"');
|
|
const asideEnd = html.indexOf("</aside>", asideStart);
|
|
const sidebar = html.slice(asideStart, asideEnd);
|
|
assert.match(sidebar, /href="\/docs\/zh\/insight\/troubleshooting"/);
|
|
assert.doesNotMatch(sidebar, /href="\/docs\/zh\/develop\/adapter\/discord\/troubleshooting"/);
|
|
});
|
|
|
|
test("the exact 14 zh-only sources publish no fallback en or ja routes", async () => {
|
|
const documents = await collectMdxDocuments(root);
|
|
const byLocale = Object.fromEntries(locales.map((locale) => [
|
|
locale,
|
|
new Set(documents.filter((item) => item.startsWith(`${locale}/`)).map((item) => item.slice(3).replace(/\.mdx$/, ""))),
|
|
]));
|
|
const actual = [...byLocale.zh].filter((route) => !byLocale.en.has(route) && !byLocale.ja.has(route)).sort();
|
|
assert.deepEqual(actual, zhOnlyRoutes);
|
|
|
|
const searchableOutputs = await Promise.all(
|
|
["sitemap.xml", "llms.txt", "llms-full.txt", "api/search"].map((file) => readFile(path.join(publicRoot, file), "utf8")),
|
|
);
|
|
for (const sourceRoute of zhOnlyRoutes) {
|
|
const route = sourceRoute === "index" ? "" : sourceRoute.replace(/\/index$/, "");
|
|
await access(path.join(publicRoot, "zh", route, "index.html"));
|
|
for (const locale of ["en", "ja"]) {
|
|
await assert.rejects(access(path.join(publicRoot, locale, route, "index.html")), { code: "ENOENT" });
|
|
for (const output of searchableOutputs) {
|
|
assert.doesNotMatch(
|
|
output,
|
|
new RegExp(`/${locale}/${route.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_-])`),
|
|
`${locale}/${route} leaked into an index`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test("every page emits a canonical and reciprocal available hreflangs", async () => {
|
|
const sitemap = await readFile(path.join(publicRoot, "sitemap.xml"), "utf8");
|
|
const routes = new Set([...sitemap.matchAll(/<loc>https:\/\/langbot\.app\/docs(\/[^<]+)<\/loc>/g)].map((match) => match[1].replace(/\/$/, "")));
|
|
const allFiles = await collectHtmlRoutes(publicRoot);
|
|
const localized = allFiles.filter((file) => routes.has(routeFromHtml(file)));
|
|
for (const file of localized) {
|
|
const route = routeFromHtml(file);
|
|
const html = await readFile(file, "utf8");
|
|
const canonical = extractHeadLinks(html, "canonical");
|
|
assert.deepEqual(canonical, [{ href: `https://langbot.app/docs${route}`, hreflang: undefined }], route);
|
|
|
|
const [, locale, ...suffixParts] = route.split("/");
|
|
const suffix = suffixParts.join("/");
|
|
const actual = Object.fromEntries(
|
|
extractHeadLinks(html, "alternate")
|
|
.filter((item) => item.hreflang)
|
|
.map((item) => [item.hreflang, item.href]),
|
|
);
|
|
if (suffix.startsWith("api-reference/")) {
|
|
assert.deepEqual(Object.keys(actual).sort(), ["en", "ja", "x-default", "zh-CN"]);
|
|
assert.equal(actual["x-default"], actual.en);
|
|
for (const href of Object.values(actual)) {
|
|
const target = new URL(href).pathname.replace(/^\/docs/, "").replace(/\/$/, "");
|
|
assert.ok(routes.has(target), `${route} links to missing alternate ${target}`);
|
|
}
|
|
} else {
|
|
const expected = {};
|
|
for (const candidate of locales) {
|
|
const candidateRoute = `/${candidate}/${suffix}`.replace(/\/$/, "");
|
|
if (routes.has(candidateRoute)) expected[hreflangByLocale[candidate]] = `https://langbot.app/docs${candidateRoute}`;
|
|
}
|
|
expected["x-default"] = expected.en ?? expected["zh-CN"] ?? expected.ja;
|
|
assert.deepEqual(actual, expected, route);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("alternate sitemap is complete, reciprocal, and advertised", async () => {
|
|
const sitemap = await readFile(path.join(publicRoot, "sitemap-alternates.xml"), "utf8");
|
|
const robots = await readFile(path.join(publicRoot, "robots.txt"), "utf8");
|
|
assert.match(robots, /^Sitemap: https:\/\/langbot\.app\/docs\/sitemap\.xml$/m);
|
|
assert.match(robots, /^Sitemap: https:\/\/langbot\.app\/docs\/sitemap-alternates\.xml$/m);
|
|
|
|
const blocks = [...sitemap.matchAll(/<url>([\s\S]*?)<\/url>/g)].map((match) => match[1]);
|
|
const groups = new Map();
|
|
for (const block of blocks) {
|
|
const loc = block.match(/<loc>([^<]+)<\/loc>/)?.[1];
|
|
const links = Object.fromEntries([...block.matchAll(/hreflang="([^"]+)" href="([^"]+)"/g)].map((match) => [match[1], match[2]]));
|
|
assert.ok(loc && links.en && links["zh-CN"] && links.ja && links["x-default"]);
|
|
const key = [links.en, links["zh-CN"], links.ja].join("\n");
|
|
groups.set(key, [...(groups.get(key) ?? []), loc]);
|
|
}
|
|
assert.ok(groups.size >= 155, `only ${groups.size} shared groups`);
|
|
for (const [key, locations] of groups) assert.deepEqual(locations.sort(), key.split("\n").sort());
|
|
for (const slug of [
|
|
"articles",
|
|
"articles/langbot-cloud-multi-user-workspaces",
|
|
"articles/langbot-v4100-sandbox-skills",
|
|
"articles/langbot-v490-rag-plugin-architecture",
|
|
"articles/langbot-plugin-system-deep-dive",
|
|
"articles/langtars-remote-pc-control-dify-n8n",
|
|
"articles/connect-deepseek-to-wechat",
|
|
"articles/deploy-ai-bot-in-5-minutes",
|
|
"articles/dify-agent-discord-telegram-slack",
|
|
"articles/langflow-drag-and-drop-ai-bot",
|
|
"articles/n8n-multi-platform-ai-chatbot",
|
|
"articles/dify-langbot-rag-knowledge-base",
|
|
"articles/welcome",
|
|
]) assert.ok(sitemap.includes(`https://langbot.app/docs/en/${slug}`), `alternate sitemap missing ${slug}`);
|
|
});
|
|
|
|
test("generated pages contain no internal .html route links", async () => {
|
|
const offenders = [];
|
|
for (const file of await collectHtmlRoutes(publicRoot)) {
|
|
const html = await readFile(file, "utf8");
|
|
for (const match of html.matchAll(/href="([^"]+)"/g)) {
|
|
const href = match[1].replaceAll("&", "&");
|
|
if (/^(?:https?:|mailto:|tel:|#|\/\/)/.test(href)) continue;
|
|
const pathname = href.split(/[?#]/, 1)[0];
|
|
if (pathname.endsWith(".html")) offenders.push([routeFromHtml(file), href]);
|
|
}
|
|
for (const match of html.matchAll(/href="(\/[^"?#]+\.html)(?:[?#][^"]*)?"/g)) offenders.push([routeFromHtml(file), match[1]]);
|
|
}
|
|
assert.deepEqual(offenders, []);
|
|
});
|
|
|
|
|
|
test("static search stays within Cloudflare Pages' 25 MiB file limit", async () => {
|
|
const search = await stat(path.join(publicRoot, "api/search"));
|
|
assert.ok(search.size <= 25 * 1024 * 1024, `search artifact is ${search.size} bytes`);
|
|
});
|