feat: integrate marketplace plugins with coding runtime
This commit is contained in:
256
tests/unit/coding-plugin-effective-resolver.test.ts
Normal file
256
tests/unit/coding-plugin-effective-resolver.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
// @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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user