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

361 lines
14 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import {
createEffectivePluginResolver,
type EffectivePluginResolver,
} from '../../electron/coding-plugins/effective-resolver';
import {
DATA_SERVICE_PLUGIN_DEFINITION,
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('rejects Marketplace Skill ID collisions with core and other packages without changing project assignment', async () => {
const coreCollision: CodingPluginDefinition = {
...skillOnlyDefinition,
id: 'makelore.core-collision',
releaseId: 'core-collision-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/core-collision-1' },
skills: [{ id: 'agent-browser', entryPath: 'skills/agent-browser/SKILL.md', grants: [] }],
};
const packageA: CodingPluginDefinition = {
...skillOnlyDefinition,
id: 'makelore.collision-a',
releaseId: 'collision-a-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/collision-a-1' },
skills: [{ id: 'shared-skill', entryPath: 'skills/shared/SKILL.md', grants: [] }],
};
const packageB: CodingPluginDefinition = {
...skillOnlyDefinition,
id: 'makelore.collision-b',
releaseId: 'collision-b-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/collision-b-1' },
skills: [{ id: 'shared-skill', entryPath: 'skills/shared/SKILL.md', grants: [] }],
};
const definitions = [coreCollision, packageA, packageB];
const result = await createEffectivePluginResolver({
definitions,
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => ({
items: definitions.map((definition) => ({
pluginId: definition.id,
title: definition.displayName,
summary: definition.description,
category: 'tools',
acquisition: 'free' as const,
acquisitionMode: 'user_acquired' as const,
catalogStatus: 'active' as const,
runtimeStatus: 'enabled' as const,
acquiredAt: null,
removedAt: null,
stableVersion: definition.version,
betaVersion: null,
})),
total: definitions.length,
stale: false,
fetchedAt: 1,
})),
getInstalled: vi.fn(async (pluginId: string) => {
const definition = definitions.find((candidate) => candidate.id === pluginId);
return definition ? installed(definition) : null;
}),
getEnabledPluginIds: vi.fn(async () => definitions.map(({ id }) => id)),
}).resolve({
projectId: 'project-a',
projectPath: 'C:/project-a',
assignedSkillIds: ['agent-browser', 'shared-skill'],
role: 'parent',
});
expect(result.effectiveSkillIds).toEqual(['agent-browser', 'shared-skill']);
expect(result.skillEntries).toEqual([
{ id: 'agent-browser', entryPath: 'agent-browser/SKILL.md' },
{ id: 'shared-skill', entryPath: 'skills/shared/SKILL.md', packageRoot: 'C:/packages/collision-a-1' },
]);
expect(result.pluginReleaseIds).toEqual(['collision-a-1']);
expect(result.unavailableReasons).toEqual([]);
});
it('reserves every bundled Skill owner, including Data Service, before accepting Marketplace packages', async () => {
const marketplaceDataServiceCollision: CodingPluginDefinition = {
...skillOnlyDefinition,
id: 'makelore.data-service-shadow',
releaseId: 'data-service-shadow-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/data-service-shadow-1' },
skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md', grants: [] }],
};
const result = await createEffectivePluginResolver({
definitions: [DATA_SERVICE_PLUGIN_DEFINITION, marketplaceDataServiceCollision],
getAccountBinding: () => binding,
getLibrary: vi.fn(async () => ({
...library(),
items: [{ ...library().items[0], pluginId: marketplaceDataServiceCollision.id }],
})),
getInstalled: vi.fn(async (pluginId: string) => pluginId === marketplaceDataServiceCollision.id
? installed(marketplaceDataServiceCollision)
: null),
getEnabledPluginIds: vi.fn(async () => [
DATA_SERVICE_PLUGIN_DEFINITION.id,
marketplaceDataServiceCollision.id,
]),
}).resolve({
projectId: 'project-a',
projectPath: 'C:/project-a',
assignedSkillIds: ['data-service'],
role: 'parent',
});
expect(result.pluginReleaseIds).toEqual([]);
expect(result.unavailableReasons).not.toEqual([
expect.objectContaining({ pluginId: marketplaceDataServiceCollision.id }),
]);
});
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();
});
});