mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 14:21:32 +00:00
* refactor(plugins): separate activation from enablement * fix(cli): sanitize verbose plugin activation reasons
790 lines
24 KiB
TypeScript
790 lines
24 KiB
TypeScript
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
createCompatibilityNotice,
|
|
createCustomHook,
|
|
createPluginLoadResult,
|
|
createPluginRecord,
|
|
createTypedHook,
|
|
HOOK_ONLY_MESSAGE,
|
|
LEGACY_BEFORE_AGENT_START_MESSAGE,
|
|
} from "./status.test-helpers.js";
|
|
|
|
const loadConfigMock = vi.fn();
|
|
const loadOpenClawPluginsMock = vi.fn();
|
|
const applyPluginAutoEnableMock = vi.fn();
|
|
const resolveBundledProviderCompatPluginIdsMock = vi.fn();
|
|
const withBundledPluginAllowlistCompatMock = vi.fn();
|
|
const withBundledPluginEnablementCompatMock = vi.fn();
|
|
const listImportedBundledPluginFacadeIdsMock = vi.fn();
|
|
const listImportedRuntimePluginIdsMock = vi.fn();
|
|
let buildPluginSnapshotReport: typeof import("./status.js").buildPluginSnapshotReport;
|
|
let buildPluginDiagnosticsReport: typeof import("./status.js").buildPluginDiagnosticsReport;
|
|
let buildPluginInspectReport: typeof import("./status.js").buildPluginInspectReport;
|
|
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(),
|
|
}));
|
|
|
|
vi.mock("../config/plugin-auto-enable.js", () => ({
|
|
applyPluginAutoEnable: (...args: unknown[]) => applyPluginAutoEnableMock(...args),
|
|
}));
|
|
|
|
vi.mock("./loader.js", () => ({
|
|
loadOpenClawPlugins: (...args: unknown[]) => loadOpenClawPluginsMock(...args),
|
|
}));
|
|
|
|
vi.mock("./providers.js", () => ({
|
|
resolveBundledProviderCompatPluginIds: (...args: unknown[]) =>
|
|
resolveBundledProviderCompatPluginIdsMock(...args),
|
|
}));
|
|
|
|
vi.mock("./bundled-compat.js", () => ({
|
|
withBundledPluginAllowlistCompat: (...args: unknown[]) =>
|
|
withBundledPluginAllowlistCompatMock(...args),
|
|
withBundledPluginEnablementCompat: (...args: unknown[]) =>
|
|
withBundledPluginEnablementCompatMock(...args),
|
|
}));
|
|
|
|
vi.mock("../plugin-sdk/facade-runtime.js", () => ({
|
|
listImportedBundledPluginFacadeIds: (...args: unknown[]) =>
|
|
listImportedBundledPluginFacadeIdsMock(...args),
|
|
}));
|
|
|
|
vi.mock("./runtime.js", () => ({
|
|
listImportedRuntimePluginIds: (...args: unknown[]) => listImportedRuntimePluginIdsMock(...args),
|
|
}));
|
|
|
|
vi.mock("../agents/agent-scope.js", () => ({
|
|
resolveAgentWorkspaceDir: () => undefined,
|
|
resolveDefaultAgentId: () => "default",
|
|
}));
|
|
|
|
vi.mock("../agents/workspace.js", () => ({
|
|
resolveDefaultAgentWorkspaceDir: () => "/default-workspace",
|
|
}));
|
|
|
|
function setPluginLoadResult(overrides: Partial<ReturnType<typeof createPluginLoadResult>>) {
|
|
loadOpenClawPluginsMock.mockReturnValue(
|
|
createPluginLoadResult({
|
|
plugins: [],
|
|
...overrides,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function setSinglePluginLoadResult(
|
|
plugin: ReturnType<typeof createPluginRecord>,
|
|
overrides: Omit<Partial<ReturnType<typeof createPluginLoadResult>>, "plugins"> = {},
|
|
) {
|
|
setPluginLoadResult({
|
|
plugins: [plugin],
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
function expectInspectReport(
|
|
pluginId: string,
|
|
): NonNullable<ReturnType<typeof buildPluginInspectReport>> {
|
|
const inspect = buildPluginInspectReport({ id: pluginId });
|
|
expect(inspect).not.toBeNull();
|
|
if (!inspect) {
|
|
throw new Error(`expected inspect report for ${pluginId}`);
|
|
}
|
|
return inspect;
|
|
}
|
|
|
|
function expectPluginLoaderCall(params: {
|
|
config?: unknown;
|
|
activationSourceConfig?: unknown;
|
|
autoEnabledReasons?: Record<string, string[]>;
|
|
workspaceDir?: string;
|
|
env?: NodeJS.ProcessEnv;
|
|
loadModules?: boolean;
|
|
}) {
|
|
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
...(params.config !== undefined ? { config: params.config } : {}),
|
|
...(params.activationSourceConfig !== undefined
|
|
? { activationSourceConfig: params.activationSourceConfig }
|
|
: {}),
|
|
...(params.autoEnabledReasons !== undefined
|
|
? { autoEnabledReasons: params.autoEnabledReasons }
|
|
: {}),
|
|
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
|
|
...(params.env ? { env: params.env } : {}),
|
|
...(params.loadModules !== undefined ? { loadModules: params.loadModules } : {}),
|
|
}),
|
|
);
|
|
}
|
|
|
|
function expectAutoEnabledStatusLoad(params: {
|
|
rawConfig: unknown;
|
|
autoEnabledConfig: unknown;
|
|
autoEnabledReasons?: Record<string, string[]>;
|
|
}) {
|
|
expect(applyPluginAutoEnableMock).toHaveBeenCalledWith({
|
|
config: params.rawConfig,
|
|
env: process.env,
|
|
});
|
|
expectPluginLoaderCall({
|
|
config: params.autoEnabledConfig,
|
|
activationSourceConfig: params.rawConfig,
|
|
autoEnabledReasons: params.autoEnabledReasons ?? {},
|
|
});
|
|
}
|
|
|
|
function createCompatChainFixture() {
|
|
const config = { plugins: { allow: ["telegram"] } };
|
|
const pluginIds = ["anthropic", "openai"];
|
|
const compatConfig = { plugins: { allow: ["telegram", ...pluginIds] } };
|
|
const enabledConfig = {
|
|
plugins: {
|
|
allow: ["telegram", ...pluginIds],
|
|
entries: {
|
|
anthropic: { enabled: true },
|
|
openai: { enabled: true },
|
|
},
|
|
},
|
|
};
|
|
return { config, pluginIds, compatConfig, enabledConfig };
|
|
}
|
|
|
|
function expectBundledCompatChainApplied(params: {
|
|
config: unknown;
|
|
pluginIds: string[];
|
|
compatConfig: unknown;
|
|
enabledConfig: unknown;
|
|
}) {
|
|
expect(withBundledPluginAllowlistCompatMock).toHaveBeenCalledWith({
|
|
config: params.config,
|
|
pluginIds: params.pluginIds,
|
|
});
|
|
expect(withBundledPluginEnablementCompatMock).toHaveBeenCalledWith({
|
|
config: params.compatConfig,
|
|
pluginIds: params.pluginIds,
|
|
});
|
|
expectPluginLoaderCall({ config: params.enabledConfig });
|
|
}
|
|
|
|
function createAutoEnabledStatusConfig(
|
|
entries: Record<string, unknown>,
|
|
rawConfigOverrides?: Record<string, unknown>,
|
|
) {
|
|
const rawConfig = {
|
|
plugins: {},
|
|
...rawConfigOverrides,
|
|
};
|
|
const autoEnabledConfig = {
|
|
...rawConfig,
|
|
plugins: {
|
|
entries,
|
|
},
|
|
};
|
|
return { rawConfig, autoEnabledConfig };
|
|
}
|
|
|
|
function expectNoCompatibilityWarnings() {
|
|
expect(buildPluginCompatibilityNotices()).toEqual([]);
|
|
expect(buildPluginCompatibilityWarnings()).toEqual([]);
|
|
}
|
|
|
|
function expectCompatibilityOutput(params: { notices?: unknown[]; warnings?: string[] }) {
|
|
if (params.notices) {
|
|
expect(buildPluginCompatibilityNotices()).toEqual(params.notices);
|
|
}
|
|
if (params.warnings) {
|
|
expect(buildPluginCompatibilityWarnings()).toEqual(params.warnings);
|
|
}
|
|
}
|
|
|
|
function expectCapabilityKinds(
|
|
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
|
|
kinds: readonly string[],
|
|
) {
|
|
expect(inspect.capabilities.map((entry) => entry.kind)).toEqual(kinds);
|
|
}
|
|
|
|
function expectInspectShape(
|
|
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
|
|
params: {
|
|
shape: string;
|
|
capabilityMode: string;
|
|
capabilityKinds: readonly string[];
|
|
},
|
|
) {
|
|
expect(inspect.shape).toBe(params.shape);
|
|
expect(inspect.capabilityMode).toBe(params.capabilityMode);
|
|
expectCapabilityKinds(inspect, params.capabilityKinds);
|
|
}
|
|
|
|
function expectInspectPolicy(
|
|
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
|
|
expected: Record<string, unknown>,
|
|
) {
|
|
expect(inspect.policy).toEqual(expected);
|
|
}
|
|
|
|
function expectBundleInspectState(
|
|
inspect: NonNullable<ReturnType<typeof buildPluginInspectReport>>,
|
|
params: {
|
|
bundleCapabilities: readonly string[];
|
|
shape: string;
|
|
},
|
|
) {
|
|
expect(inspect.bundleCapabilities).toEqual(params.bundleCapabilities);
|
|
expect(inspect.mcpServers).toEqual([]);
|
|
expect(inspect.shape).toBe(params.shape);
|
|
}
|
|
|
|
describe("plugin status reports", () => {
|
|
beforeAll(async () => {
|
|
({
|
|
buildAllPluginInspectReports,
|
|
buildPluginCompatibilityNotices,
|
|
buildPluginDiagnosticsReport,
|
|
buildPluginCompatibilityWarnings,
|
|
buildPluginInspectReport,
|
|
buildPluginSnapshotReport,
|
|
formatPluginCompatibilityNotice,
|
|
summarizePluginCompatibility,
|
|
} = await import("./status.js"));
|
|
});
|
|
|
|
beforeEach(() => {
|
|
loadConfigMock.mockReset();
|
|
loadOpenClawPluginsMock.mockReset();
|
|
applyPluginAutoEnableMock.mockReset();
|
|
resolveBundledProviderCompatPluginIdsMock.mockReset();
|
|
withBundledPluginAllowlistCompatMock.mockReset();
|
|
withBundledPluginEnablementCompatMock.mockReset();
|
|
listImportedBundledPluginFacadeIdsMock.mockReset();
|
|
listImportedRuntimePluginIdsMock.mockReset();
|
|
loadConfigMock.mockReturnValue({});
|
|
applyPluginAutoEnableMock.mockImplementation((params: { config: unknown }) => ({
|
|
config: params.config,
|
|
changes: [],
|
|
autoEnabledReasons: {},
|
|
}));
|
|
resolveBundledProviderCompatPluginIdsMock.mockReturnValue([]);
|
|
withBundledPluginAllowlistCompatMock.mockImplementation(
|
|
(params: { config: unknown }) => params.config,
|
|
);
|
|
withBundledPluginEnablementCompatMock.mockImplementation(
|
|
(params: { config: unknown }) => params.config,
|
|
);
|
|
listImportedBundledPluginFacadeIdsMock.mockReturnValue([]);
|
|
listImportedRuntimePluginIdsMock.mockReturnValue([]);
|
|
setPluginLoadResult({ plugins: [] });
|
|
});
|
|
|
|
it("forwards an explicit env to plugin loading", () => {
|
|
const env = { HOME: "/tmp/openclaw-home" } as NodeJS.ProcessEnv;
|
|
|
|
buildPluginSnapshotReport({
|
|
config: {},
|
|
workspaceDir: "/workspace",
|
|
env,
|
|
});
|
|
|
|
expectPluginLoaderCall({
|
|
config: {},
|
|
workspaceDir: "/workspace",
|
|
env,
|
|
loadModules: false,
|
|
});
|
|
});
|
|
|
|
it("uses a non-activating snapshot load for snapshot reports", () => {
|
|
buildPluginSnapshotReport({ config: {}, workspaceDir: "/workspace" });
|
|
|
|
expect(loadOpenClawPluginsMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
activate: false,
|
|
cache: false,
|
|
loadModules: false,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("loads plugin status from the auto-enabled config snapshot", () => {
|
|
const { rawConfig, autoEnabledConfig } = createAutoEnabledStatusConfig(
|
|
{
|
|
demo: { enabled: true },
|
|
},
|
|
{ channels: { demo: { enabled: true } } },
|
|
);
|
|
applyPluginAutoEnableMock.mockReturnValue({
|
|
config: autoEnabledConfig,
|
|
changes: [],
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
|
|
buildPluginSnapshotReport({ config: rawConfig });
|
|
|
|
expectAutoEnabledStatusLoad({
|
|
rawConfig,
|
|
autoEnabledConfig,
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
expectPluginLoaderCall({ loadModules: false });
|
|
});
|
|
|
|
it("uses the auto-enabled config snapshot for inspect policy summaries", () => {
|
|
const { rawConfig, autoEnabledConfig } = createAutoEnabledStatusConfig(
|
|
{
|
|
demo: {
|
|
enabled: true,
|
|
subagent: {
|
|
allowModelOverride: true,
|
|
allowedModels: ["openai/gpt-5.4"],
|
|
hasAllowedModelsConfig: true,
|
|
},
|
|
},
|
|
},
|
|
{ channels: { demo: { enabled: true } } },
|
|
);
|
|
applyPluginAutoEnableMock.mockReturnValue({
|
|
config: autoEnabledConfig,
|
|
changes: [],
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
setSinglePluginLoadResult(
|
|
createPluginRecord({
|
|
id: "demo",
|
|
name: "Demo",
|
|
description: "Auto-enabled plugin",
|
|
origin: "bundled",
|
|
providerIds: ["demo"],
|
|
}),
|
|
);
|
|
|
|
const inspect = buildPluginInspectReport({ id: "demo", config: rawConfig });
|
|
|
|
expect(inspect).not.toBeNull();
|
|
expectInspectPolicy(inspect!, {
|
|
allowPromptInjection: undefined,
|
|
allowModelOverride: true,
|
|
allowedModels: ["openai/gpt-5.4"],
|
|
hasAllowedModelsConfig: true,
|
|
});
|
|
expectPluginLoaderCall({ loadModules: true });
|
|
});
|
|
|
|
it("preserves raw config activation context when compatibility notices build their own report", () => {
|
|
const { rawConfig, autoEnabledConfig } = createAutoEnabledStatusConfig(
|
|
{
|
|
demo: { enabled: true },
|
|
},
|
|
{ channels: { demo: { enabled: true } } },
|
|
);
|
|
applyPluginAutoEnableMock.mockReturnValue({
|
|
config: autoEnabledConfig,
|
|
changes: [],
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
setSinglePluginLoadResult(
|
|
createPluginRecord({
|
|
id: "demo",
|
|
name: "Demo",
|
|
description: "Auto-enabled plugin",
|
|
origin: "bundled",
|
|
hookCount: 1,
|
|
}),
|
|
{
|
|
typedHooks: [createTypedHook({ pluginId: "demo", hookName: "before_agent_start" })],
|
|
},
|
|
);
|
|
|
|
expect(buildPluginCompatibilityNotices({ config: rawConfig })).toEqual([
|
|
createCompatibilityNotice({ pluginId: "demo", code: "legacy-before-agent-start" }),
|
|
createCompatibilityNotice({ pluginId: "demo", code: "hook-only" }),
|
|
]);
|
|
|
|
expectAutoEnabledStatusLoad({
|
|
rawConfig,
|
|
autoEnabledConfig,
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
expectPluginLoaderCall({ loadModules: true });
|
|
});
|
|
|
|
it("applies the full bundled provider compat chain before loading plugins", () => {
|
|
const { config, pluginIds, compatConfig, enabledConfig } = createCompatChainFixture();
|
|
loadConfigMock.mockReturnValue(config);
|
|
resolveBundledProviderCompatPluginIdsMock.mockReturnValue(pluginIds);
|
|
withBundledPluginAllowlistCompatMock.mockReturnValue(compatConfig);
|
|
withBundledPluginEnablementCompatMock.mockReturnValue(enabledConfig);
|
|
|
|
buildPluginSnapshotReport({ config });
|
|
|
|
expectBundledCompatChainApplied({
|
|
config,
|
|
pluginIds,
|
|
compatConfig,
|
|
enabledConfig,
|
|
});
|
|
});
|
|
|
|
it("preserves raw config activation context for compatibility-derived reports", () => {
|
|
const { rawConfig, autoEnabledConfig } = createAutoEnabledStatusConfig(
|
|
{
|
|
demo: { enabled: true },
|
|
},
|
|
{ channels: { demo: { enabled: true } } },
|
|
);
|
|
applyPluginAutoEnableMock.mockReturnValue({
|
|
config: autoEnabledConfig,
|
|
changes: [],
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
setSinglePluginLoadResult(
|
|
createPluginRecord({
|
|
id: "demo",
|
|
name: "Demo",
|
|
description: "Auto-enabled plugin",
|
|
origin: "bundled",
|
|
hookCount: 1,
|
|
}),
|
|
{
|
|
typedHooks: [createTypedHook({ pluginId: "demo", hookName: "before_agent_start" })],
|
|
},
|
|
);
|
|
|
|
expect(buildPluginCompatibilityNotices({ config: rawConfig })).toEqual([
|
|
createCompatibilityNotice({ pluginId: "demo", code: "legacy-before-agent-start" }),
|
|
createCompatibilityNotice({ pluginId: "demo", code: "hook-only" }),
|
|
]);
|
|
|
|
expectAutoEnabledStatusLoad({
|
|
rawConfig,
|
|
autoEnabledConfig,
|
|
autoEnabledReasons: {
|
|
demo: ["demo configured"],
|
|
},
|
|
});
|
|
});
|
|
|
|
it("normalizes bundled plugin versions to the core base release", () => {
|
|
setSinglePluginLoadResult(
|
|
createPluginRecord({
|
|
id: "whatsapp",
|
|
name: "WhatsApp",
|
|
description: "Bundled channel plugin",
|
|
version: "2026.3.22",
|
|
origin: "bundled",
|
|
channelIds: ["whatsapp"],
|
|
}),
|
|
);
|
|
|
|
const report = buildPluginDiagnosticsReport({
|
|
config: {},
|
|
env: {
|
|
OPENCLAW_VERSION: "2026.3.23-1",
|
|
} as NodeJS.ProcessEnv,
|
|
});
|
|
|
|
expect(report.plugins[0]?.version).toBe("2026.3.23");
|
|
});
|
|
|
|
it("marks plugins as imported when runtime or facade state has loaded them", () => {
|
|
setPluginLoadResult({
|
|
plugins: [
|
|
createPluginRecord({ id: "runtime-loaded" }),
|
|
createPluginRecord({ id: "facade-loaded" }),
|
|
createPluginRecord({ id: "bundle-loaded", format: "bundle" }),
|
|
createPluginRecord({ id: "cold-plugin" }),
|
|
],
|
|
});
|
|
listImportedRuntimePluginIdsMock.mockReturnValue(["runtime-loaded", "bundle-loaded"]);
|
|
listImportedBundledPluginFacadeIdsMock.mockReturnValue(["facade-loaded"]);
|
|
|
|
const report = buildPluginSnapshotReport({ config: {} });
|
|
|
|
expect(report.plugins).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: "runtime-loaded", imported: true }),
|
|
expect.objectContaining({ id: "facade-loaded", imported: true }),
|
|
expect.objectContaining({ id: "bundle-loaded", imported: false }),
|
|
expect.objectContaining({ id: "cold-plugin", imported: false }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("marks snapshot-loaded plugin modules as imported during full report loads", () => {
|
|
setPluginLoadResult({
|
|
plugins: [
|
|
createPluginRecord({ id: "runtime-loaded" }),
|
|
createPluginRecord({ id: "bundle-loaded", format: "bundle" }),
|
|
],
|
|
});
|
|
|
|
const report = buildPluginDiagnosticsReport({ config: {} });
|
|
|
|
expect(report.plugins).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: "runtime-loaded", imported: true }),
|
|
expect.objectContaining({ id: "bundle-loaded", imported: false }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("marks errored plugin modules as imported when full diagnostics already evaluated them", () => {
|
|
setPluginLoadResult({
|
|
plugins: [createPluginRecord({ id: "broken-plugin", status: "error" })],
|
|
});
|
|
listImportedRuntimePluginIdsMock.mockReturnValue(["broken-plugin"]);
|
|
|
|
const report = buildPluginDiagnosticsReport({ config: {} });
|
|
|
|
expect(report.plugins).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: "broken-plugin", status: "error", imported: true }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("builds an inspect report with capability shape and policy", () => {
|
|
loadConfigMock.mockReturnValue({
|
|
plugins: {
|
|
entries: {
|
|
google: {
|
|
hooks: { allowPromptInjection: false },
|
|
subagent: {
|
|
allowModelOverride: true,
|
|
allowedModels: ["openai/gpt-5.4"],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
setPluginLoadResult({
|
|
plugins: [
|
|
createPluginRecord({
|
|
id: "google",
|
|
name: "Google",
|
|
description: "Google provider plugin",
|
|
origin: "bundled",
|
|
cliBackendIds: ["google-gemini-cli"],
|
|
providerIds: ["google"],
|
|
mediaUnderstandingProviderIds: ["google"],
|
|
imageGenerationProviderIds: ["google"],
|
|
webSearchProviderIds: ["google"],
|
|
}),
|
|
],
|
|
diagnostics: [{ level: "warn", pluginId: "google", message: "watch this surface" }],
|
|
typedHooks: [createTypedHook({ pluginId: "google", hookName: "before_agent_start" })],
|
|
});
|
|
|
|
const inspect = buildPluginInspectReport({ id: "google" });
|
|
|
|
expect(inspect).not.toBeNull();
|
|
expectInspectShape(inspect!, {
|
|
shape: "hybrid-capability",
|
|
capabilityMode: "hybrid",
|
|
capabilityKinds: [
|
|
"cli-backend",
|
|
"text-inference",
|
|
"media-understanding",
|
|
"image-generation",
|
|
"web-search",
|
|
],
|
|
});
|
|
expect(inspect?.usesLegacyBeforeAgentStart).toBe(true);
|
|
expect(inspect?.compatibility).toEqual([
|
|
createCompatibilityNotice({ pluginId: "google", code: "legacy-before-agent-start" }),
|
|
]);
|
|
expectInspectPolicy(inspect!, {
|
|
allowPromptInjection: false,
|
|
allowModelOverride: true,
|
|
allowedModels: ["openai/gpt-5.4"],
|
|
hasAllowedModelsConfig: true,
|
|
});
|
|
expect(inspect?.diagnostics).toEqual([
|
|
{ level: "warn", pluginId: "google", message: "watch this surface" },
|
|
]);
|
|
});
|
|
|
|
it("builds inspect reports for every loaded plugin", () => {
|
|
setPluginLoadResult({
|
|
plugins: [
|
|
createPluginRecord({
|
|
id: "lca",
|
|
name: "LCA",
|
|
description: "Legacy hook plugin",
|
|
hookCount: 1,
|
|
}),
|
|
createPluginRecord({
|
|
id: "microsoft",
|
|
name: "Microsoft",
|
|
description: "Hybrid capability plugin",
|
|
origin: "bundled",
|
|
providerIds: ["microsoft"],
|
|
webSearchProviderIds: ["microsoft"],
|
|
}),
|
|
],
|
|
hooks: [createCustomHook({ pluginId: "lca", events: ["message"] })],
|
|
typedHooks: [createTypedHook({ pluginId: "lca", hookName: "before_agent_start" })],
|
|
});
|
|
|
|
const inspect = buildAllPluginInspectReports();
|
|
|
|
expect(inspect.map((entry) => entry.plugin.id)).toEqual(["lca", "microsoft"]);
|
|
expect(inspect.map((entry) => entry.shape)).toEqual(["hook-only", "hybrid-capability"]);
|
|
expect(inspect[0]?.usesLegacyBeforeAgentStart).toBe(true);
|
|
expectCapabilityKinds(inspect[1], ["text-inference", "web-search"]);
|
|
});
|
|
|
|
it("treats a CLI-backend-only plugin as a plain capability", () => {
|
|
setSinglePluginLoadResult(
|
|
createPluginRecord({
|
|
id: "anthropic",
|
|
name: "Anthropic",
|
|
cliBackendIds: ["claude-cli"],
|
|
}),
|
|
);
|
|
|
|
const inspect = expectInspectReport("anthropic");
|
|
|
|
expectInspectShape(inspect, {
|
|
shape: "plain-capability",
|
|
capabilityMode: "plain",
|
|
capabilityKinds: ["cli-backend"],
|
|
});
|
|
expect(inspect.capabilities).toEqual([{ kind: "cli-backend", ids: ["claude-cli"] }]);
|
|
});
|
|
|
|
it("builds compatibility warnings for legacy compatibility paths", () => {
|
|
setPluginLoadResult({
|
|
plugins: [
|
|
createPluginRecord({
|
|
id: "lca",
|
|
name: "LCA",
|
|
description: "Legacy hook plugin",
|
|
hookCount: 1,
|
|
}),
|
|
],
|
|
typedHooks: [createTypedHook({ pluginId: "lca", hookName: "before_agent_start" })],
|
|
});
|
|
|
|
expectCompatibilityOutput({
|
|
warnings: [`lca ${LEGACY_BEFORE_AGENT_START_MESSAGE}`, `lca ${HOOK_ONLY_MESSAGE}`],
|
|
});
|
|
});
|
|
|
|
it("builds structured compatibility notices with deterministic ordering", () => {
|
|
setPluginLoadResult({
|
|
plugins: [
|
|
createPluginRecord({
|
|
id: "hook-only",
|
|
name: "Hook Only",
|
|
hookCount: 1,
|
|
}),
|
|
createPluginRecord({
|
|
id: "legacy-only",
|
|
name: "Legacy Only",
|
|
providerIds: ["legacy-only"],
|
|
hookCount: 1,
|
|
}),
|
|
],
|
|
hooks: [createCustomHook({ pluginId: "hook-only", events: ["message"] })],
|
|
typedHooks: [createTypedHook({ pluginId: "legacy-only", hookName: "before_agent_start" })],
|
|
});
|
|
|
|
expectCompatibilityOutput({
|
|
notices: [
|
|
createCompatibilityNotice({ pluginId: "hook-only", code: "hook-only" }),
|
|
createCompatibilityNotice({ pluginId: "legacy-only", code: "legacy-before-agent-start" }),
|
|
],
|
|
});
|
|
});
|
|
|
|
it("returns no compatibility warnings for modern capability plugins", () => {
|
|
setSinglePluginLoadResult(
|
|
createPluginRecord({
|
|
id: "modern",
|
|
name: "Modern",
|
|
providerIds: ["modern"],
|
|
}),
|
|
);
|
|
|
|
expectNoCompatibilityWarnings();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "populates bundleCapabilities from plugin record",
|
|
plugin: createPluginRecord({
|
|
id: "claude-bundle",
|
|
name: "Claude Bundle",
|
|
description: "A bundle plugin with skills and commands",
|
|
source: "/tmp/claude-bundle/.claude-plugin/plugin.json",
|
|
format: "bundle",
|
|
bundleFormat: "claude",
|
|
bundleCapabilities: ["skills", "commands", "agents", "settings"],
|
|
rootDir: "/tmp/claude-bundle",
|
|
}),
|
|
expectedId: "claude-bundle",
|
|
expectedBundleCapabilities: ["skills", "commands", "agents", "settings"],
|
|
expectedShape: "non-capability",
|
|
},
|
|
{
|
|
name: "returns empty bundleCapabilities and mcpServers for non-bundle plugins",
|
|
plugin: createPluginRecord({
|
|
id: "plain-plugin",
|
|
name: "Plain Plugin",
|
|
description: "A regular plugin",
|
|
providerIds: ["plain"],
|
|
}),
|
|
expectedId: "plain-plugin",
|
|
expectedBundleCapabilities: [],
|
|
expectedShape: "plain-capability",
|
|
},
|
|
])("$name", ({ plugin, expectedId, expectedBundleCapabilities, expectedShape }) => {
|
|
setSinglePluginLoadResult(plugin);
|
|
|
|
const inspect = expectInspectReport(expectedId);
|
|
|
|
expectBundleInspectState(inspect, {
|
|
bundleCapabilities: expectedBundleCapabilities,
|
|
shape: expectedShape,
|
|
});
|
|
});
|
|
|
|
it("formats and summarizes compatibility notices", () => {
|
|
const notice = createCompatibilityNotice({
|
|
pluginId: "legacy-plugin",
|
|
code: "legacy-before-agent-start",
|
|
});
|
|
|
|
expect(formatPluginCompatibilityNotice(notice)).toBe(
|
|
`legacy-plugin ${LEGACY_BEFORE_AGENT_START_MESSAGE}`,
|
|
);
|
|
expect(
|
|
summarizePluginCompatibility([
|
|
notice,
|
|
createCompatibilityNotice({ pluginId: "legacy-plugin", code: "hook-only" }),
|
|
]),
|
|
).toEqual({
|
|
noticeCount: 2,
|
|
pluginCount: 1,
|
|
});
|
|
});
|
|
});
|