import { CartridgeToolHandler } from '../src/core/handlers/cartridge-handler.js';
import { HandlerContext } from '../src/core/handlers/base-handler.js';
import { Logger } from '../src/utils/logger.js';
import { join, parse, resolve } from 'path';
// Mock cartridge client
const mockCartridgeClient = {
generateCartridgeStructure: jest.fn(),
};
// Mock the ClientFactory to return our mock client
jest.mock('../src/core/handlers/client-factory.js', () => ({
ClientFactory: jest.fn().mockImplementation(() => ({
createCartridgeClient: jest.fn(() => mockCartridgeClient),
})),
}));
describe('CartridgeToolHandler', () => {
let mockLogger: jest.Mocked<Logger>;
let mockClient: typeof mockCartridgeClient;
let context: HandlerContext;
let handler: CartridgeToolHandler;
const workspaceScopedPath = join(process.cwd(), 'tmp', 'handler-tests');
const outsideWorkspacePath = join(parse(process.cwd()).root, 'tmp', 'outside-workspace');
const getResultText = (result: { content: Array<{ text: string }>; structuredContent?: unknown }): string => {
const text = result.content[0]?.text;
if (typeof text === 'string') {
return text;
}
return JSON.stringify(result.structuredContent ?? {});
};
beforeEach(() => {
mockLogger = {
debug: jest.fn(),
log: jest.fn(),
error: jest.fn(),
timing: jest.fn(),
methodEntry: jest.fn(),
methodExit: jest.fn(),
} as any;
// Reset mocks
jest.clearAllMocks();
// Use the mock client directly and reset it
mockClient = mockCartridgeClient;
mockClient.generateCartridgeStructure.mockReset();
jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger);
context = {
logger: mockLogger,
config: null as any,
capabilities: { canAccessLogs: false, canAccessOCAPI: false },
};
handler = new CartridgeToolHandler(context, 'Cartridge');
});
afterEach(() => {
jest.restoreAllMocks();
});
// Helper function to initialize handler for tests that need it
const initializeHandler = async () => {
await (handler as any).initialize();
};
describe('canHandle', () => {
it('should handle cartridge tools', () => {
expect(handler.canHandle('generate_cartridge_structure')).toBe(true);
});
it('should not handle non-cartridge tools', () => {
expect(handler.canHandle('get_latest_error')).toBe(false);
expect(handler.canHandle('unknown_tool')).toBe(false);
});
});
describe('initialization', () => {
it('should initialize cartridge generation client', async () => {
await initializeHandler();
const MockedClientFactory = jest.requireMock('../src/core/handlers/client-factory.js').ClientFactory;
const mockFactoryInstance = MockedClientFactory.mock.results[0].value;
expect(mockFactoryInstance.createCartridgeClient).toHaveBeenCalled();
expect(mockLogger.debug).toHaveBeenCalledWith('Cartridge Generation client initialized');
});
});
describe('disposal', () => {
it('should dispose cartridge generation client properly', async () => {
await initializeHandler();
await (handler as any).dispose();
expect(mockLogger.debug).toHaveBeenCalledWith('Cartridge Generation client disposed');
});
});
describe('generate_cartridge_structure tool', () => {
beforeEach(async () => {
await initializeHandler();
});
it('should handle generate_cartridge_structure with cartridgeName', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'plugin_example',
targetPath: '/path/to/project',
filesCreated: [
'cartridges/plugin_example/cartridge/controllers/Product.js',
'cartridges/plugin_example/cartridge/scripts/helpers/ProductHelper.js',
'package.json',
],
});
const args = { cartridgeName: 'plugin_example' };
const result = await handler.handle('generate_cartridge_structure', args, Date.now());
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: 'plugin_example',
targetPath: undefined,
fullProjectSetup: true,
});
expect(getResultText(result)).toContain('plugin_example');
});
it('should handle generate_cartridge_structure with all options', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'custom_cartridge',
targetPath: '/custom/path',
filesCreated: [
'cartridges/custom_cartridge/cartridge/controllers/Product.js',
],
});
const args = {
cartridgeName: 'custom_cartridge',
targetPath: workspaceScopedPath,
fullProjectSetup: false,
};
const result = await handler.handle('generate_cartridge_structure', args, Date.now());
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: 'custom_cartridge',
targetPath: workspaceScopedPath,
fullProjectSetup: false,
});
expect(getResultText(result)).toContain('custom_cartridge');
});
it('should use default fullProjectSetup when not provided', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'test_cartridge',
filesCreated: [],
});
const args = {
cartridgeName: 'test_cartridge',
targetPath: workspaceScopedPath,
};
await handler.handle('generate_cartridge_structure', args, Date.now());
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: 'test_cartridge',
targetPath: workspaceScopedPath,
fullProjectSetup: true,
});
});
it('should not enforce missing cartridgeName in handler (validated at MCP boundary)', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'fallback',
filesCreated: [],
});
const result = await handler.handle('generate_cartridge_structure', {}, Date.now());
expect(result.isError).toBe(false);
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: undefined,
targetPath: undefined,
fullProjectSetup: true,
});
});
it('should not enforce empty cartridgeName in handler (validated at MCP boundary)', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'fallback',
filesCreated: [],
});
const result = await handler.handle('generate_cartridge_structure', { cartridgeName: '' }, Date.now());
expect(result.isError).toBe(false);
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: '',
targetPath: undefined,
fullProjectSetup: true,
});
});
it('should not enforce cartridgeName type in handler (validated at MCP boundary)', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'fallback',
filesCreated: [],
});
const result = await handler.handle('generate_cartridge_structure', { cartridgeName: 123 }, Date.now());
expect(result.isError).toBe(false);
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: 123,
targetPath: undefined,
fullProjectSetup: true,
});
});
});
describe('error handling', () => {
beforeEach(async () => {
await initializeHandler();
});
it('should handle client errors gracefully', async () => {
mockClient.generateCartridgeStructure.mockRejectedValue(new Error('Directory already exists'));
const result = await handler.handle('generate_cartridge_structure', { cartridgeName: 'existing_cartridge' }, Date.now());
expect(result.isError).toBe(true);
expect(getResultText(result)).toContain('Directory already exists');
});
it('should throw error for unsupported tools', async () => {
await expect(handler.handle('unsupported_tool', {}, Date.now()))
.rejects.toThrow('Unsupported tool');
});
it('should reject targetPath outside workspace boundaries', async () => {
const result = await handler.handle(
'generate_cartridge_structure',
{
cartridgeName: 'outside_workspace',
targetPath: outsideWorkspacePath,
},
Date.now(),
);
expect(result.isError).toBe(true);
expect(getResultText(result)).toContain('Path must be within workspace roots or current working directory');
expect(mockClient.generateCartridgeStructure).not.toHaveBeenCalled();
});
});
describe('timing and logging', () => {
beforeEach(async () => {
await initializeHandler();
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'test_cartridge',
filesCreated: [],
});
});
it('should log timing information', async () => {
const startTime = Date.now();
await handler.handle('generate_cartridge_structure', { cartridgeName: 'test_cartridge' }, startTime);
expect(mockLogger.timing).toHaveBeenCalledWith('generate_cartridge_structure', startTime);
});
it('should log execution details', async () => {
await handler.handle('generate_cartridge_structure', { cartridgeName: 'test_cartridge' }, Date.now());
expect(mockLogger.debug).toHaveBeenCalledWith(
'generate_cartridge_structure completed',
expect.any(Object),
);
});
it('should create appropriate log message', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'my_cartridge',
filesCreated: [],
});
await handler.handle('generate_cartridge_structure', { cartridgeName: 'my_cartridge' }, Date.now());
// Check that the debug log contains the execution details
expect(mockLogger.debug).toHaveBeenCalledWith(
'generate_cartridge_structure completed',
expect.any(Object),
);
});
});
describe('client integration', () => {
beforeEach(async () => {
await initializeHandler();
});
it('should pass correct parameters to client for minimal request', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'minimal_cartridge',
filesCreated: ['cartridges/minimal_cartridge/cartridge/controllers/Default.js'],
});
const args = { cartridgeName: 'minimal_cartridge' };
await handler.handle('generate_cartridge_structure', args, Date.now());
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: 'minimal_cartridge',
targetPath: undefined,
fullProjectSetup: true,
});
});
it('should pass correct parameters to client for full request', async () => {
mockClient.generateCartridgeStructure.mockResolvedValue({
success: true,
cartridgeName: 'full_cartridge',
filesCreated: ['cartridges/full_cartridge/cartridge/controllers/Default.js'],
});
const args = {
cartridgeName: 'full_cartridge',
targetPath: resolve(workspaceScopedPath, 'full-cartridge'),
fullProjectSetup: false,
};
await handler.handle('generate_cartridge_structure', args, Date.now());
expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({
cartridgeName: 'full_cartridge',
targetPath: resolve(workspaceScopedPath, 'full-cartridge'),
fullProjectSetup: false,
});
});
});
});