mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 21:03:05 -07:00
feat(core): Tool Confirmation Message Bus foundation (PR 1 of 3) (#7835)
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* @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',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user