Files
makelore/tests/unit/coding-capability-registry.test.ts

322 lines
13 KiB
TypeScript

// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import type { PluginPolicyClientState } from '../../electron/services/plugin-policy-client';
import {
CodingCapabilityRegistryImpl,
} from '../../electron/coding-plugins/registry';
import type { CodingPluginAdapter } from '../../electron/coding-plugins/registry';
import { createDataServicePluginAdapter } from '../../electron/coding-plugins/adapters/data-service';
import {
createDataServiceOperations,
DataServiceCloudClient,
type DataServiceOperations,
} from '../../electron/services/data-service-client';
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
import { productToolDetailsOfResult } from '../../shared/coding-conversation-product-tool-protocol';
import type { DataServiceErrorContext, DataServiceHostResult } from '../../shared/data-service';
const policy: PluginPolicyClientState = {
status: 'current',
revision: 7,
lastVerifiedAt: 1,
catalog: {
schema_version: 1,
catalog_version: 'catalog-1',
pricing_version: null,
plugins: [{
plugin_id: 'makelore.data-service',
supported_contract_versions: [1],
status: 'active',
capabilities: [
{
capability_id: 'data-service.control',
operations: ['configure', 'inspect', 'list_projects', 'remove_collection', 'reset', 'remove_project']
.map((operation) => ({
operation,
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' },
})),
},
{
capability_id: 'data-service.documents',
operations: ['get_document', 'list_documents', 'put_document', 'delete_document']
.map((operation) => ({
operation,
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' },
})),
},
{
capability_id: 'data-service.preview',
operations: ['get', 'list', 'put', 'delete'].map((operation) => ({
operation,
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included' },
})),
},
],
}],
},
};
const context = {
conversationId: 'conversation-a',
runId: 'run-a',
resourceId: 'resource-a',
projectId: 'local-project-a',
projectPath: 'C:\\projects\\demo',
skillIds: ['data-service'],
};
function adapter(): CodingPluginAdapter {
return {
pluginId: 'makelore.data-service',
async inspect() { return { status: 'ready' }; },
async invoke() {
return {
success: true,
status: 200,
code: null,
error: null,
retryable: false,
payload_schema: 'data-service.v1',
data: { id: 'one', data: {}, revision: 1 },
};
},
};
}
function registry(overrides: Partial<ConstructorParameters<typeof CodingCapabilityRegistryImpl>[0]> = {}) {
return new CodingCapabilityRegistryImpl({
policyClient: { getState: () => policy, refresh: vi.fn().mockResolvedValue(undefined) },
getEnabledPluginIds: async () => ['makelore.data-service'],
adapters: [adapter()],
definitions: [DATA_SERVICE_PLUGIN_DEFINITION],
...overrides,
});
}
describe('CodingCapabilityRegistry', () => {
it('joins package, selection, grant, role, and server policy for parent resources', async () => {
const resources = await registry().resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: context.skillIds,
role: 'parent',
});
expect(resources.catalogRevision).toBe(7);
expect(resources.pluginIds).toEqual(['makelore.data-service']);
expect(resources.effectiveSkillIds).toEqual(['data-service']);
expect(resources.tools.map(({ name }) => name)).toHaveLength(10);
});
it('never exposes plugin resources to child workers or before the first catalog', async () => {
const child = await registry().resolveWorkerResources({
projectPath: context.projectPath, assignedSkillIds: context.skillIds, role: 'child',
});
expect(child.pluginIds).toEqual([]);
expect(child.tools).toEqual([]);
const unavailable = await registry({
policyClient: {
getState: () => ({ ...policy, catalog: null, status: 'unavailable', revision: 0 }),
refresh: vi.fn().mockResolvedValue(undefined),
},
}).resolveWorkerResources({
projectPath: context.projectPath, assignedSkillIds: context.skillIds, role: 'parent',
});
expect(unavailable.tools).toEqual([]);
});
it('does not wait for policy refresh when child or parent core-only resources cannot consume plugins', async () => {
const refresh = vi.fn(() => new Promise<void>(() => {}));
const policyClient = { getState: () => policy, refresh };
const child = await registry({ policyClient }).resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: context.skillIds,
role: 'child',
});
const coreOnly = await registry({ policyClient }).resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: ['grilling'],
role: 'parent',
});
expect(child.tools).toEqual([]);
expect(coreOnly.effectiveSkillIds).toEqual(['grilling']);
expect(refresh).not.toHaveBeenCalled();
});
it('refreshes policy before resolving an assigned server-backed parent plugin', async () => {
const refresh = vi.fn().mockResolvedValue(undefined);
const getEnabledPluginIds = vi.fn().mockResolvedValue(['makelore.data-service']);
await registry({
policyClient: { getState: () => policy, refresh },
getEnabledPluginIds,
}).resolveWorkerResources({
projectPath: context.projectPath,
assignedSkillIds: context.skillIds,
role: 'parent',
});
expect(refresh).toHaveBeenCalledOnce();
expect(refresh.mock.invocationCallOrder[0]).toBeLessThan(getEnabledPluginIds.mock.invocationCallOrder[0]);
});
it('revalidates invocation, derives the stable Pi request id, and preserves domain faults', async () => {
const result = await registry().invoke({
toolName: 'data_service_get_document',
context,
workerRole: 'parent',
effectiveSkillIds: context.skillIds,
value: { collection: 'todos', document_id: 'one' },
});
expect(result.details).toMatchObject({
schema: 'makelore-capability.v1',
plugin_id: 'makelore.data-service',
capability_id: 'data-service.documents',
operation: 'get_document',
request_id: 'pi:run-a:resource-a',
billing: { mode: 'included', status: 'included' },
payload_schema: 'data-service.v1',
});
const invalid = await registry().invoke({
toolName: 'data_service_get_document', context, workerRole: 'parent',
effectiveSkillIds: context.skillIds, value: { collection: 'todos', projectPath: 'forbidden' },
});
expect(invalid.details).toMatchObject({ success: false, code: 'plugin_input_invalid' });
const disabled = await registry({ getEnabledPluginIds: async () => [] }).invoke({
toolName: 'data_service_get_document', context, workerRole: 'parent',
effectiveSkillIds: context.skillIds, value: { collection: 'todos', document_id: 'one' },
});
expect(disabled.details).toMatchObject({ success: false, code: 'plugin_not_enabled' });
const invalidIdentity = await registry().invoke({
toolName: 'data_service_get_document',
context: { ...context, runId: '' },
workerRole: 'parent',
effectiveSkillIds: context.skillIds,
value: { collection: 'todos', document_id: 'one' },
});
expect(invalidIdentity.details).toMatchObject({
success: false, code: 'plugin_input_invalid', request_id: 'invalid-request-id',
});
expect(JSON.stringify(invalidIdentity)).not.toMatch(/[0-9a-f]{8}-[0-9a-f]{4}/i);
});
it('keeps the replay identity and parses bounded Data Service faults for all ten tools', async () => {
const fault = (
code: string,
context?: DataServiceErrorContext,
retryAfter?: number,
): DataServiceHostResult<never> => ({
success: false,
status: code === 'rate_limited' ? 429 : code === 'document_too_large' ? 413 : 409,
code,
error: `Representative ${code} fault`,
retryable: code === 'rate_limited',
...(context ? { context } : {}),
...(retryAfter === undefined ? {} : { retry_after_seconds: retryAfter }),
data: null,
});
const operations: DataServiceOperations = {
configure: vi.fn(async () => fault('quota_exceeded', {
resource: 'collections', limit: 20, current: 20, attempted: 21,
})),
inspect: vi.fn(async () => fault('instance_not_found')),
listProjects: vi.fn(async () => fault('quota_exceeded', {
resource: 'instances', limit: 20, current: 20, attempted: 21,
})),
getDocument: vi.fn(async () => fault('document_not_found')),
listDocuments: vi.fn(async () => fault('rate_limited', undefined, 17)),
putDocument: vi.fn(async () => fault('document_too_large', {
resource: 'bytes', actual: 98_305, limit: 98_304,
})),
deleteDocument: vi.fn(async () => fault('revision_conflict', { current_revision: 7 })),
removeCollection: vi.fn(async () => fault('collection_not_found')),
reset: vi.fn(async () => fault('quota_exceeded', {
resource: 'documents', limit: 5_000, current: 5_000, attempted: 5_001,
})),
removeProject: vi.fn(async () => fault('instance_not_found')),
};
const capabilityRegistry = registry({
adapters: [createDataServicePluginAdapter(operations)],
});
const inputs: Readonly<Record<string, unknown>> = {
data_service_configure: { collections: ['todos'] },
data_service_inspect: {},
data_service_list_projects: {},
data_service_get_document: { collection: 'todos', document_id: 'one' },
data_service_list_documents: { collection: 'todos' },
data_service_put_document: { collection: 'todos', document_id: 'one', data: {} },
data_service_delete_document: { collection: 'todos', document_id: 'one', confirmed: true },
data_service_remove_collection: { collection: 'todos', confirmed: true },
data_service_reset: { confirmed: true },
data_service_remove_project: { confirmed: true },
};
for (const tool of DATA_SERVICE_PLUGIN_DEFINITION.tools) {
const invoke = () => capabilityRegistry.invoke({
toolName: tool.name,
context,
workerRole: 'parent' as const,
effectiveSkillIds: context.skillIds,
value: inputs[tool.name],
});
const first = await invoke();
const replay = await invoke();
expect(first.details.request_id).toBe('pi:run-a:resource-a');
expect(replay.details.request_id).toBe(first.details.request_id);
expect(productToolDetailsOfResult(first)).toEqual(first.details);
}
expect(productToolDetailsOfResult(await capabilityRegistry.invoke({
toolName: 'data_service_put_document', context, workerRole: 'parent',
effectiveSkillIds: context.skillIds, value: inputs.data_service_put_document,
}))).toMatchObject({ context: { resource: 'bytes', actual: 98_305, limit: 98_304 } });
expect(productToolDetailsOfResult(await capabilityRegistry.invoke({
toolName: 'data_service_delete_document', context, workerRole: 'parent',
effectiveSkillIds: context.skillIds, value: inputs.data_service_delete_document,
}))).toMatchObject({ context: { current_revision: 7 } });
expect(productToolDetailsOfResult(await capabilityRegistry.invoke({
toolName: 'data_service_list_documents', context, workerRole: 'parent',
effectiveSkillIds: context.skillIds, value: inputs.data_service_list_documents,
}))).toMatchObject({ retry_after_seconds: 17 });
});
it('keeps the Pi identity through the one authoritative 401 refresh', async () => {
const fetchImpl = vi.fn<typeof fetch>()
.mockResolvedValueOnce(new Response(null, { status: 401 }))
.mockResolvedValueOnce(new Response(JSON.stringify({
id: 'one',
data: { done: false },
revision: 7,
created_at: '2026-08-27T00:00:00Z',
updated_at: '2026-08-27T00:00:00Z',
}), { status: 200, headers: { 'content-type': 'application/json' } }));
const getAccessToken = vi.fn(async (options?: { forceRefresh?: boolean }) => (
options?.forceRefresh ? 'refreshed-token' : 'stale-token'
));
const client = new DataServiceCloudClient({ fetchImpl, getAccessToken });
const operations = createDataServiceOperations({
client,
projects: {
requireActiveRealProjectWithIdentity: vi.fn(async () => ({
projectId: '11111111-1111-4111-8111-111111111111',
})) as never,
},
});
const result = await registry({
adapters: [createDataServicePluginAdapter(operations)],
}).invoke({
toolName: 'data_service_get_document', context, workerRole: 'parent',
effectiveSkillIds: context.skillIds,
value: { collection: 'todos', document_id: 'one' },
});
expect(result.details).toMatchObject({
success: true,
request_id: 'pi:run-a:resource-a',
data: { id: 'one', revision: 7 },
});
expect(fetchImpl).toHaveBeenCalledTimes(2);
expect(getAccessToken).toHaveBeenNthCalledWith(1, { fetchImpl });
expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true });
});
});