Files
TREK/server/tests/unit/plugins/mcp-session-invalidation.test.ts
Maurice 724c88c201 style: format server and shared with prettier
A mechanical prettier --write over server/src, server/tests, server/scripts
and shared/src with the workspaces' .prettierrc; nothing but layout and
import order changes, and every file keeps the same set of imports. shared
is formatted throughout and its format baseline is empty. The server
baseline keeps the 35 files that Prettier would grow past their lint:size
limit (they need a split first, which is not a formatting change), and the
generated entity files stay as their generator writes them.
2026-10-10 21:23:37 +02:00

91 lines
3.3 KiB
TypeScript

/**
* MCP session invalidation on plugin lifecycle changes — MCPINV-001 to MCPINV-007.
*
* The advertised tool surface is frozen at initialize (no event store is
* configured, and tools/list_changed would not help), so anything that changes
* which plugin tools exist has to end the sessions that were built before it.
*
* Unconditional by design: the surface is per-session, because it depends on the
* caller's scopes and the plugin's grants, so there is no ctx-free set to diff.
*/
import type { RuntimeEnvService } from '../../../src/nest/app-config/runtime-env.service';
import { PluginsController } from '../../../src/nest/plugins/plugins.controller';
import { describe, it, expect, beforeEach, vi } from 'vitest';
const invalidate = vi.fn();
vi.mock('../../../src/mcp/sessionManager', () => ({ invalidateMcpSessions: () => invalidate() }));
/* eslint-disable @typescript-eslint/no-explicit-any */
const registry = { recomputeUpdateHold: vi.fn(async () => {}) } as any;
const ctrl = (runtime: any) =>
new PluginsController({} as any, runtime, registry, { isManaged: () => false } as unknown as RuntimeEnvService);
const runtime = () =>
({
activate: vi.fn(async () => {}),
isActive: vi.fn(() => true),
deactivateWithDependents: vi.fn(async () => {}),
update: vi.fn(async () => ({ version: '1.1.0' })),
retrust: vi.fn(async () => ({ ok: true })),
uninstall: vi.fn(async () => {}),
}) as any;
/* eslint-enable @typescript-eslint/no-explicit-any */
beforeEach(() => {
invalidate.mockClear();
process.env.TREK_PLUGINS_ENABLED = 'true';
});
describe('plugin lifecycle invalidates MCP sessions', () => {
it('MCPINV-001: activate', async () => {
await ctrl(runtime()).activate('weather', { consent: true } as never);
expect(invalidate).toHaveBeenCalledTimes(1);
});
it('MCPINV-002: deactivate', async () => {
await ctrl(runtime()).deactivate('weather');
expect(invalidate).toHaveBeenCalledTimes(1);
});
it('MCPINV-003: update', async () => {
// A version bump can change the declared tools, so it is a surface change
// even though the plugin stays active.
await ctrl(runtime()).update('weather', {} as never);
expect(invalidate).toHaveBeenCalledTimes(1);
});
it('MCPINV-004: uninstall', async () => {
await ctrl(runtime()).uninstall('weather', {} as never);
expect(invalidate).toHaveBeenCalledTimes(1);
});
it('MCPINV-005: retrust', async () => {
// Retrust installs a version, so it is an update by another name.
await ctrl(runtime()).retrust('weather', { version: '1.1.0', publicKey: 'k' } as never, { id: 1 }, {
headers: {},
socket: {},
} as never);
expect(invalidate).toHaveBeenCalledTimes(1);
});
it('MCPINV-006: a failed activate does not invalidate', async () => {
// Nothing changed, so nobody's session should be dropped.
const r = runtime();
r.activate = vi.fn(async () => {
throw new Error('boom');
});
await expect(ctrl(r).activate('weather', {} as never)).rejects.toThrow();
expect(invalidate).not.toHaveBeenCalled();
});
it('MCPINV-007: a failed update does not invalidate', async () => {
const r = runtime();
r.update = vi.fn(async () => {
throw new Error('boom');
});
await expect(ctrl(r).update('weather', {} as never)).rejects.toThrow();
expect(invalidate).not.toHaveBeenCalled();
});
});