fix(terminal): tolerate undefined path in formatDocsLink (#67076, #67074) (#67086)

formatDocsLink called path.trim() unconditionally. The typed contract
says 'docsPath: string' (required on ChannelMeta), but a handful of
channel plugins and catalog rows leave it unset at runtime, so
onboarding flows that call formatChannelSelectionLine(entry.meta, ...)
hit a TypeError on the first meta without a docsPath:

  TypeError: Cannot read properties of undefined (reading 'trim')

Symptom: 'openclaw onboard --install-daemon' and the 'Select channel
(QuickStart)' -> 'Skip for now' path both crash on 2026.4.12 and
2026.4.14.

Fix: widen formatDocsLink's path parameter to 'string | undefined |
null' and fall back to the docs root when path is missing. The single
call site that guards with 'if (params.docsPath)' stays fine; the
unguarded channel-selection path now degrades gracefully.

Fixes #67076
Fixes #67074
This commit is contained in:
hcl
2026-04-16 02:10:52 +08:00
committed by GitHub
parent 2bfd808a83
commit be7f4a2342
2 changed files with 42 additions and 5 deletions

View File

@@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { formatDocsLink } from "./links.js";
describe("formatDocsLink", () => {
it("prepends the docs root when given a relative path", () => {
const out = formatDocsLink("/channels/telegram", "telegram");
expect(out).toContain("https://docs.openclaw.ai/channels/telegram");
});
it("preserves an absolute http url", () => {
const out = formatDocsLink("https://example.com/page", "page");
expect(out).toContain("https://example.com/page");
});
it("treats whitespace-only path like an empty path and falls back to docs root", () => {
const out = formatDocsLink(" ", "root");
expect(out).toContain("https://docs.openclaw.ai");
});
it("does not crash when path is undefined (regression: #67076, #67074)", () => {
expect(() =>
formatDocsLink(undefined as unknown as string, "label"),
).not.toThrow();
const out = formatDocsLink(undefined as unknown as string, "label");
expect(out).toContain("https://docs.openclaw.ai");
});
it("does not crash when path is null", () => {
expect(() => formatDocsLink(null as unknown as string)).not.toThrow();
});
});

View File

@@ -5,15 +5,21 @@ function resolveDocsRoot(): string {
}
export function formatDocsLink(
path: string,
path: string | undefined | null,
label?: string,
opts?: { fallback?: string; force?: boolean },
): string {
const trimmed = path.trim();
const docsRoot = resolveDocsRoot();
const url = trimmed.startsWith("http")
? trimmed
: `${docsRoot}${trimmed.startsWith("/") ? trimmed : `/${trimmed}`}`;
const trimmed = typeof path === "string" ? path.trim() : "";
// When a caller has no docsPath, link to the docs root rather than crashing
// the onboarding/channel-selection flows that pass meta.docsPath through
// here unguarded. The typed contract says docsPath is required, but a
// handful of channel plugins and catalog rows leave it unset at runtime.
const url = trimmed
? trimmed.startsWith("http")
? trimmed
: `${docsRoot}${trimmed.startsWith("/") ? trimmed : `/${trimmed}`}`
: docsRoot;
return formatTerminalLink(label ?? url, url, {
fallback: opts?.fallback ?? url,
force: opts?.force,