mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-29 05:25:43 +02:00
test: dedupe plugin bundle and discovery helpers
This commit is contained in:
@@ -32,6 +32,10 @@ describe("Claude bundle plugin inspect integration", () => {
|
||||
expect(values).toEqual(expect.arrayContaining([...params.includes]));
|
||||
}
|
||||
|
||||
function expectNoDiagnostics(diagnostics: unknown[]) {
|
||||
expect(diagnostics).toEqual([]);
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-claude-bundle-"));
|
||||
|
||||
@@ -182,7 +186,7 @@ describe("Claude bundle plugin inspect integration", () => {
|
||||
expect(mcp.hasSupportedStdioServer).toBe(true);
|
||||
expect(mcp.supportedServerNames).toContain("test-stdio-server");
|
||||
expect(mcp.unsupportedServerNames).toContain("test-sse-server");
|
||||
expect(mcp.diagnostics).toEqual([]);
|
||||
expectNoDiagnostics(mcp.diagnostics);
|
||||
});
|
||||
|
||||
it("inspects LSP runtime support with stdio server", () => {
|
||||
@@ -195,6 +199,6 @@ describe("Claude bundle plugin inspect integration", () => {
|
||||
expect(lsp.hasStdioServer).toBe(true);
|
||||
expect(lsp.supportedServerNames).toContain("typescript-lsp");
|
||||
expect(lsp.unsupportedServerNames).toEqual([]);
|
||||
expect(lsp.diagnostics).toEqual([]);
|
||||
expectNoDiagnostics(lsp.diagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,37 @@ function writeBundleManifest(
|
||||
fs.writeFileSync(path.join(rootDir, relativePath), JSON.stringify(manifest), "utf-8");
|
||||
}
|
||||
|
||||
function writeJsonFile(rootDir: string, relativePath: string, value: unknown) {
|
||||
mkdirSafe(path.dirname(path.join(rootDir, relativePath)));
|
||||
fs.writeFileSync(path.join(rootDir, relativePath), JSON.stringify(value), "utf-8");
|
||||
}
|
||||
|
||||
function setupClaudeHookFixture(
|
||||
rootDir: string,
|
||||
kind: "default-hooks" | "custom-hooks" | "no-hooks",
|
||||
) {
|
||||
mkdirSafe(path.join(rootDir, ".claude-plugin"));
|
||||
if (kind === "default-hooks") {
|
||||
mkdirSafe(path.join(rootDir, "hooks"));
|
||||
writeJsonFile(rootDir, "hooks/hooks.json", { hooks: [] });
|
||||
writeBundleManifest(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH, {
|
||||
name: "Hook Plugin",
|
||||
description: "Claude hooks fixture",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (kind === "custom-hooks") {
|
||||
mkdirSafe(path.join(rootDir, "custom-hooks"));
|
||||
writeBundleManifest(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH, {
|
||||
name: "Custom Hook Plugin",
|
||||
hooks: "custom-hooks",
|
||||
});
|
||||
return;
|
||||
}
|
||||
mkdirSafe(path.join(rootDir, "skills"));
|
||||
writeBundleManifest(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH, { name: "No Hooks" });
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
@@ -224,36 +255,7 @@ describe("bundle manifest parsing", () => {
|
||||
},
|
||||
] as const)("$name", ({ setupKind, expectedHooks, hasHooksCapability }) => {
|
||||
const rootDir = makeTempDir();
|
||||
mkdirSafe(path.join(rootDir, ".claude-plugin"));
|
||||
if (setupKind === "default-hooks") {
|
||||
mkdirSafe(path.join(rootDir, "hooks"));
|
||||
fs.writeFileSync(path.join(rootDir, "hooks", "hooks.json"), '{"hooks":[]}', "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
|
||||
JSON.stringify({
|
||||
name: "Hook Plugin",
|
||||
description: "Claude hooks fixture",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
} else if (setupKind === "custom-hooks") {
|
||||
mkdirSafe(path.join(rootDir, "custom-hooks"));
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
|
||||
JSON.stringify({
|
||||
name: "Custom Hook Plugin",
|
||||
hooks: "custom-hooks",
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
} else {
|
||||
mkdirSafe(path.join(rootDir, "skills"));
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
|
||||
JSON.stringify({ name: "No Hooks" }),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
setupClaudeHookFixture(rootDir, setupKind);
|
||||
const manifest = expectLoadedManifest(rootDir, "claude");
|
||||
expect(manifest.hooks).toEqual(expectedHooks);
|
||||
expect(manifest.capabilities.includes("hooks")).toBe(hasHooksCapability);
|
||||
|
||||
@@ -28,6 +28,10 @@ async function expectResolvedPathEqual(actual: unknown, expected: string): Promi
|
||||
);
|
||||
}
|
||||
|
||||
function expectNoDiagnostics(diagnostics: unknown[]) {
|
||||
expect(diagnostics).toEqual([]);
|
||||
}
|
||||
|
||||
const tempHarness = createBundleMcpTempHarness();
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -98,7 +102,7 @@ describe("loadEnabledBundleMcpConfig", () => {
|
||||
const loadedServerPath = typeof loadedArgs?.[0] === "string" ? loadedArgs[0] : undefined;
|
||||
const resolvedPluginRoot = await fs.realpath(pluginRoot);
|
||||
|
||||
expect(loaded.diagnostics).toEqual([]);
|
||||
expectNoDiagnostics(loaded.diagnostics);
|
||||
expect(isRecord(loadedServer) ? loadedServer.command : undefined).toBe("node");
|
||||
expect(loadedArgs).toHaveLength(1);
|
||||
expect(loadedServerPath).toBeDefined();
|
||||
@@ -191,7 +195,7 @@ describe("loadEnabledBundleMcpConfig", () => {
|
||||
const loadedEnv =
|
||||
isRecord(loadedServer) && isRecord(loadedServer.env) ? loadedServer.env : {};
|
||||
|
||||
expect(loaded.diagnostics).toEqual([]);
|
||||
expectNoDiagnostics(loaded.diagnostics);
|
||||
await expectResolvedPathEqual(loadedCwd, pluginRoot);
|
||||
expect(typeof loadedCommand).toBe("string");
|
||||
expect(loadedArgs).toHaveLength(2);
|
||||
|
||||
@@ -35,6 +35,21 @@ const { ClawHubRequestError } = await import("../infra/clawhub.js");
|
||||
const { CLAWHUB_INSTALL_ERROR_CODE, formatClawHubSpecifier, installPluginFromClawHub } =
|
||||
await import("./clawhub.js");
|
||||
|
||||
async function expectClawHubInstallError(params: {
|
||||
setup?: () => void;
|
||||
spec: string;
|
||||
expected: {
|
||||
ok: false;
|
||||
code: (typeof CLAWHUB_INSTALL_ERROR_CODE)[keyof typeof CLAWHUB_INSTALL_ERROR_CODE];
|
||||
error: string;
|
||||
};
|
||||
}) {
|
||||
params.setup?.();
|
||||
await expect(installPluginFromClawHub({ spec: params.spec })).resolves.toMatchObject(
|
||||
params.expected,
|
||||
);
|
||||
}
|
||||
|
||||
describe("installPluginFromClawHub", () => {
|
||||
beforeEach(() => {
|
||||
parseClawHubPluginSpecMock.mockReset();
|
||||
@@ -208,7 +223,6 @@ describe("installPluginFromClawHub", () => {
|
||||
},
|
||||
},
|
||||
] as const)("$name", async ({ setup, spec, expected }) => {
|
||||
setup();
|
||||
await expect(installPluginFromClawHub({ spec })).resolves.toMatchObject(expected);
|
||||
await expectClawHubInstallError({ setup, spec, expected });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,6 +68,10 @@ function bundledSkillPath(repoRoot: string, pluginId: string, ...relativePath: s
|
||||
return path.join(bundledPluginDir(repoRoot, pluginId), ...relativePath);
|
||||
}
|
||||
|
||||
function expectBundledSkills(repoRoot: string, pluginId: string, skills: string[]) {
|
||||
expect(readBundledManifest(repoRoot, pluginId).skills).toEqual(skills);
|
||||
}
|
||||
|
||||
function createTlonSkillPlugin(repoRoot: string, skillPath = "node_modules/@tloncorp/tlon-skill") {
|
||||
return createPlugin(repoRoot, {
|
||||
id: "tlon",
|
||||
@@ -117,8 +121,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
"utf8",
|
||||
),
|
||||
).toContain("ACP Router");
|
||||
const bundledManifest = readBundledManifest(repoRoot, "acpx");
|
||||
expect(bundledManifest.skills).toEqual(["./skills"]);
|
||||
expectBundledSkills(repoRoot, "acpx", ["./skills"]);
|
||||
const packageJson = readBundledPackageJson(repoRoot, "acpx");
|
||||
expect(packageJson.openclaw?.extensions).toEqual(["./index.js"]);
|
||||
});
|
||||
@@ -172,8 +175,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
expect(fs.existsSync(path.join(bundledPluginDir(repoRoot, "tlon"), "node_modules"))).toBe(
|
||||
false,
|
||||
);
|
||||
const bundledManifest = readBundledManifest(repoRoot, "tlon");
|
||||
expect(bundledManifest.skills).toEqual(["./bundled-skills/@tloncorp/tlon-skill"]);
|
||||
expectBundledSkills(repoRoot, "tlon", ["./bundled-skills/@tloncorp/tlon-skill"]);
|
||||
});
|
||||
|
||||
it("falls back to repo-root hoisted node_modules skill paths", () => {
|
||||
@@ -192,8 +194,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
"utf8",
|
||||
),
|
||||
).toContain("Hoisted Tlon Skill");
|
||||
const bundledManifest = readBundledManifest(repoRoot, "tlon");
|
||||
expect(bundledManifest.skills).toEqual(["./bundled-skills/@tloncorp/tlon-skill"]);
|
||||
expectBundledSkills(repoRoot, "tlon", ["./bundled-skills/@tloncorp/tlon-skill"]);
|
||||
});
|
||||
|
||||
it("omits missing declared skill paths and removes stale generated outputs", () => {
|
||||
@@ -212,8 +213,7 @@ describe("copyBundledPluginMetadata", () => {
|
||||
|
||||
copyBundledPluginMetadata({ repoRoot });
|
||||
|
||||
const bundledManifest = readBundledManifest(repoRoot, "tlon");
|
||||
expect(bundledManifest.skills).toEqual([]);
|
||||
expectBundledSkills(repoRoot, "tlon", []);
|
||||
expect(fs.existsSync(path.join(repoRoot, "dist", "extensions", "tlon", "bundled-skills"))).toBe(
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -115,6 +115,23 @@ function createPackagePlugin(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function createPackagePluginWithEntry(params: {
|
||||
packageDir: string;
|
||||
packageName: string;
|
||||
pluginId?: string;
|
||||
entryPath?: string;
|
||||
}) {
|
||||
const entryPath = params.entryPath ?? "src/index.ts";
|
||||
mkdirSafe(path.dirname(path.join(params.packageDir, entryPath)));
|
||||
createPackagePlugin({
|
||||
packageDir: params.packageDir,
|
||||
packageName: params.packageName,
|
||||
extensions: [`./${entryPath}`],
|
||||
...(params.pluginId ? { pluginId: params.pluginId } : {}),
|
||||
});
|
||||
writePluginEntry(path.join(params.packageDir, entryPath));
|
||||
}
|
||||
|
||||
function createBundleRoot(bundleDir: string, markerPath: string, manifest?: unknown) {
|
||||
mkdirSafe(path.dirname(path.join(bundleDir, markerPath)));
|
||||
if (manifest) {
|
||||
@@ -296,13 +313,11 @@ describe("discoverOpenClawPlugins", () => {
|
||||
name: "derives unscoped ids for scoped packages",
|
||||
setup: (stateDir: string) => {
|
||||
const packageDir = path.join(stateDir, "extensions", "voice-call-pack");
|
||||
mkdirSafe(path.join(packageDir, "src"));
|
||||
createPackagePlugin({
|
||||
createPackagePluginWithEntry({
|
||||
packageDir,
|
||||
packageName: "@openclaw/voice-call",
|
||||
extensions: ["./src/index.ts"],
|
||||
entryPath: "src/index.ts",
|
||||
});
|
||||
writePluginEntry(path.join(packageDir, "src", "index.ts"));
|
||||
return {};
|
||||
},
|
||||
includes: ["voice-call"],
|
||||
@@ -311,14 +326,12 @@ describe("discoverOpenClawPlugins", () => {
|
||||
name: "strips provider suffixes from package-derived ids",
|
||||
setup: (stateDir: string) => {
|
||||
const packageDir = path.join(stateDir, "extensions", "ollama-provider-pack");
|
||||
mkdirSafe(path.join(packageDir, "src"));
|
||||
createPackagePlugin({
|
||||
createPackagePluginWithEntry({
|
||||
packageDir,
|
||||
packageName: "@openclaw/ollama-provider",
|
||||
extensions: ["./src/index.ts"],
|
||||
pluginId: "ollama",
|
||||
entryPath: "src/index.ts",
|
||||
});
|
||||
writePluginEntry(path.join(packageDir, "src", "index.ts"));
|
||||
return {};
|
||||
},
|
||||
includes: ["ollama"],
|
||||
@@ -332,14 +345,12 @@ describe("discoverOpenClawPlugins", () => {
|
||||
["microsoft-speech-pack", "@openclaw/microsoft-speech", "microsoft"],
|
||||
] as const) {
|
||||
const packageDir = path.join(stateDir, "extensions", dirName);
|
||||
mkdirSafe(path.join(packageDir, "src"));
|
||||
createPackagePlugin({
|
||||
createPackagePluginWithEntry({
|
||||
packageDir,
|
||||
packageName,
|
||||
extensions: ["./src/index.ts"],
|
||||
pluginId,
|
||||
entryPath: "src/index.ts",
|
||||
});
|
||||
writePluginEntry(path.join(packageDir, "src", "index.ts"));
|
||||
}
|
||||
return {};
|
||||
},
|
||||
@@ -350,12 +361,11 @@ describe("discoverOpenClawPlugins", () => {
|
||||
name: "treats configured directory paths as plugin packages",
|
||||
setup: (stateDir: string) => {
|
||||
const packageDir = path.join(stateDir, "packs", "demo-plugin-dir");
|
||||
createPackagePlugin({
|
||||
createPackagePluginWithEntry({
|
||||
packageDir,
|
||||
packageName: "@openclaw/demo-plugin-dir",
|
||||
extensions: ["./index.js"],
|
||||
entryPath: "index.js",
|
||||
});
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "module.exports = {}", "utf-8");
|
||||
return { extraPaths: [packageDir] };
|
||||
},
|
||||
includes: ["demo-plugin-dir"],
|
||||
|
||||
@@ -44,6 +44,19 @@ function mockRemoteMarketplaceClone(manifest: unknown) {
|
||||
});
|
||||
}
|
||||
|
||||
async function expectRemoteMarketplaceError(params: { manifest: unknown; expectedError: string }) {
|
||||
mockRemoteMarketplaceClone(params.manifest);
|
||||
|
||||
const { listMarketplacePlugins } = await import("./marketplace.js");
|
||||
const result = await listMarketplacePlugins({ marketplace: "owner/repo" });
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: params.expectedError,
|
||||
});
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
describe("marketplace plugins", () => {
|
||||
afterEach(() => {
|
||||
installPluginFromPathMock.mockReset();
|
||||
@@ -296,15 +309,6 @@ describe("marketplace plugins", () => {
|
||||
"remote marketplaces may not use HTTP(S) plugin paths",
|
||||
},
|
||||
] as const)("$name", async ({ manifest, expectedError }) => {
|
||||
mockRemoteMarketplaceClone(manifest);
|
||||
|
||||
const { listMarketplacePlugins } = await import("./marketplace.js");
|
||||
const result = await listMarketplacePlugins({ marketplace: "owner/repo" });
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: expectedError,
|
||||
});
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
await expectRemoteMarketplaceError({ manifest, expectedError });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user