// @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', unknownPluginIds: [], 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' }, tool: { name: 'data_service_get_document', label: 'Data Service get document', description: 'Read a document from the active project.', mutation: 'read', permissions: ['project.data.read'], }, }], }], settingsSurface: 'data-service', })], }); expect(JSON.stringify(result)).not.toMatch(/secret upstream body|projectPath|entitlement_scope/u); }); it('projects retained selected IDs without inventing package metadata', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-unknown-')); roots.push(root); await createCodingProjectMetadata(root, { now: '2026-08-28T00:00:00.000Z' }); const service = createCodingProjectPluginService({ projects: { getProject: vi.fn().mockResolvedValue({ id: 'local-a', path: root }) }, projectPlugins: { getEnabledPluginIds: vi.fn().mockResolvedValue([ 'makelore.removed', DATA_SERVICE_PLUGIN_DEFINITION.id, 'makelore.removed', ]), setEnabled: vi.fn(), }, policyClient: { refresh: vi.fn().mockResolvedValue(undefined), getState: () => ({ status: 'unavailable' as const, revision: 0, lastVerifiedAt: null, catalog: null }), }, adapters: [], definitions: [DATA_SERVICE_PLUGIN_DEFINITION], }); const result = await service.list('local-a'); expect(result.unknownPluginIds).toEqual(['makelore.removed']); expect(result.items).toHaveLength(1); expect(result.items).not.toContainEqual(expect.objectContaining({ id: 'makelore.removed' })); }); it('projects a client-incompatible installed package as unavailable', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-incompatible-')); roots.push(root); await createCodingProjectMetadata(root, { now: '2026-08-28T00:00:00.000Z' }); await createCodingProjectAgent(root, { id: 'builder', avatarId: 'avatar-01', roleName: '实现者', name: 'Builder', model: null, modelResolution: 'required', skillIds: ['notes'], responsibility: { mission: 'Build', owns: [], boundaries: [], collaborators: [], principles: [] }, }); const definition = { ...DATA_SERVICE_PLUGIN_DEFINITION, id: 'makelore.notes', displayName: 'Notes', description: 'Notes', requiresBackend: false, runtimeKind: 'skill_only' as const, acquisitionMode: 'user_acquired' as const, releaseId: 'release-notes', provenance: { source: 'marketplace' as const, packageRoot: 'C:/packages/release-notes' }, adapterId: '', operations: [], tools: [], skills: [{ id: 'notes', entryPath: 'skills/notes/SKILL.md', grants: [] }], surfaces: {}, }; const service = createCodingProjectPluginService({ projects: { getProject: vi.fn().mockResolvedValue({ id: 'local-a', path: root }) }, projectPlugins: { getEnabledPluginIds: vi.fn().mockResolvedValue([definition.id]), setEnabled: vi.fn(), }, policyClient: { refresh: vi.fn(), getState: () => ({ status: 'unavailable' as const, revision: 0, lastVerifiedAt: null, catalog: null }), }, adapters: [], definitions: [definition], effectiveResolver: { resolve: vi.fn().mockResolvedValue({ accountSessionId: 'account-a\u00001', projectId: 'local-a', pluginReleaseIds: [], effectiveSkillIds: [], skillEntries: [], toolDefinitions: [], runtimePolicies: [], unavailableReasons: [{ pluginId: definition.id, code: 'client_incompatible', message: 'Plugin Release is incompatible with this MakeLore client', }], }), } as never, }); await expect(service.list('local-a')).resolves.toMatchObject({ items: [{ id: definition.id, enabled: true, state: 'unavailable' }], }); }); it('projects a Marketplace Skill owner collision as unavailable', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-collision-')); roots.push(root); await createCodingProjectMetadata(root, { now: '2026-08-29T00:00:00.000Z' }); const definition = { ...DATA_SERVICE_PLUGIN_DEFINITION, id: 'makelore.data-service-shadow', displayName: 'Data Service Shadow', description: 'Conflicting package', requiresBackend: false, runtimeKind: 'skill_only' as const, acquisitionMode: 'user_acquired' as const, releaseId: 'release-shadow', provenance: { source: 'marketplace' as const, packageRoot: 'C:/packages/release-shadow' }, adapterId: '', operations: [], tools: [], skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md', grants: [] }], surfaces: {}, }; const service = createCodingProjectPluginService({ projects: { getProject: vi.fn().mockResolvedValue({ id: 'local-a', path: root }) }, projectPlugins: { getEnabledPluginIds: vi.fn().mockResolvedValue([definition.id]), setEnabled: vi.fn(), }, policyClient: { refresh: vi.fn(), getState: () => ({ status: 'unavailable' as const, revision: 0, lastVerifiedAt: null, catalog: null }), }, adapters: [], definitions: [definition], effectiveResolver: { resolve: vi.fn().mockResolvedValue({ accountSessionId: 'account-a\u00001', projectId: 'local-a', pluginReleaseIds: [], effectiveSkillIds: [], skillEntries: [], toolDefinitions: [], runtimePolicies: [], unavailableReasons: [{ pluginId: definition.id, code: 'skill_owner_conflict', message: 'Plugin Skill ID conflicts with an existing owner', }], }), } as never, }); await expect(service.list('local-a')).resolves.toMatchObject({ items: [{ id: definition.id, enabled: true, state: 'unavailable' }], }); }); it('projects the exact three capabilities and fourteen mixed-policy operations', async () => { const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-policy-join-')); roots.push(root); await createCodingProjectMetadata(root, { now: '2026-08-27T00:00:00.000Z' }); const projectPlugins = createProjectPluginService(); await projectPlugins.enable(root, DATA_SERVICE_PLUGIN_DEFINITION.id); const capabilities = ['data-service.control', 'data-service.documents', 'data-service.preview'].map((capabilityId) => ({ capability_id: capabilityId, operations: DATA_SERVICE_PLUGIN_DEFINITION.operations .filter((operation) => operation.capabilityId === capabilityId) .map(({ operation }, index) => ({ operation, billing: capabilityId === 'data-service.documents' ? { mode: 'external_account' as const, notice: 'Provider billed' } : index === 0 && capabilityId === 'data-service.control' ? { mode: 'platform_metered' as const, status: 'billing_unavailable' as const, entitlement_scope: capabilityId, notice: 'Pricing unavailable', } : { 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: { refresh: vi.fn(), 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, }], }, }), }, adapters: [{ pluginId: DATA_SERVICE_PLUGIN_DEFINITION.id, inspect: vi.fn().mockResolvedValue({ status: 'ready' }), invoke: vi.fn(), }], definitions: [DATA_SERVICE_PLUGIN_DEFINITION], }); const item = (await service.list('local-a')).items[0]; expect(item?.capabilities.map(({ id }) => id)).toEqual([ 'data-service.control', 'data-service.documents', 'data-service.preview', ]); expect(item?.capabilities.flatMap(({ operations }) => operations)).toHaveLength(14); expect(item?.capabilities.find(({ id }) => id === 'data-service.preview')?.operations) .toEqual(expect.arrayContaining([ expect.objectContaining({ id: 'get', tool: null, billing: expect.objectContaining({ mode: 'included' }) }), expect.objectContaining({ id: 'delete', tool: null }), ])); expect(item?.capabilities.find(({ id }) => id === 'data-service.documents')?.operations[0]?.billing) .toMatchObject({ mode: 'external_account', availability: 'available' }); expect(item?.capabilities.find(({ id }) => id === 'data-service.control')?.operations[0]?.billing) .toMatchObject({ mode: 'platform_metered', availability: 'unavailable' }); }); 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(); }); });