Files
gemini-cli/packages/core/src/confirmation-bus/message-bus.test.ts

236 lines
7.0 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { MessageBus } from './message-bus.js';
import { PolicyEngine } from '../policy/policy-engine.js';
import { PolicyDecision } from '../policy/types.js';
import {
MessageBusType,
type ToolConfirmationRequest,
type ToolConfirmationResponse,
type ToolPolicyRejection,
type ToolExecutionSuccess,
} from './types.js';
describe('MessageBus', () => {
let messageBus: MessageBus;
let policyEngine: PolicyEngine;
beforeEach(() => {
policyEngine = new PolicyEngine();
messageBus = new MessageBus(policyEngine);
});
describe('publish', () => {
it('should emit error for invalid message', () => {
const errorHandler = vi.fn();
messageBus.on('error', errorHandler);
// @ts-expect-error - Testing invalid message
messageBus.publish({ invalid: 'message' });
expect(errorHandler).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Invalid message structure'),
}),
);
});
it('should validate tool confirmation requests have correlationId', () => {
const errorHandler = vi.fn();
messageBus.on('error', errorHandler);
// @ts-expect-error - Testing missing correlationId
messageBus.publish({
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: { name: 'test' },
});
expect(errorHandler).toHaveBeenCalled();
});
it('should emit confirmation response when policy allows', () => {
vi.spyOn(policyEngine, 'check').mockReturnValue(PolicyDecision.ALLOW);
const responseHandler = vi.fn();
messageBus.subscribe(
MessageBusType.TOOL_CONFIRMATION_RESPONSE,
responseHandler,
);
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: { name: 'test-tool', args: {} },
correlationId: '123',
};
messageBus.publish(request);
const expectedResponse: ToolConfirmationResponse = {
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: '123',
confirmed: true,
};
expect(responseHandler).toHaveBeenCalledWith(expectedResponse);
});
it('should emit rejection and response when policy denies', () => {
vi.spyOn(policyEngine, 'check').mockReturnValue(PolicyDecision.DENY);
const responseHandler = vi.fn();
const rejectionHandler = vi.fn();
messageBus.subscribe(
MessageBusType.TOOL_CONFIRMATION_RESPONSE,
responseHandler,
);
messageBus.subscribe(
MessageBusType.TOOL_POLICY_REJECTION,
rejectionHandler,
);
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: { name: 'test-tool', args: {} },
correlationId: '123',
};
messageBus.publish(request);
const expectedRejection: ToolPolicyRejection = {
type: MessageBusType.TOOL_POLICY_REJECTION,
toolCall: { name: 'test-tool', args: {} },
};
expect(rejectionHandler).toHaveBeenCalledWith(expectedRejection);
const expectedResponse: ToolConfirmationResponse = {
type: MessageBusType.TOOL_CONFIRMATION_RESPONSE,
correlationId: '123',
confirmed: false,
};
expect(responseHandler).toHaveBeenCalledWith(expectedResponse);
});
it('should pass through to UI when policy says ASK_USER', () => {
vi.spyOn(policyEngine, 'check').mockReturnValue(PolicyDecision.ASK_USER);
const requestHandler = vi.fn();
messageBus.subscribe(
MessageBusType.TOOL_CONFIRMATION_REQUEST,
requestHandler,
);
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: { name: 'test-tool', args: {} },
correlationId: '123',
};
messageBus.publish(request);
expect(requestHandler).toHaveBeenCalledWith(request);
});
it('should emit other message types directly', () => {
const successHandler = vi.fn();
messageBus.subscribe(
MessageBusType.TOOL_EXECUTION_SUCCESS,
successHandler,
);
const message: ToolExecutionSuccess<string> = {
type: MessageBusType.TOOL_EXECUTION_SUCCESS as const,
toolCall: { name: 'test-tool' },
result: 'success',
};
messageBus.publish(message);
expect(successHandler).toHaveBeenCalledWith(message);
});
});
describe('subscribe/unsubscribe', () => {
it('should allow subscribing to specific message types', () => {
const handler = vi.fn();
messageBus.subscribe(MessageBusType.TOOL_EXECUTION_SUCCESS, handler);
const message: ToolExecutionSuccess<string> = {
type: MessageBusType.TOOL_EXECUTION_SUCCESS as const,
toolCall: { name: 'test' },
result: 'test',
};
messageBus.publish(message);
expect(handler).toHaveBeenCalledWith(message);
});
it('should allow unsubscribing from message types', () => {
const handler = vi.fn();
messageBus.subscribe(MessageBusType.TOOL_EXECUTION_SUCCESS, handler);
messageBus.unsubscribe(MessageBusType.TOOL_EXECUTION_SUCCESS, handler);
const message: ToolExecutionSuccess<string> = {
type: MessageBusType.TOOL_EXECUTION_SUCCESS as const,
toolCall: { name: 'test' },
result: 'test',
};
messageBus.publish(message);
expect(handler).not.toHaveBeenCalled();
});
it('should support multiple subscribers for the same message type', () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
messageBus.subscribe(MessageBusType.TOOL_EXECUTION_SUCCESS, handler1);
messageBus.subscribe(MessageBusType.TOOL_EXECUTION_SUCCESS, handler2);
const message: ToolExecutionSuccess<string> = {
type: MessageBusType.TOOL_EXECUTION_SUCCESS as const,
toolCall: { name: 'test' },
result: 'test',
};
messageBus.publish(message);
expect(handler1).toHaveBeenCalledWith(message);
expect(handler2).toHaveBeenCalledWith(message);
});
});
describe('error handling', () => {
it('should not crash on errors during message processing', () => {
const errorHandler = vi.fn();
messageBus.on('error', errorHandler);
// Mock policyEngine to throw an error
vi.spyOn(policyEngine, 'check').mockImplementation(() => {
throw new Error('Policy check failed');
});
const request: ToolConfirmationRequest = {
type: MessageBusType.TOOL_CONFIRMATION_REQUEST,
toolCall: { name: 'test-tool' },
correlationId: '123',
};
// Should not throw
expect(() => messageBus.publish(request)).not.toThrow();
// Should emit error
expect(errorHandler).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Policy check failed',
}),
);
});
});
});