open-design/apps/daemon/tests/plugins-apply.test.ts
2026-05-14 16:35:46 +08:00

128 lines
4.8 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('grants trusted defaults plus required caps for a trusted plugin', () => {
const result = applyPlugin({ plugin: pluginFixture(), inputs: { topic: 'design' }, registry: REGISTRY });
for (const cap of TRUSTED_DEFAULT_CAPABILITIES) {
expect(result.result.capabilitiesGranted).toContain(cap);
}
});
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');
});
});