@@ -4,6 +4,8 @@ import { describe, expect, it, vi } from "vitest";
44import { i18n } from "../i18n/index.ts" ;
55import {
66 md ,
7+ OPENCLAW_DOCS_MARKDOWN_OPTIONS ,
8+ OPENCLAW_MISSION_CONTROL_MARKDOWN_OPTIONS ,
79 toSanitizedMarkdownHtml ,
810 toStreamingMarkdownHtml ,
911 toStreamingPlainTextHtml ,
603605 ) ;
604606 } ) ;
605607
606- it ( "rewrites docs-root links to the public docs host" , ( ) => {
608+ it ( "keeps docs-root links local by default" , ( ) => {
609+ const html = toSanitizedMarkdownHtml ( "[workspace](/concepts/agent-workspace)" ) ;
610+ expect ( html ) . toBe (
611+ '<p><a href="/concepts/agent-workspace" rel="noreferrer noopener" target="_blank">workspace</a></p>\n' ,
612+ ) ;
613+ } ) ;
614+
615+ it ( "rewrites docs-root links to the public docs host in docs-link mode" , ( ) => {
607616 const html = toSanitizedMarkdownHtml (
608617 "[workspace](/concepts/agent-workspace) [hooks](/automation/hooks#session-memory) [telegram](/channels/telegram?tab=setup) [shortlink](/telegram) [openai](/openai) [images](/images) [groups](/groups) [camera](/nodes/camera) [macOS](/platforms/macos) [cliSessions](/cli/sessions) [toolSkills](/tools/skills) [pluginDocs](/plugins/reference/diffs) [prose](/prose) [refactor](/refactor/ingress-core)" ,
618+ OPENCLAW_DOCS_MARKDOWN_OPTIONS ,
609619 ) ;
610620 expect ( html ) . toBe (
611621 '<p><a href="https://docs.openclaw.ai/concepts/agent-workspace" rel="noreferrer noopener" target="_blank">workspace</a> <a href="https://docs.openclaw.ai/automation/hooks#session-memory" rel="noreferrer noopener" target="_blank">hooks</a> <a href="https://docs.openclaw.ai/channels/telegram?tab=setup" rel="noreferrer noopener" target="_blank">telegram</a> <a href="https://docs.openclaw.ai/telegram" rel="noreferrer noopener" target="_blank">shortlink</a> <a href="https://docs.openclaw.ai/openai" rel="noreferrer noopener" target="_blank">openai</a> <a href="https://docs.openclaw.ai/images" rel="noreferrer noopener" target="_blank">images</a> <a href="https://docs.openclaw.ai/groups" rel="noreferrer noopener" target="_blank">groups</a> <a href="https://docs.openclaw.ai/nodes/camera" rel="noreferrer noopener" target="_blank">camera</a> <a href="https://docs.openclaw.ai/platforms/macos" rel="noreferrer noopener" target="_blank">macOS</a> <a href="https://docs.openclaw.ai/cli/sessions" rel="noreferrer noopener" target="_blank">cliSessions</a> <a href="https://docs.openclaw.ai/tools/skills" rel="noreferrer noopener" target="_blank">toolSkills</a> <a href="https://docs.openclaw.ai/plugins/reference/diffs" rel="noreferrer noopener" target="_blank">pluginDocs</a> <a href="https://docs.openclaw.ai/prose" rel="noreferrer noopener" target="_blank">prose</a> <a href="https://docs.openclaw.ai/refactor/ingress-core" rel="noreferrer noopener" target="_blank">refactor</a></p>\n' ,
612622 ) ;
613623 } ) ;
614624
615- it ( "keeps app and resource routes instead of treating them as docs roots " , ( ) => {
625+ it ( "keeps app and resource routes in Mission Control docs-link mode " , ( ) => {
616626 const html = withControlUiBasePath ( "/control" , ( ) =>
617627 toSanitizedMarkdownHtml (
618628 "[channels](/channels) [automation](/automation) [workshop](/skills/workshop) [chat](/chat) [baseChat](/control/chat?session=abc) [baseSessions](/control/sessions) [health](/healthz) [pluginDynamic](/googlechat) [asset](/api/files/1) [baseApi](/control/api/files/1) [baseAvatar](/control/avatar/main) [plugin](/plugins/diffs/view/id/token) [basePlugin](/control/plugins/diffs/view/id/token) [artifact](/__openclaw__/canvas/documents/x/index.html) [baseArtifact](/control/__openclaw__/canvas/x)" ,
629+ OPENCLAW_MISSION_CONTROL_MARKDOWN_OPTIONS ,
619630 ) ,
620631 ) ;
621632 expect ( html ) . toBe (
622633 '<p><a href="/channels" rel="noreferrer noopener" target="_blank">channels</a> <a href="/automation" rel="noreferrer noopener" target="_blank">automation</a> <a href="/skills/workshop" rel="noreferrer noopener" target="_blank">workshop</a> <a href="/chat" rel="noreferrer noopener" target="_blank">chat</a> <a href="/control/chat?session=abc" rel="noreferrer noopener" target="_blank">baseChat</a> <a href="/control/sessions" rel="noreferrer noopener" target="_blank">baseSessions</a> <a href="/healthz" rel="noreferrer noopener" target="_blank">health</a> <a href="/googlechat" rel="noreferrer noopener" target="_blank">pluginDynamic</a> <a href="/api/files/1" rel="noreferrer noopener" target="_blank">asset</a> <a href="/control/api/files/1" rel="noreferrer noopener" target="_blank">baseApi</a> <a href="/control/avatar/main" rel="noreferrer noopener" target="_blank">baseAvatar</a> <a href="/plugins/diffs/view/id/token" rel="noreferrer noopener" target="_blank">plugin</a> <a href="/control/plugins/diffs/view/id/token" rel="noreferrer noopener" target="_blank">basePlugin</a> <a href="/__openclaw__/canvas/documents/x/index.html" rel="noreferrer noopener" target="_blank">artifact</a> <a href="/control/__openclaw__/canvas/x" rel="noreferrer noopener" target="_blank">baseArtifact</a></p>\n' ,
623634 ) ;
624635 } ) ;
636+
637+ it ( "rewrites docs subpaths that share a segment with Control UI routes" , ( ) => {
638+ const html = toSanitizedMarkdownHtml (
639+ "[Channel pairing](/channels/pairing) [Cron jobs](/automation/cron-jobs)" ,
640+ OPENCLAW_MISSION_CONTROL_MARKDOWN_OPTIONS ,
641+ ) ;
642+ expect ( html ) . toBe (
643+ '<p><a href="https://docs.openclaw.ai/channels/pairing" rel="noreferrer noopener" target="_blank">Channel pairing</a> <a href="https://docs.openclaw.ai/automation/cron-jobs" rel="noreferrer noopener" target="_blank">Cron jobs</a></p>\n' ,
644+ ) ;
645+ } ) ;
646+
647+ it ( "caches default and docs-link rendering separately" , ( ) => {
648+ const markdown = "[Nodes](/nodes)" ;
649+ const local = toSanitizedMarkdownHtml ( markdown ) ;
650+ const docs = toSanitizedMarkdownHtml ( markdown , OPENCLAW_DOCS_MARKDOWN_OPTIONS ) ;
651+ const missionControl = toSanitizedMarkdownHtml (
652+ markdown ,
653+ OPENCLAW_MISSION_CONTROL_MARKDOWN_OPTIONS ,
654+ ) ;
655+ expect ( local ) . toBe (
656+ '<p><a href="/nodes" rel="noreferrer noopener" target="_blank">Nodes</a></p>\n' ,
657+ ) ;
658+ expect ( docs ) . toBe (
659+ '<p><a href="https://docs.openclaw.ai/nodes" rel="noreferrer noopener" target="_blank">Nodes</a></p>\n' ,
660+ ) ;
661+ expect ( missionControl ) . toBe (
662+ '<p><a href="/nodes" rel="noreferrer noopener" target="_blank">Nodes</a></p>\n' ,
663+ ) ;
664+ } ) ;
665+
666+ it ( "preserves docs path query strings and hashes when rewriting" , ( ) => {
667+ const html = toSanitizedMarkdownHtml (
668+ "[Control UI](/web/control-ui?from=dashboard#device-pairing-first-connection)" ,
669+ OPENCLAW_MISSION_CONTROL_MARKDOWN_OPTIONS ,
670+ ) ;
671+ expect ( html ) . toBe (
672+ '<p><a href="https://docs.openclaw.ai/web/control-ui?from=dashboard#device-pairing-first-connection" rel="noreferrer noopener" target="_blank">Control UI</a></p>\n' ,
673+ ) ;
674+ } ) ;
625675 } ) ;
626676
627677 describe ( "ReDoS protection" , ( ) => {
0 commit comments