mirror of https://github.com/openclaw/openclaw.git
docs: fix English link audits (#57039)
Merged via squash.
Prepared head SHA: d20a3b620f
Co-authored-by: velvet-shark <126378+velvet-shark@users.noreply.github.com>
Reviewed-by: @velvet-shark
This commit is contained in:
parent
6c91b27756
commit
4680335b2a
|
|
@ -66,6 +66,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Plugins/CLI: add descriptor-backed lazy plugin CLI registration so Matrix can keep its CLI module lazy-loaded without dropping `openclaw matrix ...` from parse-time command registration. (#57165) Thanks @gumadeiras.
|
||||
- Plugins/CLI: collect root-help plugin descriptors through a dedicated non-activating CLI metadata path so enabled plugins keep validated config semantics without triggering runtime-only plugin registration work, while preserving runtime CLI command registration for legacy channel plugins that still wire commands from full registration. (#57294) thanks @gumadeiras.
|
||||
- Anthropic/OAuth: inject `/fast` `service_tier` hints for direct `sk-ant-oat-*` requests so OAuth-authenticated Anthropic runs stop missing the same overload-routing signal as API-key traffic. Fixes #55758. Thanks @Cypherm and @vincentkoc.
|
||||
- Docs/anchors: fix broken English docs links and make Mint anchor audits run against the English-source docs tree. (#57039) thanks @velvet-shark.
|
||||
|
||||
## 2026.3.28
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ Lark (global) tenants should use [https://open.larksuite.com/app](https://open.l
|
|||
2. Fill in the app name + description
|
||||
3. Choose an app icon
|
||||
|
||||

|
||||

|
||||
|
||||
### 3. Copy credentials
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ From **Credentials & Basic Info**, copy:
|
|||
|
||||
❗ **Important:** keep the App Secret private.
|
||||
|
||||

|
||||

|
||||
|
||||
### 4. Configure permissions
|
||||
|
||||
|
|
@ -126,7 +126,7 @@ On **Permissions**, click **Batch import** and paste:
|
|||
}
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### 5. Enable bot capability
|
||||
|
||||
|
|
@ -135,7 +135,7 @@ In **App Capability** > **Bot**:
|
|||
1. Enable bot capability
|
||||
2. Set the bot name
|
||||
|
||||

|
||||

|
||||
|
||||
### 6. Configure event subscription
|
||||
|
||||
|
|
@ -151,7 +151,7 @@ In **Event Subscription**:
|
|||
|
||||
⚠️ If the gateway is not running, the long-connection setup may fail to save.
|
||||
|
||||

|
||||

|
||||
|
||||
### 7. Publish the app
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ When using webhook mode, set both `channels.feishu.verificationToken` and `chann
|
|||
|
||||
The screenshot below shows where to find the **Verification Token**. The **Encrypt Key** is listed in the same **Encryption** section.
|
||||
|
||||

|
||||

|
||||
|
||||
### Configure via environment variables
|
||||
|
||||
|
|
@ -395,6 +395,8 @@ In addition to allowing the group itself, **all messages** in that group are gat
|
|||
|
||||
---
|
||||
|
||||
<a id="get-groupuser-ids"></a>
|
||||
|
||||
## Get group/user IDs
|
||||
|
||||
### Group IDs (chat_id)
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ If you want...
|
|||
- Direct chats use the main session (or per-sender if configured).
|
||||
- Heartbeats are skipped for group sessions.
|
||||
|
||||
<a id="pattern-personal-dms-public-groups-single-agent"></a>
|
||||
|
||||
## Pattern: personal DMs + public groups (single agent)
|
||||
|
||||
Yes — this works well if your “personal” traffic is **DMs** and your “public” traffic is **groups**.
|
||||
|
|
|
|||
|
|
@ -1125,6 +1125,8 @@ See [Streaming](/concepts/streaming) for behavior + chunking details.
|
|||
|
||||
See [Typing Indicators](/concepts/typing-indicators).
|
||||
|
||||
<a id="agentsdefaultssandbox"></a>
|
||||
|
||||
### `agents.defaults.sandbox`
|
||||
|
||||
Optional sandboxing for the embedded agent. See [Sandboxing](/gateway/sandboxing) for the full guide.
|
||||
|
|
|
|||
|
|
@ -602,6 +602,8 @@ Recommendations:
|
|||
- When running small models, **enable sandboxing for all sessions** and **disable web_search/web_fetch/browser** unless inputs are tightly controlled.
|
||||
- For chat-only personal assistants with trusted input and no tools, smaller models are usually fine.
|
||||
|
||||
<a id="reasoning-verbose-output-in-groups"></a>
|
||||
|
||||
## Reasoning & verbose output in groups
|
||||
|
||||
`/reasoning` and `/verbose` can expose internal reasoning or tool output that
|
||||
|
|
|
|||
|
|
@ -585,11 +585,12 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Why am I seeing HTTP 429 rate_limit_error from Anthropic?">
|
||||
That means your **Anthropic quota/rate limit** is exhausted for the current window. If you
|
||||
use a **Claude subscription** (setup-token), wait for the window to
|
||||
reset or upgrade your plan. If you use an **Anthropic API key**, check the Anthropic Console
|
||||
for usage/billing and raise limits as needed.
|
||||
<a id="why-am-i-seeing-http-429-ratelimiterror-from-anthropic"></a>
|
||||
<Accordion title="Why am I seeing HTTP 429 rate_limit_error from Anthropic?">
|
||||
That means your **Anthropic quota/rate limit** is exhausted for the current window. If you
|
||||
use a **Claude subscription** (setup-token), wait for the window to
|
||||
reset or upgrade your plan. If you use an **Anthropic API key**, check the Anthropic Console
|
||||
for usage/billing and raise limits as needed.
|
||||
|
||||
If the message is specifically:
|
||||
`Extra usage is required for long context requests`, the request is trying to use
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ If install succeeds but `openclaw` is not found in a new terminal, see [Node.js
|
|||
|
||||
---
|
||||
|
||||
<a id="installsh"></a>
|
||||
|
||||
## install.sh
|
||||
|
||||
<Tip>
|
||||
|
|
@ -170,6 +172,8 @@ The script exits with code `2` for invalid method selection or invalid `--instal
|
|||
|
||||
---
|
||||
|
||||
<a id="install-clish"></a>
|
||||
|
||||
## install-cli.sh
|
||||
|
||||
<Info>
|
||||
|
|
@ -248,6 +252,8 @@ Designed for environments where you want everything under a local prefix (defaul
|
|||
|
||||
---
|
||||
|
||||
<a id="installps1"></a>
|
||||
|
||||
## install.ps1
|
||||
|
||||
### Flow (install.ps1)
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ openclaw devices list \
|
|||
--token "$(sed -n 's/^OPENCLAW_GATEWAY_TOKEN=//p' ~/.openclaw/.env | head -n1)"
|
||||
```
|
||||
|
||||
<a id="podman--tailscale"></a>
|
||||
|
||||
## Podman + Tailscale
|
||||
|
||||
For HTTPS or remote browser access, follow the main Tailscale docs.
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ dispatch.
|
|||
## Walkthrough
|
||||
|
||||
<Steps>
|
||||
<a id="step-1-package-and-manifest"></a>
|
||||
<Step title="Package and manifest">
|
||||
Create the standard plugin files. The `channel` field in `package.json` is
|
||||
what makes this a channel plugin:
|
||||
|
|
@ -298,8 +299,9 @@ dispatch.
|
|||
|
||||
</Step>
|
||||
|
||||
<Step title="Test">
|
||||
Write colocated tests in `src/channel.test.ts`:
|
||||
<a id="step-6-test"></a>
|
||||
<Step title="Test">
|
||||
Write colocated tests in `src/channel.test.ts`:
|
||||
|
||||
```typescript src/channel.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ API key auth, and dynamic model resolution.
|
|||
## Walkthrough
|
||||
|
||||
<Steps>
|
||||
<a id="step-1-package-and-manifest"></a>
|
||||
<Step title="Package and manifest">
|
||||
<CodeGroup>
|
||||
```json package.json
|
||||
|
|
@ -319,6 +320,7 @@ API key auth, and dynamic model resolution.
|
|||
</Step>
|
||||
|
||||
<Step title="Add extra capabilities (optional)">
|
||||
<a id="step-5-add-extra-capabilities"></a>
|
||||
A provider plugin can register speech, media understanding, image
|
||||
generation, and web search alongside text inference:
|
||||
|
||||
|
|
@ -360,6 +362,7 @@ API key auth, and dynamic model resolution.
|
|||
</Step>
|
||||
|
||||
<Step title="Test">
|
||||
<a id="step-6-test"></a>
|
||||
```typescript src/provider.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
// Export your provider config object from index.ts or a dedicated file
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ background.
|
|||
|
||||
## Recommended: Model Studio (Alibaba Cloud Coding Plan)
|
||||
|
||||
Use [Model Studio](/providers/modelstudio) for officially supported access to
|
||||
Use [Model Studio](/providers/qwen_modelstudio) for officially supported access to
|
||||
Qwen models (Qwen 3.5 Plus, GLM-4.7, Kimi K2.5, and more).
|
||||
|
||||
```bash
|
||||
|
|
@ -30,4 +30,4 @@ openclaw onboard --auth-choice modelstudio-api-key
|
|||
openclaw onboard --auth-choice modelstudio-api-key-cn
|
||||
```
|
||||
|
||||
See [Model Studio](/providers/modelstudio) for full setup details.
|
||||
See [Model Studio](/providers/qwen_modelstudio) for full setup details.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel.
|
|||
- If `gateway.auth.token` is configured as a SecretRef and is unresolved in your current shell, `openclaw dashboard` still prints a non-tokenized URL plus actionable auth setup guidance.
|
||||
- **Not localhost**: use Tailscale Serve (tokenless for Control UI/WebSocket if `gateway.auth.allowTailscale: true`, assumes trusted gateway host; HTTP APIs still need token/password), tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web).
|
||||
|
||||
<a id="if-you-see-unauthorized-1008"></a>
|
||||
|
||||
## If you see "unauthorized" / 1008
|
||||
|
||||
- Ensure the gateway is reachable (local: `openclaw status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/`).
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
|
|
@ -67,8 +68,12 @@ for (const item of docsConfig.redirects || []) {
|
|||
const allFiles = walk(DOCS_DIR);
|
||||
const relAllFiles = new Set(allFiles.map((abs) => normalizeSlashes(path.relative(DOCS_DIR, abs))));
|
||||
|
||||
function isLocalizedDocPath(p) {
|
||||
return /^\/?[a-z]{2}(?:-[A-Za-z]{2,8})+\//.test(p);
|
||||
}
|
||||
|
||||
function isGeneratedTranslatedDoc(relPath) {
|
||||
return relPath.startsWith("zh-CN/");
|
||||
return isLocalizedDocPath(relPath);
|
||||
}
|
||||
|
||||
const markdownFiles = allFiles.filter((abs) => {
|
||||
|
|
@ -169,6 +174,95 @@ function collectNavPageEntries(node) {
|
|||
|
||||
const markdownLinkRegex = /!?\[[^\]]*\]\(([^)]+)\)/g;
|
||||
|
||||
export function sanitizeDocsConfigForEnglishOnly(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((item) => sanitizeDocsConfigForEnglishOnly(item))
|
||||
.filter((item) => item !== undefined);
|
||||
}
|
||||
|
||||
if (!value || typeof value !== "object") {
|
||||
if (typeof value === "string" && isLocalizedDocPath(value)) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const record = /** @type {Record<string, unknown>} */ (value);
|
||||
if (typeof record.language === "string" && record.language !== "en") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** @type {Record<string, unknown>} */
|
||||
const sanitized = {};
|
||||
for (const [key, child] of Object.entries(record)) {
|
||||
const next = sanitizeDocsConfigForEnglishOnly(child);
|
||||
if (next === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(next) && next.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
next &&
|
||||
typeof next === "object" &&
|
||||
!Array.isArray(next) &&
|
||||
Object.keys(next).length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
sanitized[key] = next;
|
||||
}
|
||||
|
||||
if (record.pages && !Array.isArray(sanitized.pages)) {
|
||||
return undefined;
|
||||
}
|
||||
if (record.groups && !Array.isArray(sanitized.groups)) {
|
||||
return undefined;
|
||||
}
|
||||
if (record.tabs && !Array.isArray(sanitized.tabs)) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
"source" in record &&
|
||||
typeof record.source === "string" &&
|
||||
typeof sanitized.source !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
"destination" in record &&
|
||||
typeof record.destination === "string" &&
|
||||
typeof sanitized.destination !== "string"
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
|
||||
}
|
||||
|
||||
export function prepareAnchorAuditDocsDir(sourceDir = DOCS_DIR) {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-docs-anchor-audit-"));
|
||||
fs.cpSync(sourceDir, tempDir, { recursive: true });
|
||||
|
||||
for (const entry of fs.readdirSync(tempDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (!isGeneratedTranslatedDoc(`${entry.name}/`)) {
|
||||
continue;
|
||||
}
|
||||
fs.rmSync(path.join(tempDir, entry.name), { recursive: true, force: true });
|
||||
}
|
||||
|
||||
const docsJsonPath = path.join(tempDir, "docs.json");
|
||||
const docsConfig = JSON.parse(fs.readFileSync(docsJsonPath, "utf8"));
|
||||
const sanitized = sanitizeDocsConfigForEnglishOnly(docsConfig);
|
||||
fs.writeFileSync(docsJsonPath, `${JSON.stringify(sanitized, null, 2)}\n`, "utf8");
|
||||
|
||||
return tempDir;
|
||||
}
|
||||
|
||||
export function auditDocsLinks() {
|
||||
/** @type {{file: string; line: number; link: string; reason: string}[]} */
|
||||
const broken = [];
|
||||
|
|
@ -293,26 +387,39 @@ export function auditDocsLinks() {
|
|||
* @param {{
|
||||
* args?: string[];
|
||||
* spawnSyncImpl?: typeof spawnSync;
|
||||
* prepareAnchorAuditDocsDirImpl?: (sourceDir?: string) => string;
|
||||
* cleanupAnchorAuditDocsDirImpl?: (dir: string) => void;
|
||||
* }} [options]
|
||||
*/
|
||||
export function runDocsLinkAuditCli(options = {}) {
|
||||
const args = options.args ?? process.argv.slice(2);
|
||||
if (args.includes("--anchors")) {
|
||||
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
|
||||
const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], {
|
||||
cwd: DOCS_DIR,
|
||||
stdio: "inherit",
|
||||
});
|
||||
const prepareAnchorAuditDocsDirImpl =
|
||||
options.prepareAnchorAuditDocsDirImpl ?? prepareAnchorAuditDocsDir;
|
||||
const cleanupAnchorAuditDocsDirImpl =
|
||||
options.cleanupAnchorAuditDocsDirImpl ??
|
||||
((dir) => fs.rmSync(dir, { recursive: true, force: true }));
|
||||
const anchorDocsDir = prepareAnchorAuditDocsDirImpl(DOCS_DIR);
|
||||
|
||||
if (result.error?.code === "ENOENT") {
|
||||
const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], {
|
||||
cwd: DOCS_DIR,
|
||||
try {
|
||||
const result = spawnSyncImpl("mint", ["broken-links", "--check-anchors"], {
|
||||
cwd: anchorDocsDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
return fallback.status ?? 1;
|
||||
}
|
||||
|
||||
return result.status ?? 1;
|
||||
if (result.error?.code === "ENOENT") {
|
||||
const fallback = spawnSyncImpl("pnpm", ["dlx", "mint", "broken-links", "--check-anchors"], {
|
||||
cwd: anchorDocsDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
return fallback.status ?? 1;
|
||||
}
|
||||
|
||||
return result.status ?? 1;
|
||||
} finally {
|
||||
cleanupAnchorAuditDocsDirImpl(anchorDocsDir);
|
||||
}
|
||||
}
|
||||
|
||||
const { checked, broken } = auditDocsLinks();
|
||||
|
|
|
|||
|
|
@ -1,22 +1,33 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const { normalizeRoute, resolveRoute, runDocsLinkAuditCli } =
|
||||
(await import("../../scripts/docs-link-audit.mjs")) as unknown as {
|
||||
normalizeRoute: (route: string) => string;
|
||||
resolveRoute: (
|
||||
route: string,
|
||||
options?: { redirects?: Map<string, string>; routes?: Set<string> },
|
||||
) => { ok: boolean; terminal: string; loop?: boolean };
|
||||
runDocsLinkAuditCli: (options?: {
|
||||
args?: string[];
|
||||
spawnSyncImpl?: (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string; stdio: string },
|
||||
) => { status: number | null; error?: { code?: string } };
|
||||
}) => number;
|
||||
};
|
||||
const {
|
||||
normalizeRoute,
|
||||
prepareAnchorAuditDocsDir,
|
||||
resolveRoute,
|
||||
runDocsLinkAuditCli,
|
||||
sanitizeDocsConfigForEnglishOnly,
|
||||
} = (await import("../../scripts/docs-link-audit.mjs")) as unknown as {
|
||||
normalizeRoute: (route: string) => string;
|
||||
prepareAnchorAuditDocsDir: (sourceDir?: string) => string;
|
||||
resolveRoute: (
|
||||
route: string,
|
||||
options?: { redirects?: Map<string, string>; routes?: Set<string> },
|
||||
) => { ok: boolean; terminal: string; loop?: boolean };
|
||||
runDocsLinkAuditCli: (options?: {
|
||||
args?: string[];
|
||||
spawnSyncImpl?: (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: { cwd: string; stdio: string },
|
||||
) => { status: number | null; error?: { code?: string } };
|
||||
prepareAnchorAuditDocsDirImpl?: (sourceDir?: string) => string;
|
||||
cleanupAnchorAuditDocsDirImpl?: (dir: string) => void;
|
||||
}) => number;
|
||||
sanitizeDocsConfigForEnglishOnly: (value: unknown) => unknown;
|
||||
};
|
||||
|
||||
describe("docs-link-audit", () => {
|
||||
it("normalizes route fragments away", () => {
|
||||
|
|
@ -38,6 +49,101 @@ describe("docs-link-audit", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("sanitizes docs.json to English-only route targets", () => {
|
||||
expect(
|
||||
sanitizeDocsConfigForEnglishOnly({
|
||||
navigation: [
|
||||
{
|
||||
language: "en",
|
||||
tabs: [
|
||||
{
|
||||
tab: "Docs",
|
||||
groups: [
|
||||
{
|
||||
group: "Keep",
|
||||
pages: ["help/testing", "zh-CN/help/testing", "ja-JP/help/testing"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
language: "zh-Hans",
|
||||
tabs: [{ tab: "中文", groups: [{ group: "帮助", pages: ["zh-CN/help/testing"] }] }],
|
||||
},
|
||||
],
|
||||
redirects: [
|
||||
{ source: "/help/testing", destination: "/help/testing" },
|
||||
{ source: "/zh-CN/help/testing", destination: "/help/testing" },
|
||||
{ source: "/help/testing", destination: "/ja-JP/help/testing" },
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
navigation: [
|
||||
{
|
||||
language: "en",
|
||||
tabs: [
|
||||
{
|
||||
tab: "Docs",
|
||||
groups: [{ group: "Keep", pages: ["help/testing"] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
redirects: [{ source: "/help/testing", destination: "/help/testing" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("builds an English-only docs tree for anchor audits", () => {
|
||||
const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "docs-link-audit-fixture-"));
|
||||
const docsRoot = path.join(fixtureRoot, "docs");
|
||||
fs.mkdirSync(path.join(docsRoot, "help"), { recursive: true });
|
||||
fs.mkdirSync(path.join(docsRoot, "zh-CN", "help"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(docsRoot, "docs.json"),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
navigation: [
|
||||
{
|
||||
language: "en",
|
||||
tabs: [{ tab: "Docs", groups: [{ group: "Help", pages: ["help/testing"] }] }],
|
||||
},
|
||||
{
|
||||
language: "zh-Hans",
|
||||
tabs: [{ tab: "中文", groups: [{ group: "帮助", pages: ["zh-CN/help/testing"] }] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(path.join(docsRoot, "help", "testing.md"), "# testing\n", "utf8");
|
||||
fs.writeFileSync(path.join(docsRoot, "zh-CN", "help", "testing.md"), "# 测试\n", "utf8");
|
||||
|
||||
const anchorDocsDir = prepareAnchorAuditDocsDir(docsRoot);
|
||||
try {
|
||||
expect(fs.existsSync(path.join(anchorDocsDir, "help", "testing.md"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(anchorDocsDir, "zh-CN"))).toBe(false);
|
||||
|
||||
const sanitizedDocsJson = JSON.parse(
|
||||
fs.readFileSync(path.join(anchorDocsDir, "docs.json"), "utf8"),
|
||||
);
|
||||
expect(sanitizedDocsJson).toEqual({
|
||||
navigation: [
|
||||
{
|
||||
language: "en",
|
||||
tabs: [{ tab: "Docs", groups: [{ group: "Help", pages: ["help/testing"] }] }],
|
||||
},
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(anchorDocsDir, { recursive: true, force: true });
|
||||
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("prefers a local mint binary for anchor validation", () => {
|
||||
let invocation:
|
||||
| {
|
||||
|
|
@ -46,9 +152,17 @@ describe("docs-link-audit", () => {
|
|||
options: { cwd: string; stdio: string };
|
||||
}
|
||||
| undefined;
|
||||
let cleanedDir: string | undefined;
|
||||
const anchorDocsDir = path.join(os.tmpdir(), "docs-link-audit-anchor");
|
||||
|
||||
const exitCode = runDocsLinkAuditCli({
|
||||
args: ["--anchors"],
|
||||
prepareAnchorAuditDocsDirImpl() {
|
||||
return anchorDocsDir;
|
||||
},
|
||||
cleanupAnchorAuditDocsDirImpl(dir) {
|
||||
cleanedDir = dir;
|
||||
},
|
||||
spawnSyncImpl(command, args, options) {
|
||||
invocation = { command, args, options };
|
||||
return { status: 0 };
|
||||
|
|
@ -60,7 +174,8 @@ describe("docs-link-audit", () => {
|
|||
expect(invocation?.command).toBe("mint");
|
||||
expect(invocation?.args).toEqual(["broken-links", "--check-anchors"]);
|
||||
expect(invocation?.options.stdio).toBe("inherit");
|
||||
expect(path.basename(invocation?.options.cwd ?? "")).toBe("docs");
|
||||
expect(invocation?.options.cwd).toBe(anchorDocsDir);
|
||||
expect(cleanedDir).toBe(anchorDocsDir);
|
||||
});
|
||||
|
||||
it("falls back to pnpm dlx when mint is not on PATH", () => {
|
||||
|
|
@ -69,9 +184,17 @@ describe("docs-link-audit", () => {
|
|||
args: string[];
|
||||
options: { cwd: string; stdio: string };
|
||||
}> = [];
|
||||
let cleanedDir: string | undefined;
|
||||
const anchorDocsDir = path.join(os.tmpdir(), "docs-link-audit-anchor");
|
||||
|
||||
const exitCode = runDocsLinkAuditCli({
|
||||
args: ["--anchors"],
|
||||
prepareAnchorAuditDocsDirImpl() {
|
||||
return anchorDocsDir;
|
||||
},
|
||||
cleanupAnchorAuditDocsDirImpl(dir) {
|
||||
cleanedDir = dir;
|
||||
},
|
||||
spawnSyncImpl(command, args, options) {
|
||||
invocations.push({ command, args, options });
|
||||
if (command === "mint") {
|
||||
|
|
@ -93,7 +216,8 @@ describe("docs-link-audit", () => {
|
|||
args: ["dlx", "mint", "broken-links", "--check-anchors"],
|
||||
options: { stdio: "inherit" },
|
||||
});
|
||||
expect(path.basename(invocations[0]?.options.cwd ?? "")).toBe("docs");
|
||||
expect(path.basename(invocations[1]?.options.cwd ?? "")).toBe("docs");
|
||||
expect(invocations[0]?.options.cwd).toBe(anchorDocsDir);
|
||||
expect(invocations[1]?.options.cwd).toBe(anchorDocsDir);
|
||||
expect(cleanedDir).toBe(anchorDocsDir);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue