Files
webmail/lib/__tests__/plugin-assistant-areas.test.ts
T
Linus Rath f7ff8212a3 refactor: give the AI assistant's host APIs one set of files per area
The assistant's new host calls come in four areas (context, actions,
Files, the panel in every view), built side by side. Each area gets its
own files under lib/plugin-sandbox/assistant for its permissions, method
table, dispatcher, host feature flags and sandbox methods; the shared
files only read the merged result. No area adds anything yet. The host
features test now compares against the core flags plus the areas' flags.
2026-11-02 21:21:06 +01:00

60 lines
2.8 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
// The AI assistant's host APIs live in one set of files per area
// (lib/plugin-sandbox/assistant). These tests check that whatever an area
// adds reaches the permission table, the dispatcher, api.host.features and
// the plugin's api, without the shared files naming it.
vi.mock('@/stores/toast-store', () => ({
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() },
}));
const area = vi.hoisted(() => ({
calls: [] as unknown[][],
}));
vi.mock('@/lib/plugin-sandbox/assistant/b', () => ({
PERMS: { 'email.probeArea': 'email:read' },
dispatch: async (_plugin: unknown, method: string, args: unknown[]) => { area.calls.push([method, ...args]); return 'from area b'; },
features: () => ({ probeArea: true }),
}));
vi.mock('@/lib/plugin-sandbox/assistant/b.runtime', () => ({
runtime: (callApi: (m: string, a: unknown[]) => Promise<unknown>) => ({ email: { probeArea: (x: string) => callApi('email.probeArea', [x]) } }),
}));
const { PERM_PER_METHOD, dispatchApiCall } = await import('@/lib/plugin-sandbox/host-api');
const { snapshotHostFeatures } = await import('@/lib/plugin-sandbox/host-features');
const { withAssistantRuntime } = await import('@/lib/plugin-sandbox/assistant/runtime');
const { NOT_MINE, dispatchAssistant } = await import('@/lib/plugin-sandbox/assistant');
const plugin = { id: 'p', name: 'P', permissions: ['email:read'], managed: true } as never;
describe('the assistant areas', () => {
it('put their methods behind their permissions and run them', async () => {
expect(PERM_PER_METHOD['email.probeArea']).toBe('email:read');
expect(await dispatchApiCall(plugin, 'email.probeArea', ['x'])).toBe('from area b');
expect(area.calls).toEqual([['email.probeArea', 'x']]);
// Without the permission the area never sees the call.
const without = { ...(plugin as object), permissions: [] } as never;
await expect(dispatchApiCall(without, 'email.probeArea', [])).rejects.toThrow(/lacks permission "email:read"/);
expect(area.calls).toHaveLength(1);
});
it('leave other methods alone', async () => {
expect(await dispatchAssistant(plugin, 'email.get', [])).toBe(NOT_MINE);
});
it('add their flags to api.host.features', () => {
expect(snapshotHostFeatures()).toMatchObject({ probeArea: true, externalSenderLabel: false });
});
it('add their methods to the plugin api next to the existing ones', () => {
const calls: unknown[][] = [];
const api = withAssistantRuntime({ email: { get: () => 'core' } }, async (method, args) => { calls.push([method, args]); return null; });
expect(api.email.get()).toBe('core');
(api.email as unknown as { probeArea: (x: string) => unknown }).probeArea('y');
expect(calls).toEqual([['email.probeArea', ['y']]]);
});
});