Files
makelore/tests/unit/coding-plugin-effective-resolver.test.ts

257 lines
9.5 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import {
createEffectivePluginResolver,
type EffectivePluginResolver,
} from '../../electron/coding-plugins/effective-resolver';
import type { CodingPluginDefinition } from '../../shared/coding-plugins';
const skillOnlyDefinition: CodingPluginDefinition = {
id: 'makelore.notes',
version: '1.0.0',
contractVersion: 1,
displayName: 'Notes',
description: 'Notes skill',
runtimeKind: 'skill_only',
acquisitionMode: 'user_acquired',
releaseId: 'notes-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/notes-1' },
scope: 'project',
adapterId: '',
requiresBackend: false,
skills: [{ id: 'notes', entryPath: 'skills/notes/SKILL.md', grants: [] }],
tools: [],
operations: [],
surfaces: {},
};
const serverDefinition: CodingPluginDefinition = {
id: 'makelore.remote',
version: '1.0.0',
contractVersion: 1,
displayName: 'Remote',
description: 'Remote capability',
runtimeKind: 'platform_hosted',
acquisitionMode: 'user_acquired',
releaseId: 'remote-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/remote-1' },
scope: 'project',
adapterId: '',
requiresBackend: true,
skills: [{ id: 'remote', entryPath: 'skills/remote/SKILL.md', grants: ['remote.read'] }],
tools: [{
name: 'remote_read', label: 'Remote read', description: 'Read remotely',
capabilityId: 'remote.read', operation: 'read', roles: ['parent'], mutation: 'read',
projectWriteLease: false, permissions: ['remote.read'],
inputSchema: { type: 'object', additionalProperties: false, properties: {} },
}],
operations: [{ capabilityId: 'remote.read', operation: 'read', toolName: 'remote_read' }],
surfaces: {},
};
const binding = { accountKey: 'account-a', epoch: 4 };
function library(runtimeStatus: 'enabled' | 'suspended' = 'enabled', catalogStatus: 'active' | 'retired' = 'active') {
return {
items: [{
pluginId: 'makelore.notes', title: 'Notes', summary: 'Notes', category: 'productivity',
acquisition: 'free' as const, acquisitionMode: 'user_acquired' as const,
catalogStatus, runtimeStatus, acquiredAt: null, removedAt: null,
stableVersion: '1.0.0', betaVersion: null,
}],
total: 1, stale: false, fetchedAt: 1,
};
}
function resolver(overrides: Partial<Parameters<typeof createEffectivePluginResolver>[0]> = {}) {
const effective = createEffectivePluginResolver({
definitions: [skillOnlyDefinition],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => library()),
getInstalled: vi.fn(async () => ({
pluginId: skillOnlyDefinition.id,
releaseId: skillOnlyDefinition.releaseId!,
version: skillOnlyDefinition.version,
packageSchemaVersion: 2,
contractVersion: skillOnlyDefinition.contractVersion,
runtimeKind: 'skill_only' as const,
sha256: 'a'.repeat(64),
sizeBytes: 1,
installedAt: '2026-08-28T00:00:00.000Z',
packageRoot: skillOnlyDefinition.provenance.packageRoot,
definition: skillOnlyDefinition,
})),
getEnabledPluginIds: vi.fn(async () => [skillOnlyDefinition.id]),
...overrides,
});
return effective;
}
function installed(definition: CodingPluginDefinition) {
return {
pluginId: definition.id,
releaseId: definition.releaseId!,
version: definition.version,
packageSchemaVersion: 2,
contractVersion: definition.contractVersion,
runtimeKind: definition.runtimeKind,
sha256: 'b'.repeat(64),
sizeBytes: 1,
installedAt: '2026-08-28T00:00:00.000Z',
packageRoot: definition.provenance.packageRoot,
definition,
};
}
function currentPolicy() {
return {
status: 'current' as const,
revision: 8,
lastVerifiedAt: 1,
catalog: {
schema_version: 1 as const,
catalog_version: 'catalog-a',
pricing_version: null,
plugins: [{
plugin_id: serverDefinition.id,
supported_contract_versions: [1],
status: 'active' as const,
capabilities: [{
capability_id: 'remote.read',
operations: [{
operation: 'read',
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' },
}],
}],
}],
},
};
}
async function resolve(effective: EffectivePluginResolver, assignedSkillIds = ['notes']) {
return await effective.resolve({
projectId: 'project-a',
projectPath: 'C:/project-a',
assignedSkillIds,
role: 'parent',
});
}
describe('effective plugin resolver', () => {
it('requires current Library, installed Release, project selection, and assignment', async () => {
const effective = resolver();
await expect(resolve(effective)).resolves.toMatchObject({
accountSessionId: 'account-a\u00004',
projectId: 'project-a',
pluginReleaseIds: ['notes-1'],
effectiveSkillIds: ['notes'],
skillEntries: [{ id: 'notes', entryPath: 'skills/notes/SKILL.md' }],
toolDefinitions: [],
runtimePolicies: [],
unavailableReasons: [],
});
await expect(resolve(effective, [])).resolves.toMatchObject({
effectiveSkillIds: [],
skillEntries: [],
unavailableReasons: [expect.objectContaining({ pluginId: 'makelore.notes', code: 'skill_unassigned' })],
});
await expect(resolve(effective, ['notes', 'removed-plugin-skill'])).resolves.toMatchObject({
effectiveSkillIds: ['notes'],
unavailableReasons: [],
});
await expect(resolve(resolver({ getEnabledPluginIds: vi.fn(async () => []) }))).resolves.toMatchObject({
effectiveSkillIds: [],
unavailableReasons: [expect.objectContaining({ pluginId: 'makelore.notes', code: 'project_disabled' })],
});
});
it('keeps retired Library plugins usable but excludes suspended runtime plugins', async () => {
await expect(resolve(resolver({ getLibrary: vi.fn(async () => library('enabled', 'retired')) })))
.resolves.toMatchObject({ effectiveSkillIds: ['notes'], pluginReleaseIds: ['notes-1'] });
await expect(resolve(resolver({ getLibrary: vi.fn(async () => library('suspended')) })))
.resolves.toMatchObject({
effectiveSkillIds: [],
pluginReleaseIds: [],
unavailableReasons: [expect.objectContaining({ pluginId: 'makelore.notes', code: 'runtime_suspended' })],
});
});
it('never exposes plugin resources to a child worker', async () => {
const result = await resolver().resolve({
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['notes'], role: 'child',
});
expect(result.pluginReleaseIds).toEqual([]);
expect(result.effectiveSkillIds).toEqual([]);
expect(result.skillEntries).toEqual([]);
expect(result.toolDefinitions).toEqual([]);
});
it('keeps skill-only local while requiring current server policy for tools', async () => {
const refresh = vi.fn();
const effective = createEffectivePluginResolver({
definitions: [serverDefinition],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => ({
...library(),
items: [{ ...library().items[0], pluginId: serverDefinition.id }],
})),
getInstalled: vi.fn(async () => installed(serverDefinition)),
getEnabledPluginIds: vi.fn(async () => [serverDefinition.id]),
policyClient: { getState: currentPolicy, refresh },
});
await expect(effective.resolve({
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['remote'], role: 'parent',
})).resolves.toMatchObject({
pluginReleaseIds: ['remote-1'],
effectiveSkillIds: ['remote'],
skillEntries: [{ id: 'remote', entryPath: 'skills/remote/SKILL.md' }],
toolDefinitions: [{ name: 'remote_read' }],
runtimePolicies: [{
pluginId: serverDefinition.id, contractVersion: 1,
capabilityId: 'remote.read', operation: 'read',
}],
unavailableReasons: [],
});
expect(refresh).not.toHaveBeenCalled();
let policy = { ...currentPolicy(), status: 'stale' as const };
const stalePolicyResolver = createEffectivePluginResolver({
definitions: [serverDefinition],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => ({
...library(),
items: [{ ...library().items[0], pluginId: serverDefinition.id }],
})),
getInstalled: vi.fn(async () => installed(serverDefinition)),
getEnabledPluginIds: vi.fn(async () => [serverDefinition.id]),
policyClient: {
getState: () => policy,
refresh: vi.fn(async () => undefined),
},
});
await expect(stalePolicyResolver.resolve({
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['remote'], role: 'parent',
})).resolves.toMatchObject({
effectiveSkillIds: [], toolDefinitions: [], runtimePolicies: [],
unavailableReasons: [expect.objectContaining({ code: 'policy_unavailable' })],
});
const local = createEffectivePluginResolver({
definitions: [skillOnlyDefinition],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => library()),
getInstalled: vi.fn(async () => installed(skillOnlyDefinition)),
getEnabledPluginIds: vi.fn(async () => [skillOnlyDefinition.id]),
policyClient: { getState: () => ({ ...currentPolicy(), catalog: null }), refresh },
});
await expect(local.resolve({
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['notes'], role: 'parent',
})).resolves.toMatchObject({ effectiveSkillIds: ['notes'], toolDefinitions: [], runtimePolicies: [] });
expect(refresh).not.toHaveBeenCalled();
});
});