152 lines
7.3 KiB
TypeScript
152 lines
7.3 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { readFile } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
BUNDLED_CODING_PLUGIN_ROOTS,
|
|
CodingPluginManifestError,
|
|
loadBundledCodingPluginDefinitions,
|
|
loadBundledCodingPluginDefinitionsSync,
|
|
loadCodingPluginDefinition,
|
|
parseCodingPluginManifest,
|
|
parseAgentPluginsRootManifest,
|
|
resolveBundledCodingPluginRootPaths,
|
|
} from '../../electron/coding-plugins/manifest';
|
|
import {
|
|
AGENT_PLUGINS_SCHEMA_URL,
|
|
DATA_SERVICE_PLUGIN_DEFINITION,
|
|
DATA_SERVICE_TOOL_NAMES,
|
|
} from '../../shared/coding-plugins';
|
|
|
|
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
|
|
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
|
|
const WEB_SEARCH_ROOT = path.resolve('resources/coding-plugins/web-search');
|
|
|
|
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
|
|
return {
|
|
root: JSON.parse(await readFile(path.join(PACKAGE_ROOT, 'plugin.json'), 'utf8')) as Record<string, unknown>,
|
|
capability: JSON.parse(await readFile(path.join(PACKAGE_ROOT, 'com.makelore/capability.json'), 'utf8')) as Record<string, unknown>,
|
|
};
|
|
}
|
|
|
|
describe('bundled coding plugin manifests', () => {
|
|
it('loads the fixed Data Service package and immutable declarations', async () => {
|
|
const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'));
|
|
const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'));
|
|
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource', 'web-search']);
|
|
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
|
PACKAGE_ROOT,
|
|
GAME_RESOURCE_ROOT,
|
|
WEB_SEARCH_ROOT,
|
|
]);
|
|
expect(definitions).toHaveLength(3);
|
|
expect(definitions[0]).toMatchObject({
|
|
id: 'makelore.data-service',
|
|
adapterId: 'data-service',
|
|
contractVersion: 1,
|
|
skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
|
|
});
|
|
expect(definitions[0]?.tools.map(({ name }) => name)).toEqual(DATA_SERVICE_TOOL_NAMES);
|
|
expect(definitions[0]).toEqual(DATA_SERVICE_PLUGIN_DEFINITION);
|
|
expect(definitions.slice(1)).toMatchObject([
|
|
{
|
|
id: 'makelore.game-resource', version: '1.0.0', runtimeKind: 'platform_hosted',
|
|
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000105',
|
|
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
|
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
|
|
},
|
|
{
|
|
id: 'makelore.web-search', version: '1.0.0', runtimeKind: 'platform_hosted',
|
|
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000204',
|
|
provenance: { source: 'bundled', packageRoot: 'web-search' },
|
|
skills: [{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' }],
|
|
},
|
|
]);
|
|
expect(startupDefinitions).toEqual(definitions);
|
|
expect(Object.isFrozen(startupDefinitions)).toBe(true);
|
|
expect(Object.isFrozen(startupDefinitions[0]?.operations)).toBe(true);
|
|
expect(definitions[0]?.operations).toHaveLength(14);
|
|
expect(Object.isFrozen(definitions[0])).toBe(true);
|
|
expect(Object.isFrozen(definitions[0]?.tools)).toBe(true);
|
|
expect(DATA_SERVICE_PLUGIN_DEFINITION.tools).toHaveLength(10);
|
|
});
|
|
|
|
it('accepts exact root and capability manifests and freezes the projection', async () => {
|
|
const { root, capability } = await packageManifests();
|
|
expect(parseAgentPluginsRootManifest(root, 'plugin.json').$schema).toBe(AGENT_PLUGINS_SCHEMA_URL);
|
|
const parsed = parseCodingPluginManifest(root, capability, {
|
|
packageRoot: PACKAGE_ROOT,
|
|
capabilityManifestPath: path.join(PACKAGE_ROOT, 'com.makelore/capability.json'),
|
|
});
|
|
expect(parsed.id).toBe('makelore.data-service');
|
|
expect(Object.isFrozen(parsed)).toBe(true);
|
|
expect(Object.isFrozen(parsed.tools[0]?.inputSchema)).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
['price field', (root: Record<string, unknown>) => { root.price = 1; }],
|
|
['non-empty MCP', (root: Record<string, unknown>) => { root.mcp = [{ name: 'unsupported' }]; }],
|
|
['non-empty scripts', (root: Record<string, unknown>) => { root.scripts = ['run.js']; }],
|
|
['unknown adapter', (_root: Record<string, unknown>, capability: Record<string, unknown>) => { capability.adapterId = 'other'; }],
|
|
['unknown surface', (_root: Record<string, unknown>, capability: Record<string, unknown>) => {
|
|
capability.surfaces = { projectSettings: 'other' };
|
|
}],
|
|
])('rejects %s', async (_label, mutate) => {
|
|
const { root, capability } = await packageManifests();
|
|
mutate(root, capability);
|
|
expect(() => parseCodingPluginManifest(root, capability, {
|
|
packageRoot: PACKAGE_ROOT,
|
|
capabilityManifestPath: path.join(PACKAGE_ROOT, 'com.makelore/capability.json'),
|
|
})).toThrow(CodingPluginManifestError);
|
|
});
|
|
|
|
it('rejects duplicate tools, escaping Skill entries and unconfirmed destructive tools', async () => {
|
|
const { root, capability } = await packageManifests();
|
|
const duplicate = structuredClone(capability) as Record<string, unknown>;
|
|
duplicate.tools = [...(capability.tools as unknown[]), (capability.tools as unknown[])[0]];
|
|
expect(() => parseCodingPluginManifest(root, duplicate, {
|
|
packageRoot: PACKAGE_ROOT,
|
|
capabilityManifestPath: path.join(PACKAGE_ROOT, 'com.makelore/capability.json'),
|
|
})).toThrow('duplicate tool identifier');
|
|
|
|
const escaping = structuredClone(capability) as Record<string, unknown>;
|
|
const skill = (escaping.skills as Array<Record<string, unknown>>)[0];
|
|
skill.entry = '../../outside/SKILL.md';
|
|
expect(() => parseCodingPluginManifest(root, escaping, {
|
|
packageRoot: PACKAGE_ROOT,
|
|
capabilityManifestPath: path.join(PACKAGE_ROOT, 'com.makelore/capability.json'),
|
|
})).toThrow('escapes the package root');
|
|
|
|
const unconfirmed = structuredClone(capability) as Record<string, unknown>;
|
|
const tools = unconfirmed.tools as Array<Record<string, unknown>>;
|
|
const destructive = tools.find((tool) => tool.mutation === 'destructive');
|
|
expect(destructive).toBeDefined();
|
|
const schema = destructive?.inputSchema as Record<string, unknown>;
|
|
const properties = schema.properties as Record<string, unknown>;
|
|
delete properties.confirmed;
|
|
schema.required = (schema.required as string[]).filter((required) => required !== 'confirmed');
|
|
expect(() => parseCodingPluginManifest(root, unconfirmed, {
|
|
packageRoot: PACKAGE_ROOT,
|
|
capabilityManifestPath: path.join(PACKAGE_ROOT, 'com.makelore/capability.json'),
|
|
})).toThrow('confirmed: true');
|
|
});
|
|
|
|
it('does not infer package roots from arbitrary directories', async () => {
|
|
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
|
|
path.resolve('tmp/data-service'),
|
|
path.resolve('tmp/game-resource'),
|
|
path.resolve('tmp/web-search'),
|
|
]);
|
|
});
|
|
|
|
it('loads a single package directly through its exact capability path', async () => {
|
|
await expect(loadCodingPluginDefinition(PACKAGE_ROOT)).resolves.toMatchObject({
|
|
id: 'makelore.data-service',
|
|
tools: expect.arrayContaining([
|
|
expect.objectContaining({ name: 'data_service_configure' }),
|
|
]),
|
|
});
|
|
});
|
|
});
|