feat(plugins): surface imported runtime state in status tooling (#59659)

* feat(plugins): surface imported runtime state

* fix(plugins): keep status imports snapshot-only

* fix(plugins): keep status snapshots manifest-only

* fix(plugins): restore doctor load checks

* refactor(plugins): split snapshot and diagnostics reports

* fix(plugins): track imported erroring modules

* fix(plugins): keep hot metadata where required

* fix(plugins): keep hot doctor and write targeting

* fix(plugins): track throwing module imports
This commit is contained in:
Vincent Koc
2026-04-02 22:50:17 +09:00
committed by GitHub
parent 1ecd92af89
commit def5b954a8
18 changed files with 684 additions and 51 deletions

View File

@@ -10,7 +10,7 @@ import {
import { resolveHookEntries } from "../hooks/policy.js";
import type { HookEntry } from "../hooks/types.js";
import { loadWorkspaceHookEntries } from "../hooks/workspace.js";
import { buildPluginStatusReport } from "../plugins/status.js";
import { buildPluginDiagnosticsReport } from "../plugins/status.js";
import { defaultRuntime } from "../runtime.js";
import { formatDocsLink } from "../terminal/links.js";
import { getTerminalTableWidth, renderTable } from "../terminal/table.js";
@@ -46,7 +46,7 @@ function mergeHookEntries(pluginEntries: HookEntry[], workspaceEntries: HookEntr
function buildHooksReport(config: OpenClawConfig): HookStatusReport {
const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config));
const workspaceEntries = loadWorkspaceHookEntries(workspaceDir, { config });
const pluginReport = buildPluginStatusReport({ config, workspaceDir });
const pluginReport = buildPluginDiagnosticsReport({ config, workspaceDir });
const pluginEntries = pluginReport.hooks.map((hook) => hook.entry);
const entries = mergeHookEntries(pluginEntries, workspaceEntries);
return buildWorkspaceHookStatus(workspaceDir, { config, entries });

View File

@@ -18,7 +18,10 @@ export const resolveMarketplaceInstallShortcut = vi.fn();
export const enablePluginInConfig = vi.fn();
export const recordPluginInstall = vi.fn();
export const clearPluginManifestRegistryCache = vi.fn();
export const buildPluginStatusReport = vi.fn();
export const buildPluginSnapshotReport = vi.fn();
export const buildPluginDiagnosticsReport = vi.fn();
export const buildPluginStatusReport = buildPluginDiagnosticsReport;
export const buildPluginCompatibilityNotices = vi.fn();
export const applyExclusiveSlotSelection = vi.fn();
export const uninstallPlugin = vi.fn();
export const updateNpmInstalledPlugins = vi.fn();
@@ -72,7 +75,9 @@ vi.mock("../plugins/manifest-registry.js", () => ({
}));
vi.mock("../plugins/status.js", () => ({
buildPluginStatusReport: (...args: unknown[]) => buildPluginStatusReport(...args),
buildPluginSnapshotReport: (...args: unknown[]) => buildPluginSnapshotReport(...args),
buildPluginDiagnosticsReport: (...args: unknown[]) => buildPluginDiagnosticsReport(...args),
buildPluginCompatibilityNotices: (...args: unknown[]) => buildPluginCompatibilityNotices(...args),
}));
vi.mock("../plugins/slots.js", () => ({
@@ -154,7 +159,9 @@ export function resetPluginsCliTestState() {
enablePluginInConfig.mockReset();
recordPluginInstall.mockReset();
clearPluginManifestRegistryCache.mockReset();
buildPluginStatusReport.mockReset();
buildPluginSnapshotReport.mockReset();
buildPluginDiagnosticsReport.mockReset();
buildPluginCompatibilityNotices.mockReset();
applyExclusiveSlotSelection.mockReset();
uninstallPlugin.mockReset();
updateNpmInstalledPlugins.mockReset();
@@ -199,10 +206,13 @@ export function resetPluginsCliTestState() {
});
enablePluginInConfig.mockImplementation((cfg: OpenClawConfig) => ({ config: cfg }));
recordPluginInstall.mockImplementation((cfg: OpenClawConfig) => cfg);
buildPluginStatusReport.mockReturnValue({
const defaultPluginReport = {
plugins: [],
diagnostics: [],
});
};
buildPluginSnapshotReport.mockReturnValue(defaultPluginReport);
buildPluginDiagnosticsReport.mockReturnValue(defaultPluginReport);
buildPluginCompatibilityNotices.mockReturnValue([]);
applyExclusiveSlotSelection.mockImplementation(({ config }: { config: OpenClawConfig }) => ({
config,
warnings: [],

View File

@@ -0,0 +1,83 @@
import { beforeEach, describe, expect, it } from "vitest";
import { createPluginRecord } from "../plugins/status.test-helpers.js";
import {
buildPluginDiagnosticsReport,
buildPluginSnapshotReport,
resetPluginsCliTestState,
runPluginsCommand,
runtimeLogs,
} from "./plugins-cli-test-helpers.js";
describe("plugins cli list", () => {
beforeEach(() => {
resetPluginsCliTestState();
});
it("includes imported state in JSON output", async () => {
buildPluginSnapshotReport.mockReturnValue({
workspaceDir: "/workspace",
plugins: [
createPluginRecord({
id: "demo",
imported: true,
activated: true,
explicitlyEnabled: true,
}),
],
diagnostics: [],
});
await runPluginsCommand(["plugins", "list", "--json"]);
expect(buildPluginSnapshotReport).toHaveBeenCalledWith();
expect(JSON.parse(runtimeLogs[0] ?? "null")).toEqual({
workspaceDir: "/workspace",
plugins: [
expect.objectContaining({
id: "demo",
imported: true,
activated: true,
explicitlyEnabled: true,
}),
],
diagnostics: [],
});
});
it("shows imported state in verbose output", async () => {
buildPluginSnapshotReport.mockReturnValue({
plugins: [
createPluginRecord({
id: "demo",
name: "Demo Plugin",
imported: false,
activated: true,
explicitlyEnabled: false,
}),
],
diagnostics: [],
});
await runPluginsCommand(["plugins", "list", "--verbose"]);
expect(buildPluginSnapshotReport).toHaveBeenCalledWith();
const output = runtimeLogs.join("\n");
expect(output).toContain("activated: yes");
expect(output).toContain("imported: no");
expect(output).toContain("explicitly enabled: no");
});
it("keeps doctor on a module-loading snapshot", async () => {
buildPluginDiagnosticsReport.mockReturnValue({
plugins: [],
diagnostics: [],
});
await runPluginsCommand(["plugins", "doctor"]);
expect(buildPluginDiagnosticsReport).toHaveBeenCalledWith();
expect(runtimeLogs).toContain("No plugin issues detected.");
});
});

View File

@@ -12,9 +12,10 @@ import type { PluginRecord } from "../plugins/registry.js";
import { formatPluginSourceForTable, resolvePluginSourceRoots } from "../plugins/source-display.js";
import {
buildAllPluginInspectReports,
buildPluginDiagnosticsReport,
buildPluginCompatibilityNotices,
buildPluginInspectReport,
buildPluginStatusReport,
buildPluginSnapshotReport,
formatPluginCompatibilityNotice,
} from "../plugins/status.js";
import {
@@ -140,6 +141,9 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string {
if (plugin.activated !== undefined) {
parts.push(` activated: ${plugin.activated ? "yes" : "no"}`);
}
if (plugin.imported !== undefined) {
parts.push(` imported: ${plugin.imported ? "yes" : "no"}`);
}
if (plugin.explicitlyEnabled !== undefined) {
parts.push(` explicitly enabled: ${plugin.explicitlyEnabled ? "yes" : "no"}`);
}
@@ -236,7 +240,7 @@ export function registerPluginsCli(program: Command) {
.option("--enabled", "Only show enabled plugins", false)
.option("--verbose", "Show detailed entries", false)
.action((opts: PluginsListOptions) => {
const report = buildPluginStatusReport();
const report = buildPluginSnapshotReport();
const list = opts.enabled
? report.plugins.filter((p) => p.status === "loaded")
: report.plugins;
@@ -338,7 +342,7 @@ export function registerPluginsCli(program: Command) {
.option("--json", "Print JSON")
.action((id: string | undefined, opts: PluginInspectOptions) => {
const cfg = loadConfig();
const report = buildPluginStatusReport({ config: cfg });
const report = buildPluginDiagnosticsReport({ config: cfg });
if (opts.all) {
if (id) {
defaultRuntime.error("Pass either a plugin id or --all, not both.");
@@ -603,7 +607,7 @@ export function registerPluginsCli(program: Command) {
.action(async (id: string, opts: PluginUninstallOptions) => {
const snapshot = await readConfigFileSnapshot();
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
const report = buildPluginStatusReport({ config: cfg });
const report = buildPluginDiagnosticsReport({ config: cfg });
const extensionsDir = path.join(resolveStateDir(process.env, os.homedir), "extensions");
const keepFiles = Boolean(opts.keepFiles || opts.keepConfig);
@@ -790,7 +794,7 @@ export function registerPluginsCli(program: Command) {
.command("doctor")
.description("Report plugin load issues")
.action(() => {
const report = buildPluginStatusReport();
const report = buildPluginDiagnosticsReport();
const errors = report.plugins.filter((p) => p.status === "error");
const diags = report.diagnostics.filter((d) => d.level === "error");
const compatibility = buildPluginCompatibilityNotices({ report });

View File

@@ -4,7 +4,7 @@ import type { PluginInstallRecord } from "../config/types.plugins.js";
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js";
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
import { buildPluginStatusReport } from "../plugins/status.js";
import { buildPluginDiagnosticsReport } from "../plugins/status.js";
import { defaultRuntime } from "../runtime.js";
import { theme } from "../terminal/theme.js";
@@ -40,7 +40,7 @@ export function applySlotSelectionForPlugin(
config: OpenClawConfig,
pluginId: string,
): { config: OpenClawConfig; warnings: string[] } {
const report = buildPluginStatusReport({ config });
const report = buildPluginDiagnosticsReport({ config });
const plugin = report.plugins.find((entry) => entry.id === pluginId);
if (!plugin) {
return { config, warnings: [] };