Plugins: centralize compatibility formatting
This commit is contained in:
parent
7ba8dd112f
commit
5c4903d3fd
@ -10,6 +10,7 @@ import {
|
||||
buildAllPluginInspectReports,
|
||||
buildPluginInspectReport,
|
||||
buildPluginStatusReport,
|
||||
formatPluginCompatibilityNotice,
|
||||
type PluginStatusReport,
|
||||
} from "../../plugins/status.js";
|
||||
import { setPluginEnabledInConfig } from "../../plugins/toggle-config.js";
|
||||
@ -48,7 +49,7 @@ function buildPluginInspectJson(params: {
|
||||
compatibilityWarnings: inspect.compatibility.map((warning) => ({
|
||||
code: warning.code,
|
||||
severity: warning.severity,
|
||||
message: `${warning.pluginId} ${warning.message}`,
|
||||
message: formatPluginCompatibilityNotice(warning),
|
||||
})),
|
||||
install: params.config.plugins?.installs?.[inspect.plugin.id] ?? null,
|
||||
};
|
||||
@ -69,7 +70,7 @@ function buildAllPluginInspectJson(params: {
|
||||
compatibilityWarnings: inspect.compatibility.map((warning) => ({
|
||||
code: warning.code,
|
||||
severity: warning.severity,
|
||||
message: `${warning.pluginId} ${warning.message}`,
|
||||
message: formatPluginCompatibilityNotice(warning),
|
||||
})),
|
||||
install: params.config.plugins?.installs?.[inspect.plugin.id] ?? null,
|
||||
}));
|
||||
|
||||
168
src/cli/plugins-cli.test.ts
Normal file
168
src/cli/plugins-cli.test.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runRegisteredCli } from "../test-utils/command-runner.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({})),
|
||||
buildPluginStatusReport: vi.fn(() => ({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
})),
|
||||
buildPluginInspectReport: vi.fn(),
|
||||
buildAllPluginInspectReports: vi.fn(() => []),
|
||||
buildPluginCompatibilityNotices: vi.fn(() => []),
|
||||
defaultRuntime: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: mocks.loadConfig,
|
||||
writeConfigFile: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/status.js", () => ({
|
||||
buildPluginStatusReport: mocks.buildPluginStatusReport,
|
||||
buildPluginInspectReport: mocks.buildPluginInspectReport,
|
||||
buildAllPluginInspectReports: mocks.buildAllPluginInspectReports,
|
||||
buildPluginCompatibilityNotices: mocks.buildPluginCompatibilityNotices,
|
||||
}));
|
||||
|
||||
vi.mock("../runtime.js", () => ({
|
||||
defaultRuntime: mocks.defaultRuntime,
|
||||
}));
|
||||
|
||||
let registerPluginsCli: typeof import("./plugins-cli.js").registerPluginsCli;
|
||||
|
||||
beforeAll(async () => {
|
||||
({ registerPluginsCli } = await import("./plugins-cli.js"));
|
||||
});
|
||||
|
||||
describe("plugins cli", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mocks.loadConfig.mockReturnValue({});
|
||||
mocks.buildPluginStatusReport.mockReturnValue({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
});
|
||||
mocks.buildPluginInspectReport.mockReset();
|
||||
mocks.buildAllPluginInspectReports.mockReturnValue([]);
|
||||
mocks.buildPluginCompatibilityNotices.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("renders compatibility warnings in plugins inspect output", async () => {
|
||||
mocks.buildPluginStatusReport.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "legacy-plugin",
|
||||
name: "Legacy Plugin",
|
||||
description: "legacy seam",
|
||||
source: "/tmp/legacy.ts",
|
||||
origin: "workspace",
|
||||
enabled: true,
|
||||
status: "loaded",
|
||||
format: "openclaw",
|
||||
bundleFormat: undefined,
|
||||
version: "1.0.0",
|
||||
bundleCapabilities: [],
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
});
|
||||
mocks.buildPluginInspectReport.mockReturnValue({
|
||||
plugin: {
|
||||
id: "legacy-plugin",
|
||||
name: "Legacy Plugin",
|
||||
description: "legacy seam",
|
||||
source: "/tmp/legacy.ts",
|
||||
origin: "workspace",
|
||||
status: "loaded",
|
||||
format: "openclaw",
|
||||
bundleFormat: undefined,
|
||||
version: "1.0.0",
|
||||
bundleCapabilities: [],
|
||||
},
|
||||
shape: "hook-only",
|
||||
capabilityMode: "none",
|
||||
capabilityCount: 0,
|
||||
capabilities: [],
|
||||
typedHooks: [{ name: "before_agent_start" }],
|
||||
customHooks: [],
|
||||
tools: [],
|
||||
commands: [],
|
||||
cliCommands: [],
|
||||
services: [],
|
||||
gatewayMethods: [],
|
||||
httpRouteCount: 0,
|
||||
diagnostics: [],
|
||||
policy: {
|
||||
allowPromptInjection: undefined,
|
||||
allowModelOverride: undefined,
|
||||
allowedModels: [],
|
||||
hasAllowedModelsConfig: false,
|
||||
},
|
||||
usesLegacyBeforeAgentStart: true,
|
||||
compatibility: [
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "hook-only",
|
||||
severity: "info",
|
||||
message:
|
||||
"is hook-only; this remains supported for compatibility, but it has not migrated to explicit capability registration.",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await runRegisteredCli({
|
||||
register: registerPluginsCli as (program: import("commander").Command) => void,
|
||||
argv: ["plugins", "inspect", "legacy-plugin"],
|
||||
});
|
||||
|
||||
const output = mocks.defaultRuntime.log.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain("Compatibility warnings");
|
||||
expect(output).toContain("legacy-plugin still relies on legacy before_agent_start");
|
||||
expect(output).toContain("legacy-plugin is hook-only");
|
||||
});
|
||||
|
||||
it("renders compatibility notices in plugins doctor", async () => {
|
||||
mocks.buildPluginStatusReport.mockReturnValue({
|
||||
plugins: [],
|
||||
diagnostics: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
});
|
||||
mocks.buildPluginCompatibilityNotices.mockReturnValue([
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start",
|
||||
severity: "warn",
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
},
|
||||
]);
|
||||
|
||||
await runRegisteredCli({
|
||||
register: registerPluginsCli as (program: import("commander").Command) => void,
|
||||
argv: ["plugins", "doctor"],
|
||||
});
|
||||
|
||||
const output = mocks.defaultRuntime.log.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain("Compatibility:");
|
||||
expect(output).toContain("legacy-plugin");
|
||||
expect(output).toContain("still relies on legacy before_agent_start");
|
||||
});
|
||||
});
|
||||
@ -25,6 +25,7 @@ import {
|
||||
buildPluginCompatibilityNotices,
|
||||
buildPluginInspectReport,
|
||||
buildPluginStatusReport,
|
||||
formatPluginCompatibilityNotice,
|
||||
} from "../plugins/status.js";
|
||||
import { resolveUninstallDirectoryTarget, uninstallPlugin } from "../plugins/uninstall.js";
|
||||
import { updateNpmInstalledPlugins } from "../plugins/update.js";
|
||||
@ -762,7 +763,7 @@ export function registerPluginsCli(program: Command) {
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"Compatibility warnings",
|
||||
inspect.compatibility.map((warning) => `${warning.pluginId} ${warning.message}`),
|
||||
inspect.compatibility.map(formatPluginCompatibilityNotice),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
@ -1103,7 +1104,7 @@ export function registerPluginsCli(program: Command) {
|
||||
lines.push(theme.warn("Compatibility:"));
|
||||
for (const notice of compatibility) {
|
||||
const marker = notice.severity === "warn" ? theme.warn("warn") : theme.muted("info");
|
||||
lines.push(`- ${notice.pluginId} [${marker}]: ${notice.message}`);
|
||||
lines.push(`- ${formatPluginCompatibilityNotice(notice)} [${marker}]`);
|
||||
}
|
||||
}
|
||||
const docs = formatDocsLink("/plugin", "docs.openclaw.ai/plugin");
|
||||
|
||||
@ -6,7 +6,10 @@ import {
|
||||
type RestartSentinelPayload,
|
||||
summarizeRestartSentinel,
|
||||
} from "../../infra/restart-sentinel.js";
|
||||
import type { PluginCompatibilityNotice } from "../../plugins/status.js";
|
||||
import {
|
||||
formatPluginCompatibilityNotice,
|
||||
type PluginCompatibilityNotice,
|
||||
} from "../../plugins/status.js";
|
||||
import { formatTimeAgo, redactSecrets } from "./format.js";
|
||||
import { readFileTailLines, summarizeLogTail } from "./gateway.js";
|
||||
|
||||
@ -184,7 +187,7 @@ export async function appendStatusAllDiagnosis(params: {
|
||||
);
|
||||
for (const notice of params.pluginCompatibility.slice(0, 12)) {
|
||||
const severity = notice.severity === "warn" ? "warn" : "info";
|
||||
lines.push(` - ${notice.pluginId} [${severity}] ${notice.message}`);
|
||||
lines.push(` - [${severity}] ${formatPluginCompatibilityNotice(notice)}`);
|
||||
}
|
||||
if (params.pluginCompatibility.length > 12) {
|
||||
lines.push(` ${muted(`… +${params.pluginCompatibility.length - 12} more`)}`);
|
||||
|
||||
@ -13,6 +13,10 @@ import {
|
||||
resolveMemoryVectorState,
|
||||
type Tone,
|
||||
} from "../memory/status-format.js";
|
||||
import {
|
||||
formatPluginCompatibilityNotice,
|
||||
summarizePluginCompatibility,
|
||||
} from "../plugins/status.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
@ -421,11 +425,12 @@ export async function statusCommand(
|
||||
const updateLine = formatUpdateOneLiner(update).replace(/^Update:\s*/i, "");
|
||||
const channelLabel = channelInfo.label;
|
||||
const gitLabel = formatGitInstallLabel(update);
|
||||
const pluginCompatibilitySummary = summarizePluginCompatibility(pluginCompatibility);
|
||||
const pluginCompatibilityValue =
|
||||
pluginCompatibility.length === 0
|
||||
pluginCompatibilitySummary.noticeCount === 0
|
||||
? ok("none")
|
||||
: warn(
|
||||
`${pluginCompatibility.length} notice${pluginCompatibility.length === 1 ? "" : "s"} · ${new Set(pluginCompatibility.map((entry) => entry.pluginId)).size} plugin${new Set(pluginCompatibility.map((entry) => entry.pluginId)).size === 1 ? "" : "s"}`,
|
||||
`${pluginCompatibilitySummary.noticeCount} notice${pluginCompatibilitySummary.noticeCount === 1 ? "" : "s"} · ${pluginCompatibilitySummary.pluginCount} plugin${pluginCompatibilitySummary.pluginCount === 1 ? "" : "s"}`,
|
||||
);
|
||||
|
||||
const overviewRows = [
|
||||
@ -484,7 +489,7 @@ export async function statusCommand(
|
||||
runtime.log(theme.heading("Plugin compatibility"));
|
||||
for (const notice of pluginCompatibility.slice(0, 8)) {
|
||||
const label = notice.severity === "warn" ? theme.warn("WARN") : theme.muted("INFO");
|
||||
runtime.log(` ${label} ${notice.pluginId} ${notice.message}`);
|
||||
runtime.log(` ${label} ${formatPluginCompatibilityNotice(notice)}`);
|
||||
}
|
||||
if (pluginCompatibility.length > 8) {
|
||||
runtime.log(theme.muted(` … +${pluginCompatibility.length - 8} more`));
|
||||
|
||||
@ -7,6 +7,8 @@ let buildPluginInspectReport: typeof import("./status.js").buildPluginInspectRep
|
||||
let buildAllPluginInspectReports: typeof import("./status.js").buildAllPluginInspectReports;
|
||||
let buildPluginCompatibilityNotices: typeof import("./status.js").buildPluginCompatibilityNotices;
|
||||
let buildPluginCompatibilityWarnings: typeof import("./status.js").buildPluginCompatibilityWarnings;
|
||||
let formatPluginCompatibilityNotice: typeof import("./status.js").formatPluginCompatibilityNotice;
|
||||
let summarizePluginCompatibility: typeof import("./status.js").summarizePluginCompatibility;
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
loadConfig: () => loadConfigMock(),
|
||||
@ -56,6 +58,8 @@ describe("buildPluginStatusReport", () => {
|
||||
buildPluginCompatibilityWarnings,
|
||||
buildPluginInspectReport,
|
||||
buildPluginStatusReport,
|
||||
formatPluginCompatibilityNotice,
|
||||
summarizePluginCompatibility,
|
||||
} = await import("./status.js"));
|
||||
});
|
||||
|
||||
@ -488,4 +492,33 @@ describe("buildPluginStatusReport", () => {
|
||||
expect(buildPluginCompatibilityNotices()).toEqual([]);
|
||||
expect(buildPluginCompatibilityWarnings()).toEqual([]);
|
||||
});
|
||||
|
||||
it("formats and summarizes compatibility notices", () => {
|
||||
const notice = {
|
||||
pluginId: "legacy-plugin",
|
||||
code: "legacy-before-agent-start" as const,
|
||||
severity: "warn" as const,
|
||||
message:
|
||||
"still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
};
|
||||
|
||||
expect(formatPluginCompatibilityNotice(notice)).toBe(
|
||||
"legacy-plugin still relies on legacy before_agent_start; keep upgrade coverage on this plugin and prefer before_model_resolve/before_prompt_build for new work.",
|
||||
);
|
||||
expect(
|
||||
summarizePluginCompatibility([
|
||||
notice,
|
||||
{
|
||||
pluginId: "legacy-plugin",
|
||||
code: "hook-only",
|
||||
severity: "info",
|
||||
message:
|
||||
"is hook-only; this remains supported for compatibility, but it has not migrated to explicit capability registration.",
|
||||
},
|
||||
]),
|
||||
).toEqual({
|
||||
noticeCount: 2,
|
||||
pluginCount: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -33,6 +33,11 @@ export type PluginCompatibilityNotice = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type PluginCompatibilitySummary = {
|
||||
noticeCount: number;
|
||||
pluginCount: number;
|
||||
};
|
||||
|
||||
export type PluginInspectReport = {
|
||||
workspaceDir?: string;
|
||||
plugin: PluginRegistry["plugins"][number];
|
||||
@ -288,9 +293,7 @@ export function buildPluginCompatibilityWarnings(params?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
report?: PluginStatusReport;
|
||||
}): string[] {
|
||||
return buildAllPluginInspectReports(params).flatMap((inspect) =>
|
||||
inspect.compatibility.map((warning) => `${warning.pluginId} ${warning.message}`),
|
||||
);
|
||||
return buildPluginCompatibilityNotices(params).map(formatPluginCompatibilityNotice);
|
||||
}
|
||||
|
||||
export function buildPluginCompatibilityNotices(params?: {
|
||||
@ -301,3 +304,16 @@ export function buildPluginCompatibilityNotices(params?: {
|
||||
}): PluginCompatibilityNotice[] {
|
||||
return buildAllPluginInspectReports(params).flatMap((inspect) => inspect.compatibility);
|
||||
}
|
||||
|
||||
export function formatPluginCompatibilityNotice(notice: PluginCompatibilityNotice): string {
|
||||
return `${notice.pluginId} ${notice.message}`;
|
||||
}
|
||||
|
||||
export function summarizePluginCompatibility(
|
||||
notices: PluginCompatibilityNotice[],
|
||||
): PluginCompatibilitySummary {
|
||||
return {
|
||||
noticeCount: notices.length,
|
||||
pluginCount: new Set(notices.map((notice) => notice.pluginId)).size,
|
||||
};
|
||||
}
|
||||
|
||||
@ -13,7 +13,10 @@ import {
|
||||
writeConfigFile,
|
||||
} from "../config/config.js";
|
||||
import { normalizeSecretInputString } from "../config/types.secrets.js";
|
||||
import { buildPluginCompatibilityNotices } from "../plugins/status.js";
|
||||
import {
|
||||
buildPluginCompatibilityNotices,
|
||||
formatPluginCompatibilityNotice,
|
||||
} from "../plugins/status.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@ -112,7 +115,7 @@ export async function runSetupWizard(
|
||||
`Detected ${compatibilityNotices.length} plugin compatibility notice${compatibilityNotices.length === 1 ? "" : "s"} in the current config.`,
|
||||
...compatibilityNotices
|
||||
.slice(0, 4)
|
||||
.map((notice) => `- ${notice.pluginId}: ${notice.message}`),
|
||||
.map((notice) => `- ${formatPluginCompatibilityNotice(notice)}`),
|
||||
...(compatibilityNotices.length > 4
|
||||
? [`- ... +${compatibilityNotices.length - 4} more`]
|
||||
: []),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user