Files
open-design/apps/daemon/tests/plugins-apply.test.ts
lorenzozaneandGitHub 78d5637cb4 fix(daemon,web): forward pure plugin applies and honor persisted capability grants (#7597)
* fix(daemon,web): forward pure plugin applies and honor persisted capability grants

Pure apply returns snapshotId "" for existing projects, so the web run
path now forwards pluginId and pluginInputs when no snapshot id exists.
The daemon trust vocabulary now accepts pipeline:* and applyPlugin honors
stored capabilitiesGranted with prompt:inject floor.

Fixes #6186

* test(web): cover pure-apply plugin identity fallback in run requests
2026-10-16 06:52:55 +00:00

169 lines
6.0 KiB
TypeScript

// Daemon `applyPlugin` purity test (plan F4).
//
// applyPlugin must:
// - Compute a deterministic snapshot for the same inputs.
// - Refuse to mutate the registry / FS / SQLite — caller owns persistence.
// - Throw MissingInputError when a required input is absent.
import { describe, expect, it } from 'vitest';
import path from 'node:path';
import { applyPlugin, MissingInputError } from '../src/plugins/apply.js';
import { defaultRegistryRoots } from '../src/plugins/registry.js';
import { TRUSTED_DEFAULT_CAPABILITIES } from '../src/plugins/trust.js';
import type { ContextItem, InstalledPluginRecord } from '@open-design/contracts';
function pluginFixture(extra: Partial<InstalledPluginRecord> = {}): InstalledPluginRecord {
return {
id: 'sample-plugin',
title: 'Sample Plugin',
version: '1.0.0',
sourceKind: 'local',
source: '/tmp/sample-plugin',
sourceMarketplaceId: undefined,
pinnedRef: undefined,
sourceDigest: undefined,
trust: 'trusted',
capabilitiesGranted: ['prompt:inject'],
fsPath: '/tmp/sample-plugin',
installedAt: 0,
updatedAt: 0,
manifest: {
name: 'sample-plugin',
title: 'Sample Plugin',
version: '1.0.0',
description: 'Fixture for apply tests.',
od: {
kind: 'skill',
taskKind: 'new-generation',
useCase: { query: 'Generate a {{topic}} brief.' },
inputs: [
{ name: 'topic', type: 'string', required: true },
{ name: 'audience', type: 'string', default: 'general' },
],
context: {
skills: [{ ref: 'sample-skill' }],
atoms: ['todo-write'],
},
capabilities: ['prompt:inject'],
},
},
...extra,
};
}
const REGISTRY = {
skills: [{ id: 'sample-skill', title: 'Sample Skill' }],
designSystems: [],
craft: [],
atoms: [{ id: 'todo-write', label: 'Todo write' }],
};
describe('applyPlugin', () => {
it('produces a deterministic snapshot for the same inputs', () => {
const a = applyPlugin({ plugin: pluginFixture(), inputs: { topic: 'design' }, registry: REGISTRY });
const b = applyPlugin({ plugin: pluginFixture(), inputs: { topic: 'design' }, registry: REGISTRY });
expect(a.manifestSourceDigest).toBe(b.manifestSourceDigest);
expect(a.result.appliedPlugin.manifestSourceDigest).toBe(b.result.appliedPlugin.manifestSourceDigest);
expect(a.result.appliedPlugin.appliedAt).not.toBe(0);
});
it('throws MissingInputError when a required input is missing', () => {
expect(() => applyPlugin({ plugin: pluginFixture(), inputs: {}, registry: REGISTRY })).toThrow(MissingInputError);
});
it('coerces optional inputs by defaulting when blank', () => {
const result = applyPlugin({ plugin: pluginFixture(), inputs: { topic: 'design' }, registry: REGISTRY });
expect(result.result.appliedPlugin.inputs.audience).toBe('general');
});
it('resolves localized use-case queries at apply time', () => {
const base = pluginFixture();
const result = applyPlugin({
plugin: {
...base,
manifest: {
...base.manifest,
od: {
...base.manifest.od,
useCase: {
query: {
en: 'Generate a {{topic}} brief.',
'zh-CN': '生成一份关于 {{topic}} 的简报。',
},
},
},
},
},
inputs: { topic: 'design' },
registry: REGISTRY,
locale: 'zh-CN',
});
expect(result.result.query).toBe('生成一份关于 {{topic}} 的简报。');
expect(result.result.appliedPlugin.query).toBe('生成一份关于 {{topic}} 的简报。');
});
it('uses persisted capabilitiesGranted for a restricted plugin', () => {
const capabilitiesGranted = ['pipeline:*', 'prompt:inject'];
const result = applyPlugin({
plugin: pluginFixture({ trust: 'restricted', capabilitiesGranted }),
inputs: { topic: 'design' },
registry: REGISTRY,
});
expect(result.result.capabilitiesGranted).toEqual(capabilitiesGranted);
expect(result.result.appliedPlugin.capabilitiesGranted).toEqual(capabilitiesGranted);
});
it('does not resurrect a persisted capability revoked from a trusted plugin', () => {
const capabilitiesGranted = TRUSTED_DEFAULT_CAPABILITIES.filter((cap) => cap !== 'pipeline:*');
const result = applyPlugin({
plugin: pluginFixture({ capabilitiesGranted }),
inputs: { topic: 'design' },
registry: REGISTRY,
});
expect(result.result.capabilitiesGranted).not.toContain('pipeline:*');
expect(result.result.appliedPlugin.capabilitiesGranted).not.toContain('pipeline:*');
});
it('emits skill+atom items in resolvedContext.items', () => {
const result = applyPlugin({ plugin: pluginFixture(), inputs: { topic: 'design' }, registry: REGISTRY });
const kinds = result.result.contextItems.map((c: ContextItem) => c.kind);
expect(kinds).toContain('skill');
expect(kinds).toContain('atom');
});
it('does not require a registry roots argument (no FS access at apply time)', () => {
// Sanity: the function must not reach for the on-disk plugin folder.
const roots = defaultRegistryRoots();
const expectedDataDir = path.resolve(process.env.OD_DATA_DIR ?? path.join(process.cwd(), '.od'));
expect(roots.userPluginsRoot).toBe(path.join(expectedDataDir, 'plugins'));
const result = applyPlugin({ plugin: pluginFixture(), inputs: { topic: 'design' }, registry: REGISTRY });
expect(result.result.appliedPlugin.pluginId).toBe('sample-plugin');
});
it('does not interpret a community strategy extension as a bundled declaration', () => {
const base = pluginFixture();
const plugin = pluginFixture({
sourceKind: 'local',
manifest: {
...base.manifest,
od: {
...base.manifest.od,
strategy: {
schema: 'community.example/v9',
customRecipe: true,
},
},
},
});
expect(applyPlugin({
plugin,
inputs: { topic: 'design' },
registry: REGISTRY,
}).result.appliedPlugin.pluginId).toBe('sample-plugin');
});
});