// @vitest-environment node import { describe, expect, it, vi } from 'vitest'; import { parsePluginCatalog, PluginPolicyClient, } from '../../electron/services/plugin-policy-client'; const 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.documents', operations: [ { operation: 'get_document', billing: { mode: 'included', entitlement_scope: null, notice: 'Included in development data service', }, }, ], }, ], }], }; describe('PluginPolicyClient', () => { it('parses the exact catalog and rejects unknown or malformed fields', () => { expect(parsePluginCatalog(catalog)).toMatchObject({ schema_version: 1, catalog_version: 'catalog-1', plugins: [{ plugin_id: 'makelore.data-service' }], }); expect(() => parsePluginCatalog({ ...catalog, extra: true })).toThrow('unexpected fields'); expect(() => parsePluginCatalog({ ...catalog, plugins: [{ ...catalog.plugins[0], capabilities: [{ ...catalog.plugins[0].capabilities[0], operations: [{ ...catalog.plugins[0].capabilities[0].operations[0], billing: { mode: 'included', entitlement_scope: 'wrong', notice: 'nope' }, }], }], }], })).toThrow(); }); it('accepts an unavailable platform-metered policy without exposing a price', () => { const parsed = parsePluginCatalog({ ...catalog, plugins: [{ ...catalog.plugins[0], capabilities: [{ ...catalog.plugins[0].capabilities[0], operations: [{ ...catalog.plugins[0].capabilities[0].operations[0], billing: { mode: 'platform_metered', status: 'billing_unavailable', entitlement_scope: 'data-service.documents', notice: 'Pricing is unavailable', }, }], }], }], }); expect(parsed.plugins[0]?.capabilities[0]?.operations[0]?.billing).toEqual({ mode: 'platform_metered', status: 'billing_unavailable', entitlement_scope: 'data-service.documents', notice: 'Pricing is unavailable', }); }); it('coalesces concurrent refreshes and marks the verified catalog stale after failure', async () => { let resolveRequest: ((response: Response) => void) | undefined; const fetchImpl = vi.fn(() => new Promise((resolve) => { resolveRequest = resolve; })); const client = new PluginPolicyClient({ fetchImpl, apiBaseUrl: 'https://works.example', now: () => 123, }); const first = client.refresh(); const second = client.refresh(); expect(first).toBe(second); expect(fetchImpl).toHaveBeenCalledTimes(1); resolveRequest?.(new Response(JSON.stringify(catalog), { status: 200, headers: { 'content-type': 'application/json' }, })); await expect(first).resolves.toMatchObject({ status: 'current', revision: 1 }); fetchImpl.mockRejectedValueOnce(new Error('offline')); await expect(client.refresh()).resolves.toMatchObject({ status: 'stale', revision: 1, errorCode: 'plugin_backend_unavailable', }); expect(client.getState().catalog).toEqual(expect.objectContaining({ catalog_version: 'catalog-1' })); }); it('keeps an unavailable first state when no catalog was verified', async () => { const client = new PluginPolicyClient({ fetchImpl: vi.fn().mockRejectedValue(new Error('offline')) }); await expect(client.refresh()).resolves.toMatchObject({ status: 'unavailable', catalog: null, revision: 0, errorCode: 'plugin_backend_unavailable', }); }); it.each(['headers', 'body'] as const)( 'bounds a catalog request whose %s never settle', async (phase) => { const fetchImpl = vi.fn((_input: string | URL, init?: RequestInit) => { if (phase === 'headers') { return new Promise((_resolve, reject) => { init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true }); }); } return Promise.resolve(new Response(new ReadableStream({ start(controller) { init?.signal?.addEventListener('abort', () => controller.error(init.signal?.reason), { once: true }); }, }), { status: 200, headers: { 'content-type': 'application/json' } })); }); const client = new PluginPolicyClient({ fetchImpl, requestTimeoutMs: 20 }); await expect(client.refresh()).resolves.toMatchObject({ status: 'unavailable', catalog: null, revision: 0, errorCode: 'plugin_backend_unavailable', }); expect(fetchImpl).toHaveBeenCalledOnce(); }, ); });