mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(plugins): tighten shadowed install diagnostics
This commit is contained in:
@@ -25,6 +25,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/update: run `doctor --non-interactive --fix` after Control UI global package updates before reporting success, so legacy config is migrated before the gateway restart. Thanks @stevenchouai.
|
||||
- Gateway/cron: stop a lazy cron startup that loses a hot-reload race, preventing the old cron service from starting after reload has already replaced cron state.
|
||||
- CLI/plugins: warn when npm plugin installs remain shadowed by a failing config-selected source and surface the repair path in `plugins doctor`. Thanks @LindalyX-Lee.
|
||||
- Active Memory: apply `setupGraceTimeoutMs` to the embedded recall runner as well as the outer prompt-build watchdog, so very-cold first recalls keep the configured setup grace end-to-end. (#74480) Thanks @volcano303.
|
||||
- CLI/config: keep JSON dry-run patches validating touched channel configuration against bundled channel schemas even when the patch only contains SecretRef objects.
|
||||
- Plugins/tools: keep disabled bundled tool plugins out of explicit runtime allowlist ownership and fall back from loaded-but-empty channel registries to tool-bearing plugin registries, so Active Memory can use bundled `memory-core` search/get tools even when `memory-lancedb` is disabled. Fixes #76603. Thanks @jwong-art.
|
||||
|
||||
@@ -112,6 +112,32 @@ describe("plugins cli list", () => {
|
||||
expect(output).toContain("openclaw plugins registry --refresh");
|
||||
});
|
||||
|
||||
it("does not report healthy config-selected plugin source shadowing as doctor issue", async () => {
|
||||
buildPluginDiagnosticsReport.mockReturnValue({
|
||||
plugins: [
|
||||
createPluginRecord({
|
||||
id: "discord",
|
||||
origin: "config",
|
||||
source: "/tmp/openclaw-upstream/extensions/discord/index.ts",
|
||||
status: "loaded",
|
||||
}),
|
||||
],
|
||||
diagnostics: [
|
||||
{
|
||||
level: "warn",
|
||||
pluginId: "discord",
|
||||
source: "/tmp/openclaw/npm/node_modules/@openclaw/discord/index.ts",
|
||||
message:
|
||||
"duplicate plugin id resolved by explicit config-selected plugin; global plugin will be overridden by config plugin (/tmp/openclaw-upstream/extensions/discord/index.ts)",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "doctor"]);
|
||||
|
||||
expect(runtimeLogs).toContain("No plugin issues detected.");
|
||||
});
|
||||
|
||||
it("reports persisted plugin registry state without refreshing", async () => {
|
||||
inspectPluginRegistry.mockResolvedValue({
|
||||
state: "stale",
|
||||
|
||||
@@ -71,6 +71,21 @@ function isConfigSelectedShadowDiagnostic(entry: { level?: string; message?: str
|
||||
);
|
||||
}
|
||||
|
||||
function isErroredConfigSelectedShadowDiagnostic(params: {
|
||||
entry: { level?: string; message?: string; pluginId?: string };
|
||||
plugins: readonly { id: string; origin: string; status: string }[];
|
||||
}): boolean {
|
||||
if (!params.entry.pluginId || !isConfigSelectedShadowDiagnostic(params.entry)) {
|
||||
return false;
|
||||
}
|
||||
return params.plugins.some(
|
||||
(plugin) =>
|
||||
plugin.id === params.entry.pluginId &&
|
||||
plugin.origin === "config" &&
|
||||
plugin.status === "error",
|
||||
);
|
||||
}
|
||||
|
||||
export function registerPluginsCli(program: Command) {
|
||||
const plugins = program
|
||||
.command("plugins")
|
||||
@@ -345,7 +360,9 @@ export function registerPluginsCli(program: Command) {
|
||||
const report = buildPluginDiagnosticsReport({ effectiveOnly: true });
|
||||
const errors = report.plugins.filter((p) => p.status === "error");
|
||||
const diags = report.diagnostics.filter((d) => d.level === "error");
|
||||
const shadowed = report.diagnostics.filter(isConfigSelectedShadowDiagnostic);
|
||||
const shadowed = report.diagnostics.filter((entry) =>
|
||||
isErroredConfigSelectedShadowDiagnostic({ entry, plugins: report.plugins }),
|
||||
);
|
||||
const compatibility = buildPluginCompatibilityNotices({ report });
|
||||
|
||||
if (
|
||||
|
||||
@@ -183,6 +183,49 @@ describe("persistPluginInstall", () => {
|
||||
expect(runtimeLogs.join("\n")).toContain("openclaw plugins doctor");
|
||||
});
|
||||
|
||||
it("does not warn when the config-selected source is inside the npm install path", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
const enabledConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
discord: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
enablePluginInConfig.mockReturnValue({ config: enabledConfig });
|
||||
buildPluginSnapshotReport.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "discord",
|
||||
origin: "config",
|
||||
source: "/tmp/openclaw/npm/node_modules/@openclaw/discord/dist/index.js",
|
||||
status: "loaded",
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
await persistPluginInstall({
|
||||
snapshot: {
|
||||
config: baseConfig,
|
||||
baseHash: "config-1",
|
||||
},
|
||||
pluginId: "discord",
|
||||
install: {
|
||||
source: "npm",
|
||||
spec: "@openclaw/discord",
|
||||
installPath: "/tmp/openclaw/npm/node_modules/@openclaw/discord",
|
||||
},
|
||||
});
|
||||
|
||||
expect(runtimeLogs.join("\n")).not.toContain("is not the active source");
|
||||
});
|
||||
|
||||
it("invalidates runtime cache even when registry refresh fails", async () => {
|
||||
const { persistPluginInstall } = await import("./plugins-install-persist.js");
|
||||
const baseConfig = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { replaceConfigFile } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { type HookInstallUpdate, recordHookInstall } from "../hooks/installs.js";
|
||||
import { isPathInside } from "../infra/path-guards.js";
|
||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||
import {
|
||||
loadInstalledPluginIndexInstallRecords,
|
||||
@@ -12,7 +13,7 @@ import { tracePluginLifecyclePhaseAsync } from "../plugins/plugin-lifecycle-trac
|
||||
import { buildPluginSnapshotReport } from "../plugins/status.js";
|
||||
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import { shortenHomePath } from "../utils.js";
|
||||
import { resolveUserPath, shortenHomePath } from "../utils.js";
|
||||
import {
|
||||
applySlotSelectionForPlugin,
|
||||
enableInternalHookEntries,
|
||||
@@ -60,6 +61,16 @@ export type ConfigSnapshotForInstallPersist = {
|
||||
baseHash: string | undefined;
|
||||
};
|
||||
|
||||
function sourceMatchesInstalledPath(params: {
|
||||
activeSource: string;
|
||||
installedSource: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const activeSource = resolveUserPath(params.activeSource, params.env);
|
||||
const installedSource = resolveUserPath(params.installedSource, params.env);
|
||||
return activeSource === installedSource || isPathInside(installedSource, activeSource);
|
||||
}
|
||||
|
||||
function logShadowedNpmInstallWarning(params: {
|
||||
config: OpenClawConfig;
|
||||
pluginId: string;
|
||||
@@ -79,7 +90,11 @@ function logShadowedNpmInstallWarning(params: {
|
||||
onlyPluginIds: [params.pluginId],
|
||||
});
|
||||
const active = report.plugins.find((plugin) => plugin.id === params.pluginId);
|
||||
if (!active || active.origin !== "config" || active.source === installedSource) {
|
||||
if (
|
||||
!active ||
|
||||
active.origin !== "config" ||
|
||||
sourceMatchesInstalledPath({ activeSource: active.source, installedSource })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user