Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESFAnh2t9HmLwng8oW95St
205 lines
7.2 KiB
TypeScript
205 lines
7.2 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { SlashCommandPayload } from '@mosaicstack/types';
|
|
import { ChatGateway } from './chat.gateway.js';
|
|
|
|
const payload: SlashCommandPayload = {
|
|
command: 'gc',
|
|
conversationId: 'conversation-1',
|
|
approvalId: 'approval-1',
|
|
};
|
|
|
|
/**
|
|
* Task 5 fence (F, existing control): gateway-owned command authorization/approval must
|
|
* cause ZERO chat-runtime dispatch. Placed in the gateway's chat-runtime-router slot (the
|
|
* former direct `AgentService` slot) so any accidental chat-runtime resolution throws
|
|
* loudly instead of silently passing. Because execute/approval run entirely through the
|
|
* command executor dependency and never resolve a chat runtime, this fixture is never
|
|
* triggered and the ingress stays a GREEN control.
|
|
*/
|
|
function failIfUsedChatRuntimeRouter() {
|
|
return {
|
|
onModuleInit: () => {
|
|
throw new Error('chat runtime router must not initialise on the command approval path');
|
|
},
|
|
get active(): never {
|
|
throw new Error('chat runtime must not be resolved on the command approval path');
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildGateway(commandExecutor: {
|
|
execute: ReturnType<typeof vi.fn>;
|
|
createApproval: ReturnType<typeof vi.fn>;
|
|
}): ChatGateway {
|
|
return new ChatGateway(
|
|
failIfUsedChatRuntimeRouter() as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
commandExecutor as never,
|
|
{} as never,
|
|
);
|
|
}
|
|
|
|
describe('ChatGateway command approval ingress', () => {
|
|
it('passes the client approval ID through to command execution while deriving the actor server-side', async (): Promise<void> => {
|
|
const commandExecutor = {
|
|
execute: vi.fn().mockResolvedValue({ ...payload, success: true }),
|
|
createApproval: vi.fn(),
|
|
};
|
|
const gateway = buildGateway(commandExecutor);
|
|
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
|
|
|
await gateway.handleCommandExecute(client as never, payload);
|
|
|
|
expect(commandExecutor.execute).toHaveBeenCalledWith(payload, {
|
|
userId: 'admin-1',
|
|
tenantId: 'admin-1',
|
|
});
|
|
expect(client.emit).toHaveBeenCalledWith(
|
|
'command:result',
|
|
expect.objectContaining({ success: true }),
|
|
);
|
|
});
|
|
|
|
it('issues a durable approval only for the authenticated actor', async (): Promise<void> => {
|
|
const commandExecutor = {
|
|
execute: vi.fn(),
|
|
createApproval: vi.fn().mockResolvedValue({
|
|
approvalId: 'approval-1',
|
|
expiresAt: '2026-07-12T00:05:00.000Z',
|
|
}),
|
|
};
|
|
const gateway = buildGateway(commandExecutor);
|
|
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
|
|
|
await gateway.handleCommandApproval(client as never, {
|
|
command: 'gc',
|
|
conversationId: 'conversation-1',
|
|
});
|
|
|
|
expect(commandExecutor.createApproval).toHaveBeenCalledWith(
|
|
{ command: 'gc', conversationId: 'conversation-1' },
|
|
{ userId: 'admin-1', tenantId: 'admin-1' },
|
|
);
|
|
expect(client.emit).toHaveBeenCalledWith('command:approval', {
|
|
command: 'gc',
|
|
conversationId: 'conversation-1',
|
|
success: true,
|
|
approvalId: 'approval-1',
|
|
expiresAt: '2026-07-12T00:05:00.000Z',
|
|
});
|
|
});
|
|
});
|
|
|
|
/**
|
|
* Task 5 (G3) command runtime fence. Under pi-rpc there is no embedded chat session, so
|
|
* embedded slash-commands (/model, /agent, and every other non-audited command) are fixed
|
|
* "unsupported" and MUST fail closed BEFORE reaching the command executor — never a silent
|
|
* fall-through to embedded execution. Only runtime-independent audited system commands
|
|
* (/reload) pass through as a positive control, and the approval path stays runtime-independent.
|
|
* The router stub here carries `runtimeMode: 'pi-rpc'` and throws if any runtime is resolved, so
|
|
* a fence bypass surfaces as a thrown error rather than a silent embedded dispatch.
|
|
*/
|
|
function buildPiRpcGateway(commandExecutor: {
|
|
execute: ReturnType<typeof vi.fn>;
|
|
createApproval: ReturnType<typeof vi.fn>;
|
|
}): ChatGateway {
|
|
const piRpcRouter = {
|
|
runtimeMode: 'pi-rpc' as const,
|
|
onModuleInit: () => {
|
|
throw new Error('chat runtime router must not initialise on the pi-rpc command path');
|
|
},
|
|
get active(): never {
|
|
throw new Error('chat runtime must not be resolved on the pi-rpc command path');
|
|
},
|
|
};
|
|
return new ChatGateway(
|
|
piRpcRouter as never,
|
|
{} as never,
|
|
{} as never,
|
|
{} as never,
|
|
commandExecutor as never,
|
|
{} as never,
|
|
);
|
|
}
|
|
|
|
describe('ChatGateway command runtime fence (Task 5 G3, pi-rpc)', () => {
|
|
const UNSUPPORTED = 'Slash commands are not available on this deployment.';
|
|
|
|
it.each(['model', 'agent', 'gc'])(
|
|
'fails /%s closed before the executor under pi-rpc (execute never called)',
|
|
async (command): Promise<void> => {
|
|
const commandExecutor = {
|
|
execute: vi
|
|
.fn()
|
|
.mockResolvedValue({ command, conversationId: 'conversation-1', success: true }),
|
|
createApproval: vi.fn(),
|
|
};
|
|
const gateway = buildPiRpcGateway(commandExecutor);
|
|
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
|
|
|
await gateway.handleCommandExecute(client as never, {
|
|
command,
|
|
conversationId: 'conversation-1',
|
|
});
|
|
|
|
expect(commandExecutor.execute).toHaveBeenCalledTimes(0);
|
|
expect(client.emit).toHaveBeenCalledWith('command:result', {
|
|
command,
|
|
conversationId: 'conversation-1',
|
|
success: false,
|
|
message: UNSUPPORTED,
|
|
});
|
|
},
|
|
);
|
|
|
|
it('passes the audited /reload system command through as a positive control under pi-rpc', async (): Promise<void> => {
|
|
const reloadResult = { command: 'reload', conversationId: 'conversation-1', success: true };
|
|
const commandExecutor = {
|
|
execute: vi.fn().mockResolvedValue(reloadResult),
|
|
createApproval: vi.fn(),
|
|
};
|
|
const gateway = buildPiRpcGateway(commandExecutor);
|
|
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
|
|
|
await gateway.handleCommandExecute(client as never, {
|
|
command: 'reload',
|
|
conversationId: 'conversation-1',
|
|
});
|
|
|
|
expect(commandExecutor.execute).toHaveBeenCalledTimes(1);
|
|
expect(commandExecutor.execute).toHaveBeenCalledWith(
|
|
{ command: 'reload', conversationId: 'conversation-1' },
|
|
{ userId: 'admin-1', tenantId: 'admin-1' },
|
|
);
|
|
expect(client.emit).toHaveBeenCalledWith('command:result', reloadResult);
|
|
});
|
|
|
|
it('keeps command approval runtime-independent under pi-rpc (createApproval still runs)', async (): Promise<void> => {
|
|
const commandExecutor = {
|
|
execute: vi.fn(),
|
|
createApproval: vi.fn().mockResolvedValue({
|
|
approvalId: 'approval-1',
|
|
expiresAt: '2026-07-12T00:05:00.000Z',
|
|
}),
|
|
};
|
|
const gateway = buildPiRpcGateway(commandExecutor);
|
|
const client = { data: { user: { id: 'admin-1' } }, emit: vi.fn() };
|
|
|
|
await gateway.handleCommandApproval(client as never, {
|
|
command: 'gc',
|
|
conversationId: 'conversation-1',
|
|
});
|
|
|
|
expect(commandExecutor.createApproval).toHaveBeenCalledWith(
|
|
{ command: 'gc', conversationId: 'conversation-1' },
|
|
{ userId: 'admin-1', tenantId: 'admin-1' },
|
|
);
|
|
expect(client.emit).toHaveBeenCalledWith(
|
|
'command:approval',
|
|
expect.objectContaining({ success: true, approvalId: 'approval-1' }),
|
|
);
|
|
});
|
|
});
|