test: dedupe plugin manifest and wizard suites

This commit is contained in:
Peter Steinberger 2026-03-28 01:37:46 +00:00
parent fad42b19ee
commit c364fc8428
6 changed files with 288 additions and 270 deletions

View File

@ -22,6 +22,15 @@ function makeTempDir() {
const mkdirSafe = mkdirSafeDir;
function expectLoadedManifest(rootDir: string, bundleFormat: "codex" | "claude" | "cursor") {
const result = loadBundleManifest({ rootDir, bundleFormat });
expect(result.ok).toBe(true);
if (!result.ok) {
throw new Error("expected bundle manifest to load");
}
return result.manifest;
}
afterEach(() => {
cleanupTrackedTempDirs(tempDirs);
});
@ -55,12 +64,7 @@ describe("bundle manifest parsing", () => {
);
expect(detectBundleManifestFormat(rootDir)).toBe("codex");
const result = loadBundleManifest({ rootDir, bundleFormat: "codex" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest).toMatchObject({
expect(expectLoadedManifest(rootDir, "codex")).toMatchObject({
id: "sample-bundle",
name: "Sample Bundle",
description: "Codex fixture",
@ -101,12 +105,7 @@ describe("bundle manifest parsing", () => {
);
expect(detectBundleManifestFormat(rootDir)).toBe("claude");
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest).toMatchObject({
expect(expectLoadedManifest(rootDir, "claude")).toMatchObject({
id: "claude-sample",
name: "Claude Sample",
description: "Claude fixture",
@ -147,12 +146,7 @@ describe("bundle manifest parsing", () => {
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({
expect(expectLoadedManifest(rootDir, "cursor")).toMatchObject({
id: "cursor-sample",
name: "Cursor Sample",
description: "Cursor fixture",
@ -177,82 +171,69 @@ describe("bundle manifest parsing", () => {
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(
const manifest = expectLoadedManifest(rootDir, "claude");
expect(manifest.id).toBe(path.basename(rootDir).toLowerCase());
expect(manifest.skills).toEqual(["skills", "commands"]);
expect(manifest.settingsFiles).toEqual(["settings.json"]);
expect(manifest.capabilities).toEqual(
expect.arrayContaining(["skills", "commands", "settings"]),
);
});
it("resolves Claude bundle hooks from default and declared paths", () => {
it.each([
{
name: "resolves Claude bundle hooks from default and declared paths",
setupKind: "default-hooks",
expectedHooks: ["hooks/hooks.json"],
hasHooksCapability: true,
},
{
name: "resolves Claude bundle hooks from manifest-declared paths only",
setupKind: "custom-hooks",
expectedHooks: ["custom-hooks"],
hasHooksCapability: true,
},
{
name: "returns empty hooks for Claude bundles with no hooks directory",
setupKind: "no-hooks",
expectedHooks: [],
hasHooksCapability: false,
},
] as const)("$name", ({ setupKind, expectedHooks, hasHooksCapability }) => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, ".claude-plugin"));
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",
);
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
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",
);
}
expect(result.manifest.hooks).toEqual(["hooks/hooks.json"]);
expect(result.manifest.capabilities).toContain("hooks");
});
it("resolves Claude bundle hooks from manifest-declared paths only", () => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, ".claude-plugin"));
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",
);
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest.hooks).toEqual(["custom-hooks"]);
expect(result.manifest.capabilities).toContain("hooks");
});
it("returns empty hooks for Claude bundles with no hooks directory", () => {
const rootDir = makeTempDir();
mkdirSafe(path.join(rootDir, ".claude-plugin"));
mkdirSafe(path.join(rootDir, "skills"));
fs.writeFileSync(
path.join(rootDir, CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH),
JSON.stringify({ name: "No Hooks" }),
"utf-8",
);
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
return;
}
expect(result.manifest.hooks).toEqual([]);
expect(result.manifest.capabilities).not.toContain("hooks");
const manifest = expectLoadedManifest(rootDir, "claude");
expect(manifest.hooks).toEqual(expectedHooks);
expect(manifest.capabilities.includes("hooks")).toBe(hasHooksCapability);
});
it("does not misclassify native index plugins as manifestless Claude bundles", () => {

View File

@ -20,6 +20,14 @@ const BUNDLED_PLUGIN_METADATA_TEST_TIMEOUT_MS = 300_000;
installGeneratedPluginTempRootCleanup();
function expectTestOnlyArtifactsExcluded(artifacts: readonly string[]) {
for (const artifact of artifacts) {
expect(artifact).not.toMatch(/^test-/);
expect(artifact).not.toContain(".test-");
expect(artifact).not.toMatch(/\.test\.js$/);
}
}
describe("bundled plugin metadata", () => {
it(
"matches the generated metadata snapshot",
@ -49,13 +57,9 @@ describe("bundled plugin metadata", () => {
});
it("excludes test-only public surface artifacts", () => {
for (const entry of BUNDLED_PLUGIN_METADATA) {
for (const artifact of entry.publicSurfaceArtifacts ?? []) {
expect(artifact).not.toMatch(/^test-/);
expect(artifact).not.toContain(".test-");
expect(artifact).not.toMatch(/\.test\.js$/);
}
}
BUNDLED_PLUGIN_METADATA.forEach((entry) =>
expectTestOnlyArtifactsExcluded(entry.publicSurfaceArtifacts ?? []),
);
});
it("prefers built generated paths when present and falls back to source paths", () => {

View File

@ -23,24 +23,34 @@ describe("bundled provider auth env vars", () => {
});
it("reads bundled provider auth env vars from plugin manifests", () => {
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.brave).toEqual(["BRAVE_API_KEY"]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.firecrawl).toEqual(["FIRECRAWL_API_KEY"]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["github-copilot"]).toEqual([
"COPILOT_GITHUB_TOKEN",
"GH_TOKEN",
"GITHUB_TOKEN",
]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.perplexity).toEqual([
"PERPLEXITY_API_KEY",
"OPENROUTER_API_KEY",
]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.tavily).toEqual(["TAVILY_API_KEY"]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES["minimax-portal"]).toEqual([
"MINIMAX_OAUTH_TOKEN",
"MINIMAX_API_KEY",
]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.openai).toEqual(["OPENAI_API_KEY"]);
expect(BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES.fal).toEqual(["FAL_KEY"]);
expect(
Object.fromEntries(
[
["brave", ["BRAVE_API_KEY"]],
["firecrawl", ["FIRECRAWL_API_KEY"]],
["github-copilot", ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"]],
["perplexity", ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"]],
["tavily", ["TAVILY_API_KEY"]],
["minimax-portal", ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"]],
["openai", ["OPENAI_API_KEY"]],
["fal", ["FAL_KEY"]],
].map(([providerId]) => [
providerId,
BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES[
providerId as keyof typeof BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES
],
]),
),
).toEqual({
brave: ["BRAVE_API_KEY"],
firecrawl: ["FIRECRAWL_API_KEY"],
"github-copilot": ["COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"],
perplexity: ["PERPLEXITY_API_KEY", "OPENROUTER_API_KEY"],
tavily: ["TAVILY_API_KEY"],
"minimax-portal": ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
openai: ["OPENAI_API_KEY"],
fal: ["FAL_KEY"],
});
expect("openai-codex" in BUNDLED_PROVIDER_AUTH_ENV_VAR_CANDIDATES).toBe(false);
});

View File

@ -51,8 +51,9 @@ describe("provider auth choice manifest helpers", () => {
expect(resolveManifestProviderAuthChoice("openai-api-key")?.providerId).toBe("openai");
});
it("deduplicates flag metadata by option key + flag", () => {
loadPluginManifestRegistry.mockReturnValue({
it.each([
{
name: "deduplicates flag metadata by option key + flag",
plugins: [
{
id: "moonshot",
@ -80,21 +81,19 @@ describe("provider auth choice manifest helpers", () => {
],
},
],
});
expect(resolveManifestProviderOnboardAuthFlags()).toEqual([
{
optionKey: "moonshotApiKey",
authChoice: "moonshot-api-key",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
description: "Moonshot API key",
},
]);
});
it("resolves deprecated auth-choice aliases through manifest metadata", () => {
loadPluginManifestRegistry.mockReturnValue({
run: () =>
expect(resolveManifestProviderOnboardAuthFlags()).toEqual([
{
optionKey: "moonshotApiKey",
authChoice: "moonshot-api-key",
cliFlag: "--moonshot-api-key",
cliOption: "--moonshot-api-key <key>",
description: "Moonshot API key",
},
]),
},
{
name: "resolves deprecated auth-choice aliases through manifest metadata",
plugins: [
{
id: "minimax",
@ -108,14 +107,20 @@ describe("provider auth choice manifest helpers", () => {
],
},
],
run: () => {
expect(resolveManifestDeprecatedProviderAuthChoice("minimax")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("minimax-api")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("openai")).toBeUndefined();
},
},
])("$name", ({ plugins, run }) => {
loadPluginManifestRegistry.mockReturnValue({
plugins,
});
expect(resolveManifestDeprecatedProviderAuthChoice("minimax")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("minimax-api")?.choiceId).toBe(
"minimax-global-api",
);
expect(resolveManifestDeprecatedProviderAuthChoice("openai")).toBeUndefined();
run();
});
});

View File

@ -23,59 +23,74 @@ function createModel(id: string, name: string): ModelDefinitionConfig {
};
}
describe("provider onboarding preset appliers", () => {
it("creates provider and primary-model appliers for a default model preset", () => {
const appliers = createDefaultModelPresetAppliers({
primaryModelRef: "demo/demo-default",
resolveParams: () => ({
providerId: "demo",
api: "openai-completions" as const,
baseUrl: "https://demo.test/v1",
defaultModel: createModel("demo-default", "Demo Default"),
defaultModelId: "demo-default",
aliases: [{ modelRef: "demo/demo-default", alias: "Demo" }],
}),
});
it.each([
{
name: "creates provider and primary-model appliers for a default model preset",
kind: "default-model",
},
{
name: "passes variant args through default-models resolvers",
kind: "default-models",
},
{
name: "creates model-catalog appliers that preserve existing aliases",
kind: "catalog-models",
},
] as const)("$name", ({ kind }) => {
if (kind === "default-model") {
const appliers = createDefaultModelPresetAppliers({
primaryModelRef: "demo/demo-default",
resolveParams: () => ({
providerId: "demo",
api: "openai-completions" as const,
baseUrl: "https://demo.test/v1",
defaultModel: createModel("demo-default", "Demo Default"),
defaultModelId: "demo-default",
aliases: [{ modelRef: "demo/demo-default", alias: "Demo" }],
}),
});
const providerOnly = appliers.applyProviderConfig({});
expect(providerOnly.agents?.defaults?.models).toMatchObject({
"demo/demo-default": {
alias: "Demo",
},
});
expect(providerOnly.agents?.defaults?.model).toBeUndefined();
const providerOnly = appliers.applyProviderConfig({});
expect(providerOnly.agents?.defaults?.models).toMatchObject({
"demo/demo-default": {
alias: "Demo",
},
});
expect(providerOnly.agents?.defaults?.model).toBeUndefined();
const withPrimary = appliers.applyConfig({});
expect(withPrimary.agents?.defaults?.model).toEqual({
primary: "demo/demo-default",
});
});
const withPrimary = appliers.applyConfig({});
expect(withPrimary.agents?.defaults?.model).toEqual({
primary: "demo/demo-default",
});
return;
}
it("passes variant args through default-models resolvers", () => {
const appliers = createDefaultModelsPresetAppliers<[string]>({
primaryModelRef: "demo/a",
resolveParams: (_cfg, baseUrl) => ({
providerId: "demo",
api: "openai-completions" as const,
baseUrl,
defaultModels: [createModel("a", "Model A"), createModel("b", "Model B")],
aliases: [{ modelRef: "demo/a", alias: "Demo A" }],
}),
});
if (kind === "default-models") {
const appliers = createDefaultModelsPresetAppliers<[string]>({
primaryModelRef: "demo/a",
resolveParams: (_cfg, baseUrl) => ({
providerId: "demo",
api: "openai-completions" as const,
baseUrl,
defaultModels: [createModel("a", "Model A"), createModel("b", "Model B")],
aliases: [{ modelRef: "demo/a", alias: "Demo A" }],
}),
});
const cfg = appliers.applyConfig({}, "https://alt.test/v1");
expect(cfg.models?.providers?.demo).toMatchObject({
baseUrl: "https://alt.test/v1",
models: [
{ id: "a", name: "Model A" },
{ id: "b", name: "Model B" },
],
});
expect(cfg.agents?.defaults?.model).toEqual({
primary: "demo/a",
});
});
const cfg = appliers.applyConfig({}, "https://alt.test/v1");
expect(cfg.models?.providers?.demo).toMatchObject({
baseUrl: "https://alt.test/v1",
models: [
{ id: "a", name: "Model A" },
{ id: "b", name: "Model B" },
],
});
expect(cfg.agents?.defaults?.model).toEqual({
primary: "demo/a",
});
return;
}
it("creates model-catalog appliers that preserve existing aliases", () => {
const appliers = createModelCatalogPresetAppliers({
primaryModelRef: "catalog/default",
resolveParams: () => ({

View File

@ -60,92 +60,98 @@ function resolveWizardOptionsTwice(params: {
});
}
function expectSingleWizardChoice(params: {
provider: ProviderPlugin;
choice: string;
expectedOption: Record<string, unknown>;
expectedWizard: unknown;
}) {
resolvePluginProviders.mockReturnValue([params.provider]);
expect(resolveProviderWizardOptions({})).toEqual([params.expectedOption]);
expect(
resolveProviderPluginChoice({
providers: [params.provider],
choice: params.choice,
}),
).toEqual({
provider: params.provider,
method: params.provider.auth[0],
wizard: params.expectedWizard,
});
}
describe("provider wizard boundaries", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useRealTimers();
});
it("uses explicit setup choice ids and bound method ids", () => {
const provider = makeProvider({
id: "vllm",
label: "vLLM",
auth: [
{ id: "local", label: "Local", kind: "custom", run: vi.fn() },
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
],
wizard: {
setup: {
choiceId: "self-hosted-vllm",
methodId: "local",
choiceLabel: "vLLM local",
groupId: "local-runtimes",
groupLabel: "Local runtimes",
it.each([
{
name: "uses explicit setup choice ids and bound method ids",
provider: makeProvider({
id: "vllm",
label: "vLLM",
auth: [
{ id: "local", label: "Local", kind: "custom", run: vi.fn() },
{ id: "cloud", label: "Cloud", kind: "custom", run: vi.fn() },
],
wizard: {
setup: {
choiceId: "self-hosted-vllm",
methodId: "local",
choiceLabel: "vLLM local",
groupId: "local-runtimes",
groupLabel: "Local runtimes",
},
},
},
});
resolvePluginProviders.mockReturnValue([provider]);
expect(resolveProviderWizardOptions({})).toEqual([
{
}),
choice: "self-hosted-vllm",
expectedOption: {
value: "self-hosted-vllm",
label: "vLLM local",
groupId: "local-runtimes",
groupLabel: "Local runtimes",
},
]);
expect(
resolveProviderPluginChoice({
providers: [provider],
choice: "self-hosted-vllm",
}),
).toEqual({
provider,
method: provider.auth[0],
wizard: provider.wizard?.setup,
});
});
it("builds wizard options from method-level metadata", () => {
const provider = makeProvider({
id: "openai",
label: "OpenAI",
auth: [
{
id: "api-key",
label: "OpenAI API key",
kind: "api_key",
wizard: {
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
groupId: "openai",
groupLabel: "OpenAI",
onboardingScopes: ["text-inference"],
resolveWizard: (provider: ProviderPlugin) => provider.wizard?.setup,
},
{
name: "builds wizard options from method-level metadata",
provider: makeProvider({
id: "openai",
label: "OpenAI",
auth: [
{
id: "api-key",
label: "OpenAI API key",
kind: "api_key",
wizard: {
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
groupId: "openai",
groupLabel: "OpenAI",
onboardingScopes: ["text-inference"],
},
run: vi.fn(),
},
run: vi.fn(),
},
],
});
resolvePluginProviders.mockReturnValue([provider]);
expect(resolveProviderWizardOptions({})).toEqual([
{
],
}),
choice: "openai-api-key",
expectedOption: {
value: "openai-api-key",
label: "OpenAI API key",
groupId: "openai",
groupLabel: "OpenAI",
onboardingScopes: ["text-inference"],
},
]);
expect(
resolveProviderPluginChoice({
providers: [provider],
choice: "openai-api-key",
}),
).toEqual({
resolveWizard: (provider: ProviderPlugin) => provider.auth[0]?.wizard,
},
] as const)("$name", ({ provider, choice, expectedOption, resolveWizard }) => {
expectSingleWizardChoice({
provider,
method: provider.auth[0],
wizard: provider.auth[0]?.wizard,
choice,
expectedOption,
expectedWizard: resolveWizard(provider),
});
});
@ -317,27 +323,24 @@ describe("provider wizard boundaries", () => {
expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
});
it("skips provider-wizard memoization when plugin cache opt-outs are set", () => {
it.each([
{
name: "skips provider-wizard memoization when plugin cache opt-outs are set",
env: {
OPENCLAW_HOME: "/tmp/openclaw-home",
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
} as NodeJS.ProcessEnv,
},
{
name: "skips provider-wizard memoization when discovery cache ttl is zero",
env: {
OPENCLAW_HOME: "/tmp/openclaw-home",
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "0",
} as NodeJS.ProcessEnv,
},
] as const)("$name", ({ env }) => {
const provider = createSglangSetupProvider();
const config = createSglangConfig();
const env = {
OPENCLAW_HOME: "/tmp/openclaw-home",
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
} as NodeJS.ProcessEnv;
resolvePluginProviders.mockReturnValue([provider]);
resolveWizardOptionsTwice({ config, env });
expect(resolvePluginProviders).toHaveBeenCalledTimes(2);
});
it("skips provider-wizard memoization when discovery cache ttl is zero", () => {
const provider = createSglangSetupProvider();
const config = createSglangConfig();
const env = {
OPENCLAW_HOME: "/tmp/openclaw-home",
OPENCLAW_PLUGIN_DISCOVERY_CACHE_MS: "0",
} as NodeJS.ProcessEnv;
resolvePluginProviders.mockReturnValue([provider]);
resolveWizardOptionsTwice({ config, env });