Files
makelore/tests/unit/plugin-policy-client.test.ts

149 lines
4.9 KiB
TypeScript

// @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<Response>((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<Response>((_resolve, reject) => {
init?.signal?.addEventListener('abort', () => reject(init.signal?.reason), { once: true });
});
}
return Promise.resolve(new Response(new ReadableStream<Uint8Array>({
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();
},
);
});