import fs from
"node:fs" ;
import path from
"node:path" ;
import { afterAll, afterEach, describe, expect, it } from
"vitest" ;
import { withEnv } from
"../test-utils/env.js" ;
import { loadOpenClawPlugins } from
"./loader.js" ;
import {
cleanupPluginLoaderFixturesForTest,
loadBundleFixture,
makeTempDir,
mkdirSafe,
resetPluginLoaderTestStateForTest,
useNoBundledPlugins,
} from
"./loader.test-fixtures.js" ;
function expectNoUnwiredBundleDiagnostic(
registry: ReturnType<
typeof loadOpenClawPlugins>,
pluginId: string,
) {
expect(
registry.diagnostics.some(
(diag) =>
diag.pluginId === pluginId &&
diag.message.includes(
"bundle capability detected but not wired" ),
),
).toBe(
false );
}
afterEach(() => {
resetPluginLoaderTestStateForTest();
});
afterAll(() => {
cleanupPluginLoaderFixturesForTest();
});
describe(
"bundle plugins" , () => {
it(
"reports Codex bundles as loaded bundle plugins without importing runtime code" , () => {
useNoBundledPlugins();
const workspaceDir = makeTempDir();
const stateDir = makeTempDir();
const bundleRoot = path.join(workspaceDir,
".openclaw" ,
"extensions" ,
"sample-bundle" )
;
mkdirSafe(path.join(bundleRoot, ".codex-plugin" ));
mkdirSafe(path.join(bundleRoot, "skills" ));
fs.writeFileSync(
path.join(bundleRoot, ".codex-plugin" , "plugin.json" ),
JSON.stringify({
name: "Sample Bundle" ,
description: "Codex bundle fixture" ,
skills: "skills" ,
}),
"utf-8" ,
);
fs.writeFileSync(
path.join(bundleRoot, "skills" , "SKILL.md" ),
"---\ndescription: fixture\n---\n" ,
);
const registry = withEnv({ OPENCLAW_STATE_DIR: stateDir }, () =>
loadOpenClawPlugins({
workspaceDir,
onlyPluginIds: ["sample-bundle" ],
config: {
plugins: {
entries: {
"sample-bundle" : {
enabled: true ,
},
},
},
},
cache: false ,
}),
);
const plugin = registry.plugins.find((entry) => entry.id === "sample-bundle" );
expect(plugin?.status).toBe("loaded" );
expect(plugin?.format).toBe("bundle" );
expect(plugin?.bundleFormat).toBe("codex" );
expect(plugin?.bundleCapabilities).toContain("skills" );
});
it.each([
{
name: "treats Claude command roots and settings as supported bundle surfaces" ,
pluginId: "claude-skills" ,
expectedFormat: "claude" ,
expectedCapabilities: ["skills" , "commands" , "settings" ],
build: (bundleRoot: string) => {
mkdirSafe(path.join(bundleRoot, "commands" ));
fs.writeFileSync(
path.join(bundleRoot, "commands" , "review.md" ),
"---\ndescription: fixture\n---\n" ,
);
fs.writeFileSync(
path.join(bundleRoot, "settings.json" ),
'{"hideThinkingBlock":true}' ,
"utf-8" ,
);
},
},
{
name: "treats bundle MCP as a supported bundle surface" ,
pluginId: "claude-mcp" ,
expectedFormat: "claude" ,
expectedCapabilities: ["mcpServers" ],
build: (bundleRoot: string) => {
mkdirSafe(path.join(bundleRoot, ".claude-plugin" ));
fs.writeFileSync(
path.join(bundleRoot, ".claude-plugin" , "plugin.json" ),
JSON.stringify({
name: "Claude MCP" ,
}),
"utf-8" ,
);
fs.writeFileSync(
path.join(bundleRoot, ".mcp.json" ),
JSON.stringify({
mcpServers: {
probe: {
command: "node" ,
args: ["./probe.mjs" ],
},
},
}),
"utf-8" ,
);
},
},
{
name: "treats Cursor command roots as supported bundle skill surfaces" ,
pluginId: "cursor-skills" ,
expectedFormat: "cursor" ,
expectedCapabilities: ["skills" , "commands" ],
build: (bundleRoot: string) => {
mkdirSafe(path.join(bundleRoot, ".cursor-plugin" ));
mkdirSafe(path.join(bundleRoot, ".cursor" , "commands" ));
fs.writeFileSync(
path.join(bundleRoot, ".cursor-plugin" , "plugin.json" ),
JSON.stringify({
name: "Cursor Skills" ,
}),
"utf-8" ,
);
fs.writeFileSync(
path.join(bundleRoot, ".cursor" , "commands" , "review.md" ),
"---\ndescription: fixture\n---\n" ,
);
},
},
])("$name" , ({ pluginId, expectedFormat, expectedCapabilities, build }) => {
const registry = loadBundleFixture({ pluginId, build });
const plugin = registry.plugins.find((entry) => entry.id === pluginId);
expect(plugin?.status).toBe("loaded" );
expect(plugin?.bundleFormat).toBe(expectedFormat);
expect(plugin?.bundleCapabilities).toEqual(expect.arrayContaining(expectedCapabilities));
expectNoUnwiredBundleDiagnostic(registry, pluginId);
});
it("warns when bundle MCP only declares unsupported non-stdio transports" , () => {
const stateDir = makeTempDir();
const registry = loadBundleFixture({
pluginId: "claude-mcp-url" ,
env: {
OPENCLAW_HOME: stateDir,
},
build: (bundleRoot) => {
mkdirSafe(path.join(bundleRoot, ".claude-plugin" ));
fs.writeFileSync(
path.join(bundleRoot, ".claude-plugin" , "plugin.json" ),
JSON.stringify({
name: "Claude MCP URL" ,
}),
"utf-8" ,
);
fs.writeFileSync(
path.join(bundleRoot, ".mcp.json" ),
JSON.stringify({
mcpServers: {
remoteProbe: {
url: "http://127.0.0.1:8787/mcp ",
},
},
}),
"utf-8" ,
);
},
});
const plugin = registry.plugins.find((entry) => entry.id === "claude-mcp-url" );
expect(plugin?.status).toBe("loaded" );
expect(plugin?.bundleCapabilities).toEqual(expect.arrayContaining(["mcpServers" ]));
expect(
registry.diagnostics.some(
(diag) =>
diag.pluginId === "claude-mcp-url" &&
diag.message.includes("stdio only today" ) &&
diag.message.includes("remoteProbe" ),
),
).toBe(true );
});
});
Messung V0.5 in Prozent C=100 H=100 G=100
¤ Dauer der Verarbeitung: 0.10 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland