mirror of https://github.com/openclaw/openclaw.git
202 lines
6.3 KiB
TypeScript
202 lines
6.3 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH,
|
|
CODEX_BUNDLE_MANIFEST_RELATIVE_PATH,
|
|
CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH,
|
|
detectBundleManifestFormat,
|
|
loadBundleManifest,
|
|
} from "./bundle-manifest.js";
|
|
import {
|
|
cleanupTrackedTempDirs,
|
|
makeTrackedTempDir,
|
|
mkdirSafeDir,
|
|
} from "./test-helpers/fs-fixtures.js";
|
|
|
|
const tempDirs: string[] = [];
|
|
|
|
function makeTempDir() {
|
|
return makeTrackedTempDir("openclaw-bundle-manifest", tempDirs);
|
|
}
|
|
|
|
const mkdirSafe = mkdirSafeDir;
|
|
|
|
afterEach(() => {
|
|
cleanupTrackedTempDirs(tempDirs);
|
|
});
|
|
|
|
describe("bundle manifest parsing", () => {
|
|
it("detects and loads Codex bundle manifests", () => {
|
|
const rootDir = makeTempDir();
|
|
mkdirSafe(path.join(rootDir, ".codex-plugin"));
|
|
mkdirSafe(path.join(rootDir, "skills"));
|
|
mkdirSafe(path.join(rootDir, "hooks"));
|
|
fs.writeFileSync(
|
|
path.join(rootDir, CODEX_BUNDLE_MANIFEST_RELATIVE_PATH),
|
|
JSON.stringify({
|
|
name: "Sample Bundle",
|
|
description: "Codex fixture",
|
|
skills: "skills",
|
|
hooks: "hooks",
|
|
mcpServers: {
|
|
sample: {
|
|
command: "node",
|
|
args: ["server.js"],
|
|
},
|
|
},
|
|
apps: {
|
|
sample: {
|
|
title: "Sample App",
|
|
},
|
|
},
|
|
}),
|
|
"utf-8",
|
|
);
|
|
|
|
expect(detectBundleManifestFormat(rootDir)).toBe("codex");
|
|
const result = loadBundleManifest({ rootDir, bundleFormat: "codex" });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) {
|
|
return;
|
|
}
|
|
expect(result.manifest).toMatchObject({
|
|
id: "sample-bundle",
|
|
name: "Sample Bundle",
|
|
description: "Codex fixture",
|
|
bundleFormat: "codex",
|
|
skills: ["skills"],
|
|
hooks: ["hooks"],
|
|
capabilities: expect.arrayContaining(["hooks", "skills", "mcpServers", "apps"]),
|
|
});
|
|
});
|
|
|
|
it("detects and loads Claude bundle manifests from the component layout", () => {
|
|
const rootDir = makeTempDir();
|
|
mkdirSafe(path.join(rootDir, ".claude-plugin"));
|
|
mkdirSafe(path.join(rootDir, "skill-packs", "starter"));
|
|
mkdirSafe(path.join(rootDir, "commands-pack"));
|
|
mkdirSafe(path.join(rootDir, "agents-pack"));
|
|
mkdirSafe(path.join(rootDir, "hooks-pack"));
|
|
mkdirSafe(path.join(rootDir, "mcp"));
|
|
mkdirSafe(path.join(rootDir, "lsp"));
|
|
mkdirSafe(path.join(rootDir, "styles"));
|
|
mkdirSafe(path.join(rootDir, "hooks"));
|
|
fs.writeFileSync(path.join(rootDir, "hooks", "hooks.json"), '{"hooks":[]}', "utf-8");
|
|
fs.writeFileSync(path.join(rootDir, "settings.json"), '{"hideThinkingBlock":true}', "utf-8");
|
|
fs.writeFileSync(
|
|
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
|
|
JSON.stringify({
|
|
name: "Claude Sample",
|
|
description: "Claude fixture",
|
|
skills: ["skill-packs/starter"],
|
|
commands: "commands-pack",
|
|
agents: "agents-pack",
|
|
hooks: "hooks-pack",
|
|
mcpServers: "mcp",
|
|
lspServers: "lsp",
|
|
outputStyles: "styles",
|
|
}),
|
|
"utf-8",
|
|
);
|
|
|
|
expect(detectBundleManifestFormat(rootDir)).toBe("claude");
|
|
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) {
|
|
return;
|
|
}
|
|
expect(result.manifest).toMatchObject({
|
|
id: "claude-sample",
|
|
name: "Claude Sample",
|
|
description: "Claude fixture",
|
|
bundleFormat: "claude",
|
|
skills: ["skill-packs/starter", "commands-pack"],
|
|
settingsFiles: ["settings.json"],
|
|
hooks: [],
|
|
capabilities: expect.arrayContaining([
|
|
"hooks",
|
|
"skills",
|
|
"commands",
|
|
"agents",
|
|
"mcpServers",
|
|
"lspServers",
|
|
"outputStyles",
|
|
"settings",
|
|
]),
|
|
});
|
|
});
|
|
|
|
it("detects and loads Cursor bundle manifests", () => {
|
|
const rootDir = makeTempDir();
|
|
mkdirSafe(path.join(rootDir, ".cursor-plugin"));
|
|
mkdirSafe(path.join(rootDir, "skills"));
|
|
mkdirSafe(path.join(rootDir, ".cursor", "commands"));
|
|
mkdirSafe(path.join(rootDir, ".cursor", "rules"));
|
|
mkdirSafe(path.join(rootDir, ".cursor", "agents"));
|
|
fs.writeFileSync(path.join(rootDir, ".cursor", "hooks.json"), '{"hooks":[]}', "utf-8");
|
|
fs.writeFileSync(
|
|
path.join(rootDir, CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH),
|
|
JSON.stringify({
|
|
name: "Cursor Sample",
|
|
description: "Cursor fixture",
|
|
mcpServers: "./.mcp.json",
|
|
}),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(path.join(rootDir, ".mcp.json"), '{"servers":{}}', "utf-8");
|
|
|
|
expect(detectBundleManifestFormat(rootDir)).toBe("cursor");
|
|
const result = loadBundleManifest({ rootDir, bundleFormat: "cursor" });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) {
|
|
return;
|
|
}
|
|
expect(result.manifest).toMatchObject({
|
|
id: "cursor-sample",
|
|
name: "Cursor Sample",
|
|
description: "Cursor fixture",
|
|
bundleFormat: "cursor",
|
|
skills: ["skills", ".cursor/commands"],
|
|
hooks: [],
|
|
capabilities: expect.arrayContaining([
|
|
"skills",
|
|
"commands",
|
|
"agents",
|
|
"rules",
|
|
"hooks",
|
|
"mcpServers",
|
|
]),
|
|
});
|
|
});
|
|
|
|
it("detects manifestless Claude bundles from the default layout", () => {
|
|
const rootDir = makeTempDir();
|
|
mkdirSafe(path.join(rootDir, "commands"));
|
|
mkdirSafe(path.join(rootDir, "skills"));
|
|
fs.writeFileSync(path.join(rootDir, "settings.json"), '{"hideThinkingBlock":true}', "utf-8");
|
|
|
|
expect(detectBundleManifestFormat(rootDir)).toBe("claude");
|
|
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
|
|
expect(result.ok).toBe(true);
|
|
if (!result.ok) {
|
|
return;
|
|
}
|
|
|
|
expect(result.manifest.id).toBe(path.basename(rootDir).toLowerCase());
|
|
expect(result.manifest.skills).toEqual(["skills", "commands"]);
|
|
expect(result.manifest.settingsFiles).toEqual(["settings.json"]);
|
|
expect(result.manifest.capabilities).toEqual(
|
|
expect.arrayContaining(["skills", "commands", "settings"]),
|
|
);
|
|
});
|
|
|
|
it("does not misclassify native index plugins as manifestless Claude bundles", () => {
|
|
const rootDir = makeTempDir();
|
|
mkdirSafe(path.join(rootDir, "commands"));
|
|
fs.writeFileSync(path.join(rootDir, "index.ts"), "export default {}", "utf-8");
|
|
|
|
expect(detectBundleManifestFormat(rootDir)).toBeNull();
|
|
});
|
|
});
|