Files
makelore/tests/unit/coding-plugin-composition.test.ts

131 lines
5.2 KiB
TypeScript

// @vitest-environment node
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
createCodingProjectPluginService,
} from '../../electron/api/coding-product-services';
import { createCodingProjectMetadata, createCodingProjectAgent } from '../../electron/coding-projects/project-config';
import type { CodingPluginAdapter } from '../../electron/coding-plugins/registry';
import { createProjectPluginService } from '../../electron/coding-plugins/project-service';
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
const roots: string[] = [];
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
describe('coding plugin bounded product service', () => {
it('joins package and policy exactly while isolating adapter inspection failure', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-product-'));
roots.push(root);
await createCodingProjectMetadata(root, {
projectId: '11111111-1111-4111-8111-111111111111',
now: '2026-08-27T00:00:00.000Z',
});
await createCodingProjectAgent(root, {
id: 'builder', avatarId: 'avatar-01', roleName: '实现者', name: 'Builder',
model: null, modelResolution: 'required', skillIds: ['data-service'],
responsibility: { mission: 'Build', owns: [], boundaries: [], collaborators: [], principles: [] },
});
const projectPlugins = createProjectPluginService({ now: () => '2026-08-27T00:00:00.000Z' });
await projectPlugins.enable(root, DATA_SERVICE_PLUGIN_DEFINITION.id);
const adapter: CodingPluginAdapter = {
pluginId: DATA_SERVICE_PLUGIN_DEFINITION.id,
inspect: vi.fn().mockRejectedValue(new Error('secret upstream body')),
invoke: vi.fn(),
};
const policyClient = {
refresh: vi.fn().mockResolvedValue(undefined),
getState: () => ({
status: 'current' as const,
revision: 1,
lastVerifiedAt: 1,
catalog: {
schema_version: 1 as const,
catalog_version: 'catalog-a',
pricing_version: null,
plugins: [{
plugin_id: DATA_SERVICE_PLUGIN_DEFINITION.id,
supported_contract_versions: [1],
status: 'active' as const,
capabilities: [{
capability_id: 'data-service.documents',
operations: [{
operation: 'get_document',
billing: { mode: 'included' as const, entitlement_scope: null, notice: 'Included quota' },
}],
}],
}],
},
}),
};
const service = createCodingProjectPluginService({
projects: {
getProject: vi.fn().mockResolvedValue({ id: 'local-a', path: root }),
},
projectPlugins,
policyClient,
adapters: [adapter],
definitions: [DATA_SERVICE_PLUGIN_DEFINITION],
});
const result = await service.list('local-a');
expect(policyClient.refresh).toHaveBeenCalledOnce();
expect(result).toEqual({
schemaVersion: 1,
project: {
localProjectId: 'local-a',
durableProjectId: '11111111-1111-4111-8111-111111111111',
},
policyStatus: 'current',
items: [expect.objectContaining({
id: DATA_SERVICE_PLUGIN_DEFINITION.id,
enabled: true,
state: 'degraded',
backend: {
status: 'degraded', code: 'plugin_backend_unavailable',
message: 'Plugin backend is temporarily unavailable', retryable: true,
},
skills: [{ id: 'data-service', assignedAgentIds: ['builder'] }],
capabilities: [{
id: 'data-service.documents',
operations: [{
id: 'get_document',
billing: { mode: 'included', availability: 'available', notice: 'Included quota' },
}],
}],
settingsSurface: 'data-service',
})],
});
expect(JSON.stringify(result)).not.toMatch(/secret upstream body|projectPath|entitlement_scope/u);
});
it('deactivates only the requested adapter and isolates cleanup failures', async () => {
const firstDeactivate = vi.fn().mockRejectedValue(new Error('cleanup failed'));
const secondDeactivate = vi.fn().mockResolvedValue(undefined);
const service = createCodingProjectPluginService({
projects: { getProject: vi.fn() },
projectPlugins: { getEnabledPluginIds: vi.fn(), setEnabled: vi.fn() },
policyClient: { refresh: vi.fn(), getState: vi.fn() },
adapters: [
{ pluginId: 'plugin.first', inspect: vi.fn(), invoke: vi.fn(), deactivate: firstDeactivate },
{ pluginId: 'plugin.second', inspect: vi.fn(), invoke: vi.fn(), deactivate: secondDeactivate },
],
definitions: [],
});
await expect(service.deactivate('C:\\project', 'plugin.first')).resolves.toBeUndefined();
expect(firstDeactivate).toHaveBeenCalledWith('C:\\project');
expect(secondDeactivate).not.toHaveBeenCalled();
await expect(service.deactivate('C:\\project')).resolves.toBeUndefined();
expect(firstDeactivate).toHaveBeenCalledTimes(2);
expect(secondDeactivate).toHaveBeenCalledOnce();
});
});