mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
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:
31
src/terminal/links.test.ts
Normal file
31
src/terminal/links.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user