608 lines
23 KiB
TypeScript
608 lines
23 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,
|
|
GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
|
GAME_RESOURCE_PLUGIN_ID,
|
|
PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
|
PROJECT_SCAFFOLD_PLUGIN_ID,
|
|
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 projectScaffoldDefinition: CodingPluginDefinition = {
|
|
...skillOnlyDefinition,
|
|
id: PROJECT_SCAFFOLD_PLUGIN_ID,
|
|
displayName: 'Project Scaffold',
|
|
description: 'Project-scoped scaffold Skill',
|
|
releaseId: PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
|
provenance: { source: 'bundled', packageRoot: 'project-scaffold' },
|
|
skills: [{
|
|
id: 'makelore-project-scaffold',
|
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
|
grants: [],
|
|
}],
|
|
};
|
|
|
|
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('accepts former local Skill IDs and rejects collisions between Marketplace packages', 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: 'skills/agent-browser/SKILL.md',
|
|
packageRoot: 'C:/packages/core-collision-1',
|
|
},
|
|
{ id: 'shared-skill', entryPath: 'skills/shared/SKILL.md', packageRoot: 'C:/packages/collision-a-1' },
|
|
]);
|
|
expect(result.pluginReleaseIds).toEqual(['core-collision-1', 'collision-a-1']);
|
|
expect(result.unavailableReasons).toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ pluginId: packageB.id, code: 'skill_owner_conflict' }),
|
|
]));
|
|
expect(result.unavailableReasons).not.toEqual(expect.arrayContaining([
|
|
expect.objectContaining({ pluginId: coreCollision.id, code: 'skill_owner_conflict' }),
|
|
]));
|
|
});
|
|
|
|
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).toContainEqual(expect.objectContaining({
|
|
pluginId: marketplaceDataServiceCollision.id,
|
|
code: 'skill_owner_conflict',
|
|
}));
|
|
});
|
|
|
|
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('keeps installed skill-only local under a trusted stale Library while hosted stays fail-closed', async () => {
|
|
const staleNotesLibrary = { ...library(), stale: true };
|
|
await expect(resolve(resolver({ getLibrary: vi.fn(async () => staleNotesLibrary) })))
|
|
.resolves.toMatchObject({
|
|
effectiveSkillIds: ['notes'],
|
|
pluginReleaseIds: ['notes-1'],
|
|
unavailableReasons: [],
|
|
});
|
|
|
|
const staleHostedLibrary = {
|
|
...library(),
|
|
stale: true,
|
|
items: [{ ...library().items[0], pluginId: serverDefinition.id }],
|
|
};
|
|
const hosted = createEffectivePluginResolver({
|
|
definitions: [serverDefinition],
|
|
getAccountBinding: () => binding,
|
|
getLibrary: vi.fn(async () => staleHostedLibrary),
|
|
getInstalled: vi.fn(async () => installed(serverDefinition)),
|
|
getEnabledPluginIds: vi.fn(async () => [serverDefinition.id]),
|
|
policyClient: { getState: currentPolicy, refresh: vi.fn() },
|
|
});
|
|
await expect(hosted.resolve({
|
|
projectId: 'project-a', projectPath: 'C:/project-a', assignedSkillIds: ['remote'], role: 'parent',
|
|
})).resolves.toMatchObject({
|
|
effectiveSkillIds: [],
|
|
pluginReleaseIds: [],
|
|
unavailableReasons: [expect.objectContaining({
|
|
pluginId: serverDefinition.id,
|
|
code: 'library_unavailable',
|
|
})],
|
|
});
|
|
});
|
|
|
|
it('keeps a frozen hosted worker usable through a transient stale Library refresh', async () => {
|
|
const currentHostedLibrary = {
|
|
...library(),
|
|
items: [{ ...library().items[0], pluginId: serverDefinition.id }],
|
|
};
|
|
const getLibrary = vi.fn()
|
|
.mockResolvedValueOnce(currentHostedLibrary)
|
|
.mockResolvedValue({ ...currentHostedLibrary, stale: true });
|
|
const hosted = createEffectivePluginResolver({
|
|
definitions: [serverDefinition],
|
|
getAccountBinding: () => binding,
|
|
getLibrary,
|
|
getInstalled: vi.fn(async () => installed(serverDefinition)),
|
|
getEnabledPluginIds: vi.fn(async () => [serverDefinition.id]),
|
|
policyClient: { getState: currentPolicy, refresh: vi.fn() },
|
|
});
|
|
const input = {
|
|
projectId: 'project-a', projectPath: 'C:/project-a',
|
|
assignedSkillIds: ['remote'], role: 'parent' as const,
|
|
};
|
|
|
|
const frozen = await hosted.resolve(input);
|
|
expect(frozen.toolDefinitions).toEqual([expect.objectContaining({ name: 'remote_read' })]);
|
|
await expect(hosted.resolveForInvocation(input)).resolves.toMatchObject({
|
|
accountSessionId: frozen.accountSessionId,
|
|
pluginReleaseIds: frozen.pluginReleaseIds,
|
|
toolDefinitions: [{ name: 'remote_read' }],
|
|
runtimePolicies: [{ pluginId: serverDefinition.id, operation: 'read' }],
|
|
unavailableReasons: [],
|
|
});
|
|
|
|
await expect(hosted.resolve(input)).resolves.toMatchObject({
|
|
toolDefinitions: [],
|
|
runtimePolicies: [],
|
|
unavailableReasons: [expect.objectContaining({ code: 'library_unavailable' })],
|
|
});
|
|
});
|
|
|
|
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();
|
|
});
|
|
|
|
it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes or Agent assignment', async () => {
|
|
const bundled: CodingPluginDefinition = {
|
|
...serverDefinition,
|
|
id: GAME_RESOURCE_PLUGIN_ID,
|
|
releaseId: GAME_RESOURCE_BUNDLED_RELEASE_ID,
|
|
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
|
skills: [{
|
|
id: 'game-resource',
|
|
entryPath: 'skills/game-resource/SKILL.md',
|
|
grants: ['remote.read'],
|
|
}],
|
|
};
|
|
const getInstalled = vi.fn(async () => null);
|
|
const getInstalledRelease = vi.fn(async () => null);
|
|
const policy = currentPolicy();
|
|
policy.catalog.plugins[0] = { ...policy.catalog.plugins[0], plugin_id: bundled.id };
|
|
const effective = createEffectivePluginResolver({
|
|
definitions: [bundled],
|
|
getAccountBinding: () => binding,
|
|
getLibrary: vi.fn(async () => ({
|
|
...library(),
|
|
items: [{ ...library().items[0], pluginId: bundled.id }],
|
|
})),
|
|
packageStore: {
|
|
readInstalledIndex: vi.fn(async () => []),
|
|
getInstalled,
|
|
getInstalledRelease,
|
|
},
|
|
getEnabledPluginIds: vi.fn(async () => [bundled.id]),
|
|
policyClient: { getState: () => policy, refresh: vi.fn() },
|
|
});
|
|
|
|
await expect(effective.resolve({
|
|
projectId: 'project-a',
|
|
projectPath: 'C:/project-a',
|
|
assignedSkillIds: [],
|
|
role: 'parent',
|
|
})).resolves.toMatchObject({
|
|
pluginReleaseIds: [bundled.releaseId],
|
|
effectiveSkillIds: ['game-resource'],
|
|
skillEntries: [{
|
|
id: 'game-resource',
|
|
entryPath: 'skills/game-resource/SKILL.md',
|
|
}],
|
|
toolDefinitions: [{ name: 'remote_read' }],
|
|
unavailableReasons: [],
|
|
});
|
|
expect(getInstalled).not.toHaveBeenCalled();
|
|
await expect(effective.getSkillSources()).resolves.toEqual([]);
|
|
await expect(effective.getInstalledDefinition(bundled.id, bundled.releaseId))
|
|
.resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId });
|
|
expect(getInstalledRelease).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('materializes the system-included Data Service after project enablement without Agent assignment', async () => {
|
|
const policy = currentPolicy();
|
|
policy.catalog.plugins[0] = {
|
|
...policy.catalog.plugins[0],
|
|
plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id,
|
|
capabilities: DATA_SERVICE_PLUGIN_DEFINITION.operations.map(({ capabilityId, operation }) => ({
|
|
capability_id: capabilityId,
|
|
operations: [{
|
|
operation,
|
|
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' },
|
|
}],
|
|
})),
|
|
};
|
|
const effective = createEffectivePluginResolver({
|
|
definitions: [DATA_SERVICE_PLUGIN_DEFINITION],
|
|
getAccountBinding: () => binding,
|
|
getEnabledPluginIds: vi.fn(async () => [DATA_SERVICE_PLUGIN_DEFINITION.id]),
|
|
policyClient: { getState: () => policy, refresh: vi.fn() },
|
|
});
|
|
|
|
await expect(effective.resolve({
|
|
projectId: 'project-a',
|
|
projectPath: 'C:/project-a',
|
|
assignedSkillIds: [],
|
|
role: 'parent',
|
|
})).resolves.toMatchObject({
|
|
effectiveSkillIds: ['data-service'],
|
|
skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
|
|
unavailableReasons: [],
|
|
});
|
|
});
|
|
|
|
it('materializes the project-wide Scaffold Skill after project enablement without Agent assignment', async () => {
|
|
const scaffoldLibrary = {
|
|
...library(),
|
|
items: [{
|
|
...library().items[0],
|
|
pluginId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
|
title: projectScaffoldDefinition.displayName,
|
|
}],
|
|
};
|
|
const effective = createEffectivePluginResolver({
|
|
definitions: [projectScaffoldDefinition],
|
|
getAccountBinding: () => binding,
|
|
getLibrary: vi.fn(async () => scaffoldLibrary),
|
|
getEnabledPluginIds: vi.fn(async () => [PROJECT_SCAFFOLD_PLUGIN_ID]),
|
|
});
|
|
|
|
await expect(effective.resolve({
|
|
projectId: 'project-a',
|
|
projectPath: 'C:/project-a',
|
|
assignedSkillIds: [],
|
|
role: 'parent',
|
|
})).resolves.toMatchObject({
|
|
pluginReleaseIds: [PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID],
|
|
effectiveSkillIds: ['makelore-project-scaffold'],
|
|
skillEntries: [{
|
|
id: 'makelore-project-scaffold',
|
|
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
|
}],
|
|
unavailableReasons: [],
|
|
});
|
|
|
|
await expect(createEffectivePluginResolver({
|
|
definitions: [projectScaffoldDefinition],
|
|
getAccountBinding: () => binding,
|
|
getLibrary: vi.fn(async () => scaffoldLibrary),
|
|
getEnabledPluginIds: vi.fn(async () => []),
|
|
}).resolve({
|
|
projectId: 'project-a',
|
|
projectPath: 'C:/project-a',
|
|
assignedSkillIds: [],
|
|
role: 'parent',
|
|
})).resolves.toMatchObject({
|
|
effectiveSkillIds: [],
|
|
unavailableReasons: [expect.objectContaining({
|
|
pluginId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
|
code: 'project_disabled',
|
|
})],
|
|
});
|
|
|
|
await expect(effective.resolve({
|
|
projectId: 'project-a',
|
|
projectPath: 'C:/project-a',
|
|
assignedSkillIds: [],
|
|
role: 'child',
|
|
})).resolves.toMatchObject({
|
|
pluginReleaseIds: [],
|
|
effectiveSkillIds: [],
|
|
skillEntries: [],
|
|
});
|
|
});
|
|
});
|