feat(core): restore MessageBus optionality for soft migration (Phase 1) (#15774)

This commit is contained in:
Abhi
2026-01-04 14:59:35 -05:00
committed by GitHub
parent d3c206c677
commit eec5d5ebf8
16 changed files with 105 additions and 82 deletions

View File

@@ -59,6 +59,10 @@ import { ApprovalMode } from '../policy/types.js';
import type { Content, Part, SchemaUnion } from '@google/genai';
import { StandardFileSystemService } from '../services/fileSystemService.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import {
createMockMessageBus,
getMockMessageBusInstance,
} from '../test-utils/mock-message-bus.js';
describe('EditTool', () => {
let tool: EditTool;
@@ -177,7 +181,9 @@ describe('EditTool', () => {
},
);
tool = new EditTool(mockConfig);
const bus = createMockMessageBus();
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
tool = new EditTool(mockConfig, bus);
});
afterEach(() => {
@@ -1029,7 +1035,10 @@ describe('EditTool', () => {
it('should use windows-style path examples on windows', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32');
const tool = new EditTool({} as unknown as Config);
const tool = new EditTool(
{} as unknown as Config,
createMockMessageBus(),
);
const schema = tool.schema;
expect(
(schema.parametersJsonSchema as EditFileParameterSchema).properties
@@ -1040,7 +1049,10 @@ describe('EditTool', () => {
it('should use unix-style path examples on non-windows platforms', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('linux');
const tool = new EditTool({} as unknown as Config);
const tool = new EditTool(
{} as unknown as Config,
createMockMessageBus(),
);
const schema = tool.schema;
expect(
(schema.parametersJsonSchema as EditFileParameterSchema).properties

View File

@@ -16,6 +16,7 @@ import type { Config } from '../config/config.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { ToolErrorType } from './tool-error.js';
import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('glob', { spy: true });
@@ -43,7 +44,7 @@ describe('GlobTool', () => {
// Create a unique root directory for each test run
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'glob-tool-root-'));
await fs.writeFile(path.join(tempRootDir, '.git'), ''); // Fake git repo
globTool = new GlobTool(mockConfig);
globTool = new GlobTool(mockConfig, createMockMessageBus());
// Create some test files and directories within this root
// Top-level files

View File

@@ -14,6 +14,7 @@ import type { Config } from '../config/config.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { ToolErrorType } from './tool-error.js';
import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('glob', { spy: true });
@@ -47,7 +48,7 @@ describe('GrepTool', () => {
beforeEach(async () => {
tempRootDir = await fs.mkdtemp(path.join(os.tmpdir(), 'grep-tool-root-'));
grepTool = new GrepTool(mockConfig);
grepTool = new GrepTool(mockConfig, createMockMessageBus());
// Create some test files and directories
await fs.writeFile(
@@ -270,7 +271,10 @@ describe('GrepTool', () => {
}),
} as unknown as Config;
const multiDirGrepTool = new GrepTool(multiDirConfig);
const multiDirGrepTool = new GrepTool(
multiDirConfig,
createMockMessageBus(),
);
const params: GrepToolParams = { pattern: 'world' };
const invocation = multiDirGrepTool.build(params);
const result = await invocation.execute(abortSignal);
@@ -323,7 +327,10 @@ describe('GrepTool', () => {
}),
} as unknown as Config;
const multiDirGrepTool = new GrepTool(multiDirConfig);
const multiDirGrepTool = new GrepTool(
multiDirConfig,
createMockMessageBus(),
);
// Search only in the 'sub' directory of the first workspace
const params: GrepToolParams = { pattern: 'world', dir_path: 'sub' };
@@ -385,7 +392,10 @@ describe('GrepTool', () => {
}),
} as unknown as Config;
const multiDirGrepTool = new GrepTool(multiDirConfig);
const multiDirGrepTool = new GrepTool(
multiDirConfig,
createMockMessageBus(),
);
const params: GrepToolParams = { pattern: 'testPattern' };
const invocation = multiDirGrepTool.build(params);
expect(invocation.getDescription()).toBe(

View File

@@ -13,6 +13,7 @@ import type { Config } from '../config/config.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { ToolErrorType } from './tool-error.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
describe('LSTool', () => {
let lsTool: LSTool;
@@ -39,7 +40,7 @@ describe('LSTool', () => {
}),
} as unknown as Config;
lsTool = new LSTool(mockConfig);
lsTool = new LSTool(mockConfig, createMockMessageBus());
});
afterEach(async () => {

View File

@@ -56,22 +56,19 @@ class TestToolInvocation extends BaseToolInvocation<TestParams, TestResult> {
override async shouldConfirmExecute(
abortSignal: AbortSignal,
): Promise<false> {
// This conditional is here to allow testing of the case where there is no message bus.
if (this.messageBus) {
const decision = await this.getMessageBusDecision(abortSignal);
if (decision === 'ALLOW') {
return false;
}
if (decision === 'DENY') {
throw new Error('Tool execution denied by policy');
}
const decision = await this.getMessageBusDecision(abortSignal);
if (decision === 'ALLOW') {
return false;
}
if (decision === 'DENY') {
throw new Error('Tool execution denied by policy');
}
return false;
}
}
class TestTool extends BaseDeclarativeTool<TestParams, TestResult> {
constructor(messageBus?: MessageBus) {
constructor(messageBus: MessageBus) {
super(
'test-tool',
'Test Tool',
@@ -90,7 +87,7 @@ class TestTool extends BaseDeclarativeTool<TestParams, TestResult> {
);
}
protected createInvocation(params: TestParams, messageBus?: MessageBus) {
protected createInvocation(params: TestParams, messageBus: MessageBus) {
return new TestToolInvocation(params, messageBus);
}
}
@@ -220,16 +217,6 @@ describe('Message Bus Integration', () => {
);
});
it('should fall back to default behavior when no message bus', async () => {
const tool = new TestTool(); // No message bus
const invocation = tool.build({ testParam: 'test-value' });
const result = await invocation.shouldConfirmExecute(
new AbortController().signal,
);
expect(result).toBe(false);
});
it('should ignore responses with wrong correlation ID', async () => {
vi.useFakeTimers();
@@ -260,28 +247,6 @@ describe('Message Bus Integration', () => {
});
});
describe('Backward Compatibility', () => {
it('should work with existing tools that do not use message bus', async () => {
const tool = new TestTool(); // No message bus
const invocation = tool.build({ testParam: 'test-value' });
// Should execute normally
const result = await invocation.execute(new AbortController().signal);
expect(result.testValue).toBe('test-value');
expect(result.llmContent).toBe('Executed with test-value');
});
it('should work with tools that have message bus but use default confirmation', async () => {
const tool = new TestTool(messageBus);
const invocation = tool.build({ testParam: 'test-value' });
// Should execute normally even with message bus available
const result = await invocation.execute(new AbortController().signal);
expect(result.testValue).toBe('test-value');
expect(result.llmContent).toBe('Executed with test-value');
});
});
describe('Error Handling', () => {
it('should handle message bus publish errors gracefully', async () => {
const tool = new TestTool(messageBus);

View File

@@ -17,6 +17,7 @@ import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { StandardFileSystemService } from '../services/fileSystemService.js';
import { createMockWorkspaceContext } from '../test-utils/mockWorkspaceContext.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('../telemetry/loggers.js', () => ({
logFileOperation: vi.fn(),
@@ -46,7 +47,7 @@ describe('ReadFileTool', () => {
},
isInteractive: () => false,
} as unknown as Config;
tool = new ReadFileTool(mockConfigInstance);
tool = new ReadFileTool(mockConfigInstance, createMockMessageBus());
});
afterEach(async () => {
@@ -438,7 +439,7 @@ describe('ReadFileTool', () => {
getProjectTempDir: () => path.join(tempRootDir, '.temp'),
},
} as unknown as Config;
tool = new ReadFileTool(mockConfigInstance);
tool = new ReadFileTool(mockConfigInstance, createMockMessageBus());
});
it('should throw error if path is ignored by a .geminiignore pattern', async () => {

View File

@@ -21,6 +21,7 @@ import {
DEFAULT_FILE_EXCLUDES,
} from '../utils/ignorePatterns.js';
import * as glob from 'glob';
import { createMockMessageBus } from '../test-utils/mock-message-bus.js';
vi.mock('glob', { spy: true });
@@ -90,7 +91,7 @@ describe('ReadManyFilesTool', () => {
}),
isInteractive: () => false,
} as Partial<Config> as Config;
tool = new ReadManyFilesTool(mockConfig);
tool = new ReadManyFilesTool(mockConfig, createMockMessageBus());
mockReadFileFn = mockControl.mockReadFile;
mockReadFileFn.mockReset();
@@ -505,7 +506,7 @@ describe('ReadManyFilesTool', () => {
}),
isInteractive: () => false,
} as Partial<Config> as Config;
tool = new ReadManyFilesTool(mockConfig);
tool = new ReadManyFilesTool(mockConfig, createMockMessageBus());
fs.writeFileSync(path.join(tempDir1, 'file1.txt'), 'Content1');
fs.writeFileSync(path.join(tempDir2, 'file2.txt'), 'Content2');

View File

@@ -172,7 +172,7 @@ Signal: Signal number or \`(none)\` if no signal was received.
protected createInvocation(
params: ToolParams,
_messageBus?: MessageBus,
messageBus?: MessageBus,
_toolName?: string,
_displayName?: string,
): ToolInvocation<ToolParams, ToolResult> {
@@ -181,7 +181,7 @@ Signal: Signal number or \`(none)\` if no signal was received.
this.originalName,
this.name,
params,
_messageBus,
messageBus,
);
}
}
@@ -194,11 +194,8 @@ export class ToolRegistry {
private config: Config;
private messageBus?: MessageBus;
constructor(config: Config) {
constructor(config: Config, messageBus?: MessageBus) {
this.config = config;
}
setMessageBus(messageBus: MessageBus): void {
this.messageBus = messageBus;
}
@@ -206,6 +203,15 @@ export class ToolRegistry {
return this.messageBus;
}
/**
* @deprecated migration only - will be removed in PR 3 (Enforcement)
* TODO: DELETE ME in PR 3. This is a temporary shim to allow for soft migration
* of tools while the core infrastructure is updated to require a MessageBus at birth.
*/
setMessageBus(messageBus: MessageBus): void {
this.messageBus = messageBus;
}
/**
* Registers a tool definition.
*

View File

@@ -41,6 +41,10 @@ import { StandardFileSystemService } from '../services/fileSystemService.js';
import type { DiffUpdateResult } from '../ide/ide-client.js';
import { IdeClient } from '../ide/ide-client.js';
import { WorkspaceContext } from '../utils/workspaceContext.js';
import {
createMockMessageBus,
getMockMessageBusInstance,
} from '../test-utils/mock-message-bus.js';
const rootDir = path.resolve(os.tmpdir(), 'gemini-cli-test-root');
@@ -150,7 +154,9 @@ describe('WriteFileTool', () => {
mockBaseLlmClientInstance,
);
tool = new WriteFileTool(mockConfig);
const bus = createMockMessageBus();
getMockMessageBusInstance(bus).defaultToolDecision = 'ask_user';
tool = new WriteFileTool(mockConfig, bus);
// Reset mocks before each test
mockConfigInternal.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT);

View File

@@ -475,11 +475,12 @@ export class WriteFileTool
protected createInvocation(
params: WriteFileToolParams,
messageBus?: MessageBus,
): ToolInvocation<WriteFileToolParams, ToolResult> {
return new WriteFileToolInvocation(
this.config,
params,
this.messageBus,
messageBus ?? this.messageBus,
this.name,
this.displayName,
);