feat: add configurable MCP tool loading to reduce LLM context usage (#1181)
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
This commit is contained in:
410
tests/unit/mcp/tools/tool-registration.test.js
Normal file
410
tests/unit/mcp/tools/tool-registration.test.js
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* tool-registration.test.js
|
||||
* Comprehensive unit tests for the Task Master MCP tool registration system
|
||||
* Tests environment variable control system covering all configuration modes and edge cases
|
||||
*/
|
||||
|
||||
import {
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
jest
|
||||
} from '@jest/globals';
|
||||
|
||||
import {
|
||||
EXPECTED_TOOL_COUNTS,
|
||||
EXPECTED_CORE_TOOLS,
|
||||
validateToolCounts,
|
||||
validateToolStructure
|
||||
} from '../../../helpers/tool-counts.js';
|
||||
|
||||
import { registerTaskMasterTools } from '../../../../mcp-server/src/tools/index.js';
|
||||
import {
|
||||
toolRegistry,
|
||||
coreTools,
|
||||
standardTools
|
||||
} from '../../../../mcp-server/src/tools/tool-registry.js';
|
||||
|
||||
// Derive constants from imported registry to avoid brittle magic numbers
|
||||
const ALL_COUNT = Object.keys(toolRegistry).length;
|
||||
const CORE_COUNT = coreTools.length;
|
||||
const STANDARD_COUNT = standardTools.length;
|
||||
|
||||
describe('Task Master Tool Registration System', () => {
|
||||
let mockServer;
|
||||
let originalEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = process.env.TASK_MASTER_TOOLS;
|
||||
|
||||
mockServer = {
|
||||
tools: [],
|
||||
addTool: jest.fn((tool) => {
|
||||
mockServer.tools.push(tool);
|
||||
return tool;
|
||||
})
|
||||
};
|
||||
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalEnv !== undefined) {
|
||||
process.env.TASK_MASTER_TOOLS = originalEnv;
|
||||
} else {
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
}
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Test Environment Setup', () => {
|
||||
it('should have properly configured mock server', () => {
|
||||
expect(mockServer).toBeDefined();
|
||||
expect(typeof mockServer.addTool).toBe('function');
|
||||
expect(Array.isArray(mockServer.tools)).toBe(true);
|
||||
expect(mockServer.tools.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should have correct tool registry structure', () => {
|
||||
const validation = validateToolCounts();
|
||||
expect(validation.isValid).toBe(true);
|
||||
|
||||
if (!validation.isValid) {
|
||||
console.error('Tool count validation failed:', validation);
|
||||
}
|
||||
|
||||
expect(validation.actual.total).toBe(EXPECTED_TOOL_COUNTS.total);
|
||||
expect(validation.actual.core).toBe(EXPECTED_TOOL_COUNTS.core);
|
||||
expect(validation.actual.standard).toBe(EXPECTED_TOOL_COUNTS.standard);
|
||||
});
|
||||
|
||||
it('should have correct core tools', () => {
|
||||
const structure = validateToolStructure();
|
||||
expect(structure.isValid).toBe(true);
|
||||
|
||||
if (!structure.isValid) {
|
||||
console.error('Tool structure validation failed:', structure);
|
||||
}
|
||||
|
||||
expect(coreTools).toEqual(expect.arrayContaining(EXPECTED_CORE_TOOLS));
|
||||
expect(coreTools.length).toBe(EXPECTED_TOOL_COUNTS.core);
|
||||
});
|
||||
|
||||
it('should have correct standard tools that include all core tools', () => {
|
||||
const structure = validateToolStructure();
|
||||
expect(structure.details.coreInStandard).toBe(true);
|
||||
expect(standardTools.length).toBe(EXPECTED_TOOL_COUNTS.standard);
|
||||
|
||||
coreTools.forEach((tool) => {
|
||||
expect(standardTools).toContain(tool);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have all expected tools in registry', () => {
|
||||
const expectedTools = [
|
||||
'initialize_project',
|
||||
'models',
|
||||
'research',
|
||||
'add_tag',
|
||||
'delete_tag',
|
||||
'get_tasks',
|
||||
'next_task',
|
||||
'get_task'
|
||||
];
|
||||
expectedTools.forEach((tool) => {
|
||||
expect(toolRegistry).toHaveProperty(tool);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration Modes', () => {
|
||||
it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS is not set (default behavior)`, () => {
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
EXPECTED_TOOL_COUNTS.total
|
||||
);
|
||||
});
|
||||
|
||||
it(`should register all tools (${ALL_COUNT}) when TASK_MASTER_TOOLS=all`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'all';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it(`should register exactly ${CORE_COUNT} core tools when TASK_MASTER_TOOLS=core`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'core';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
EXPECTED_TOOL_COUNTS.core
|
||||
);
|
||||
});
|
||||
|
||||
it(`should register exactly ${STANDARD_COUNT} standard tools when TASK_MASTER_TOOLS=standard`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'standard';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'standard');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
EXPECTED_TOOL_COUNTS.standard
|
||||
);
|
||||
});
|
||||
|
||||
it(`should treat lean as alias for core mode (${CORE_COUNT} tools)`, () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'lean';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'lean');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
|
||||
});
|
||||
|
||||
it('should handle case insensitive configuration values', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'CORE';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'CORE');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom Tool Selection and Edge Cases', () => {
|
||||
it('should register specific tools from comma-separated list', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'get_tasks,next_task,get_task';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'get_tasks,next_task,get_task');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle mixed valid and invalid tool names gracefully', () => {
|
||||
process.env.TASK_MASTER_TOOLS =
|
||||
'invalid_tool,get_tasks,fake_tool,next_task';
|
||||
|
||||
registerTaskMasterTools(
|
||||
mockServer,
|
||||
'invalid_tool,get_tasks,fake_tool,next_task'
|
||||
);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should default to all tools with completely invalid input', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'completely_invalid';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should handle empty string environment variable', () => {
|
||||
process.env.TASK_MASTER_TOOLS = '';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should handle whitespace in comma-separated lists', () => {
|
||||
process.env.TASK_MASTER_TOOLS = ' get_tasks , next_task , get_task ';
|
||||
|
||||
registerTaskMasterTools(mockServer, ' get_tasks , next_task , get_task ');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should ignore duplicate tools in list', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'get_tasks,get_tasks,next_task,get_tasks';
|
||||
|
||||
registerTaskMasterTools(
|
||||
mockServer,
|
||||
'get_tasks,get_tasks,next_task,get_tasks'
|
||||
);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle only commas and empty entries', () => {
|
||||
process.env.TASK_MASTER_TOOLS = ',,,';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should handle single tool selection', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'get_tasks';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'get_tasks');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Coverage Analysis and Integration Tests', () => {
|
||||
it('should provide 100% code coverage for environment control logic', () => {
|
||||
const testCases = [
|
||||
{
|
||||
env: undefined,
|
||||
expectedCount: ALL_COUNT,
|
||||
description: 'undefined env (all)'
|
||||
},
|
||||
{
|
||||
env: '',
|
||||
expectedCount: ALL_COUNT,
|
||||
description: 'empty string (all)'
|
||||
},
|
||||
{ env: 'all', expectedCount: ALL_COUNT, description: 'all mode' },
|
||||
{ env: 'core', expectedCount: CORE_COUNT, description: 'core mode' },
|
||||
{
|
||||
env: 'lean',
|
||||
expectedCount: CORE_COUNT,
|
||||
description: 'lean mode (alias)'
|
||||
},
|
||||
{
|
||||
env: 'standard',
|
||||
expectedCount: STANDARD_COUNT,
|
||||
description: 'standard mode'
|
||||
},
|
||||
{
|
||||
env: 'get_tasks,next_task',
|
||||
expectedCount: 2,
|
||||
description: 'custom list'
|
||||
},
|
||||
{
|
||||
env: 'invalid_tool',
|
||||
expectedCount: ALL_COUNT,
|
||||
description: 'invalid fallback'
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
delete process.env.TASK_MASTER_TOOLS;
|
||||
if (testCase.env !== undefined) {
|
||||
process.env.TASK_MASTER_TOOLS = testCase.env;
|
||||
}
|
||||
|
||||
mockServer.tools = [];
|
||||
mockServer.addTool.mockClear();
|
||||
|
||||
registerTaskMasterTools(mockServer, testCase.env || 'all');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(
|
||||
testCase.expectedCount
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have optimal performance characteristics', () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
process.env.TASK_MASTER_TOOLS = 'all';
|
||||
|
||||
registerTaskMasterTools(mockServer);
|
||||
|
||||
const endTime = Date.now();
|
||||
const executionTime = endTime - startTime;
|
||||
|
||||
expect(executionTime).toBeLessThan(100);
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
|
||||
it('should validate token reduction claims', () => {
|
||||
expect(coreTools.length).toBeLessThan(standardTools.length);
|
||||
expect(standardTools.length).toBeLessThan(
|
||||
Object.keys(toolRegistry).length
|
||||
);
|
||||
|
||||
expect(coreTools.length).toBe(CORE_COUNT);
|
||||
expect(standardTools.length).toBe(STANDARD_COUNT);
|
||||
expect(Object.keys(toolRegistry).length).toBe(ALL_COUNT);
|
||||
|
||||
const allToolsCount = Object.keys(toolRegistry).length;
|
||||
const coreReduction =
|
||||
((allToolsCount - coreTools.length) / allToolsCount) * 100;
|
||||
const standardReduction =
|
||||
((allToolsCount - standardTools.length) / allToolsCount) * 100;
|
||||
|
||||
expect(coreReduction).toBeGreaterThan(80);
|
||||
expect(standardReduction).toBeGreaterThan(50);
|
||||
});
|
||||
|
||||
it('should maintain referential integrity of tool registry', () => {
|
||||
coreTools.forEach((tool) => {
|
||||
expect(standardTools).toContain(tool);
|
||||
});
|
||||
|
||||
standardTools.forEach((tool) => {
|
||||
expect(toolRegistry).toHaveProperty(tool);
|
||||
});
|
||||
|
||||
Object.keys(toolRegistry).forEach((tool) => {
|
||||
expect(typeof toolRegistry[tool]).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle concurrent registration attempts', () => {
|
||||
process.env.TASK_MASTER_TOOLS = 'core';
|
||||
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
registerTaskMasterTools(mockServer, 'core');
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(CORE_COUNT * 3);
|
||||
});
|
||||
|
||||
it('should validate all documented tool categories exist', () => {
|
||||
const allTools = Object.keys(toolRegistry);
|
||||
|
||||
const projectSetupTools = allTools.filter((tool) =>
|
||||
['initialize_project', 'models', 'rules', 'parse_prd'].includes(tool)
|
||||
);
|
||||
expect(projectSetupTools.length).toBeGreaterThan(0);
|
||||
|
||||
const taskManagementTools = allTools.filter((tool) =>
|
||||
['get_tasks', 'get_task', 'next_task', 'set_task_status'].includes(tool)
|
||||
);
|
||||
expect(taskManagementTools.length).toBeGreaterThan(0);
|
||||
|
||||
const analysisTools = allTools.filter((tool) =>
|
||||
['analyze_project_complexity', 'complexity_report'].includes(tool)
|
||||
);
|
||||
expect(analysisTools.length).toBeGreaterThan(0);
|
||||
|
||||
const tagManagementTools = allTools.filter((tool) =>
|
||||
['add_tag', 'delete_tag', 'list_tags', 'use_tag'].includes(tool)
|
||||
);
|
||||
expect(tagManagementTools.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle error conditions gracefully', () => {
|
||||
const problematicInputs = [
|
||||
'null',
|
||||
'undefined',
|
||||
' ',
|
||||
'\n\t',
|
||||
'special!@#$%^&*()characters',
|
||||
'very,very,very,very,very,very,very,long,comma,separated,list,with,invalid,tools,that,should,fallback,to,all'
|
||||
];
|
||||
|
||||
problematicInputs.forEach((input) => {
|
||||
mockServer.tools = [];
|
||||
mockServer.addTool.mockClear();
|
||||
|
||||
process.env.TASK_MASTER_TOOLS = input;
|
||||
|
||||
expect(() => registerTaskMasterTools(mockServer)).not.toThrow();
|
||||
|
||||
expect(mockServer.addTool).toHaveBeenCalledTimes(ALL_COUNT);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user