mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2025-11-21 04:27:34 +00:00
664 lines
16 KiB
JavaScript
664 lines
16 KiB
JavaScript
|
|
import { jest } from '@jest/globals';
|
||
|
|
|
||
|
|
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
|
||
|
|
findProjectRoot: jest.fn(() => '/test/project/root'),
|
||
|
|
log: jest.fn(),
|
||
|
|
readJSON: jest.fn(),
|
||
|
|
flattenTasksWithSubtasks: jest.fn(() => []),
|
||
|
|
isEmpty: jest.fn(() => false)
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Mock UI-affecting external libs to minimal no-op implementations
|
||
|
|
jest.unstable_mockModule('chalk', () => ({
|
||
|
|
default: {
|
||
|
|
white: Object.assign(
|
||
|
|
jest.fn((text) => text),
|
||
|
|
{
|
||
|
|
bold: jest.fn((text) => text)
|
||
|
|
}
|
||
|
|
),
|
||
|
|
cyan: Object.assign(
|
||
|
|
jest.fn((text) => text),
|
||
|
|
{
|
||
|
|
bold: jest.fn((text) => text)
|
||
|
|
}
|
||
|
|
),
|
||
|
|
green: Object.assign(
|
||
|
|
jest.fn((text) => text),
|
||
|
|
{
|
||
|
|
bold: jest.fn((text) => text)
|
||
|
|
}
|
||
|
|
),
|
||
|
|
yellow: jest.fn((text) => text),
|
||
|
|
red: jest.fn((text) => text),
|
||
|
|
gray: jest.fn((text) => text),
|
||
|
|
blue: Object.assign(
|
||
|
|
jest.fn((text) => text),
|
||
|
|
{
|
||
|
|
bold: jest.fn((text) => text)
|
||
|
|
}
|
||
|
|
),
|
||
|
|
bold: jest.fn((text) => text)
|
||
|
|
}
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.unstable_mockModule('boxen', () => ({ default: (text) => text }));
|
||
|
|
|
||
|
|
jest.unstable_mockModule('inquirer', () => ({
|
||
|
|
default: { prompt: jest.fn() }
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.unstable_mockModule('cli-highlight', () => ({
|
||
|
|
highlight: (code) => code
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.unstable_mockModule('cli-table3', () => ({
|
||
|
|
default: jest.fn().mockImplementation(() => ({
|
||
|
|
push: jest.fn(),
|
||
|
|
toString: jest.fn(() => '')
|
||
|
|
}))
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.unstable_mockModule(
|
||
|
|
'../../../../../scripts/modules/utils/contextGatherer.js',
|
||
|
|
() => ({
|
||
|
|
ContextGatherer: jest.fn().mockImplementation(() => ({
|
||
|
|
gather: jest.fn().mockResolvedValue({
|
||
|
|
context: 'Gathered context',
|
||
|
|
tokenBreakdown: { total: 500 }
|
||
|
|
}),
|
||
|
|
countTokens: jest.fn(() => 100)
|
||
|
|
}))
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
jest.unstable_mockModule(
|
||
|
|
'../../../../../scripts/modules/utils/fuzzyTaskSearch.js',
|
||
|
|
() => ({
|
||
|
|
FuzzyTaskSearch: jest.fn().mockImplementation(() => ({
|
||
|
|
findRelevantTasks: jest.fn(() => []),
|
||
|
|
getTaskIds: jest.fn(() => [])
|
||
|
|
}))
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
jest.unstable_mockModule(
|
||
|
|
'../../../../../scripts/modules/ai-services-unified.js',
|
||
|
|
() => ({
|
||
|
|
generateTextService: jest.fn().mockResolvedValue({
|
||
|
|
mainResult:
|
||
|
|
'Test research result with ```javascript\nconsole.log("test");\n```',
|
||
|
|
telemetryData: {}
|
||
|
|
})
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
|
||
|
|
displayAiUsageSummary: jest.fn(),
|
||
|
|
startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })),
|
||
|
|
stopLoadingIndicator: jest.fn()
|
||
|
|
}));
|
||
|
|
|
||
|
|
jest.unstable_mockModule(
|
||
|
|
'../../../../../scripts/modules/prompt-manager.js',
|
||
|
|
() => ({
|
||
|
|
getPromptManager: jest.fn().mockReturnValue({
|
||
|
|
loadPrompt: jest.fn().mockResolvedValue({
|
||
|
|
systemPrompt: 'System prompt',
|
||
|
|
userPrompt: 'User prompt'
|
||
|
|
})
|
||
|
|
})
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
const { performResearch } = await import(
|
||
|
|
'../../../../../scripts/modules/task-manager/research.js'
|
||
|
|
);
|
||
|
|
|
||
|
|
// Import mocked modules for testing
|
||
|
|
const utils = await import('../../../../../scripts/modules/utils.js');
|
||
|
|
const { ContextGatherer } = await import(
|
||
|
|
'../../../../../scripts/modules/utils/contextGatherer.js'
|
||
|
|
);
|
||
|
|
const { FuzzyTaskSearch } = await import(
|
||
|
|
'../../../../../scripts/modules/utils/fuzzyTaskSearch.js'
|
||
|
|
);
|
||
|
|
const { generateTextService } = await import(
|
||
|
|
'../../../../../scripts/modules/ai-services-unified.js'
|
||
|
|
);
|
||
|
|
|
||
|
|
describe('performResearch project root validation', () => {
|
||
|
|
it('throws error when project root cannot be determined', async () => {
|
||
|
|
// Mock findProjectRoot to return null
|
||
|
|
utils.findProjectRoot.mockReturnValueOnce(null);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
performResearch('Test query', {}, {}, 'json', false)
|
||
|
|
).rejects.toThrow('Could not determine project root directory');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('performResearch tag-aware functionality', () => {
|
||
|
|
let mockContextGatherer;
|
||
|
|
let mockFuzzySearch;
|
||
|
|
let mockReadJSON;
|
||
|
|
let mockFlattenTasks;
|
||
|
|
|
||
|
|
beforeEach(() => {
|
||
|
|
// Reset all mocks
|
||
|
|
jest.clearAllMocks();
|
||
|
|
|
||
|
|
// Set up default mocks
|
||
|
|
utils.findProjectRoot.mockReturnValue('/test/project/root');
|
||
|
|
utils.readJSON.mockResolvedValue({
|
||
|
|
tasks: [
|
||
|
|
{ id: 1, title: 'Task 1', description: 'Description 1' },
|
||
|
|
{ id: 2, title: 'Task 2', description: 'Description 2' }
|
||
|
|
]
|
||
|
|
});
|
||
|
|
utils.flattenTasksWithSubtasks.mockReturnValue([
|
||
|
|
{ id: 1, title: 'Task 1', description: 'Description 1' },
|
||
|
|
{ id: 2, title: 'Task 2', description: 'Description 2' }
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Set up ContextGatherer mock
|
||
|
|
mockContextGatherer = {
|
||
|
|
gather: jest.fn().mockResolvedValue({
|
||
|
|
context: 'Gathered context',
|
||
|
|
tokenBreakdown: { total: 500 }
|
||
|
|
}),
|
||
|
|
countTokens: jest.fn(() => 100)
|
||
|
|
};
|
||
|
|
ContextGatherer.mockImplementation(() => mockContextGatherer);
|
||
|
|
|
||
|
|
// Set up FuzzyTaskSearch mock
|
||
|
|
mockFuzzySearch = {
|
||
|
|
findRelevantTasks: jest.fn(() => [
|
||
|
|
{ id: 1, title: 'Task 1', description: 'Description 1' }
|
||
|
|
]),
|
||
|
|
getTaskIds: jest.fn(() => ['1'])
|
||
|
|
};
|
||
|
|
FuzzyTaskSearch.mockImplementation(() => mockFuzzySearch);
|
||
|
|
|
||
|
|
// Store references for easier access
|
||
|
|
mockReadJSON = utils.readJSON;
|
||
|
|
mockFlattenTasks = utils.flattenTasksWithSubtasks;
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('tag parameter passing to ContextGatherer', () => {
|
||
|
|
it('passes tag parameter to ContextGatherer constructor', async () => {
|
||
|
|
const testTag = 'feature-branch';
|
||
|
|
|
||
|
|
await performResearch('Test query', { tag: testTag }, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(ContextGatherer).toHaveBeenCalledWith(
|
||
|
|
'/test/project/root',
|
||
|
|
testTag
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('passes undefined tag when no tag is provided', async () => {
|
||
|
|
await performResearch('Test query', {}, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(ContextGatherer).toHaveBeenCalledWith(
|
||
|
|
'/test/project/root',
|
||
|
|
undefined
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('passes empty string tag when empty string is provided', async () => {
|
||
|
|
await performResearch('Test query', { tag: '' }, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(ContextGatherer).toHaveBeenCalledWith('/test/project/root', '');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('passes null tag when null is provided', async () => {
|
||
|
|
await performResearch('Test query', { tag: null }, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(ContextGatherer).toHaveBeenCalledWith('/test/project/root', null);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('tag-aware readJSON calls', () => {
|
||
|
|
it('calls readJSON with correct tag parameter for task discovery', async () => {
|
||
|
|
const testTag = 'development';
|
||
|
|
|
||
|
|
await performResearch('Test query', { tag: testTag }, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(mockReadJSON).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('tasks.json'),
|
||
|
|
'/test/project/root',
|
||
|
|
testTag
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls readJSON with undefined tag when no tag provided', async () => {
|
||
|
|
await performResearch('Test query', {}, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(mockReadJSON).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('tasks.json'),
|
||
|
|
'/test/project/root',
|
||
|
|
undefined
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls readJSON with provided projectRoot and tag', async () => {
|
||
|
|
const customProjectRoot = '/custom/project/root';
|
||
|
|
const testTag = 'production';
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{
|
||
|
|
projectRoot: customProjectRoot,
|
||
|
|
tag: testTag
|
||
|
|
},
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(mockReadJSON).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('tasks.json'),
|
||
|
|
customProjectRoot,
|
||
|
|
testTag
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('context gathering behavior for different tags', () => {
|
||
|
|
it('calls contextGatherer.gather with correct parameters', async () => {
|
||
|
|
const options = {
|
||
|
|
taskIds: ['1', '2'],
|
||
|
|
filePaths: ['src/file.js'],
|
||
|
|
customContext: 'Custom context',
|
||
|
|
includeProjectTree: true,
|
||
|
|
tag: 'feature-branch'
|
||
|
|
};
|
||
|
|
|
||
|
|
await performResearch('Test query', options, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalledWith({
|
||
|
|
tasks: expect.arrayContaining(['1', '2']),
|
||
|
|
files: ['src/file.js'],
|
||
|
|
customContext: 'Custom context',
|
||
|
|
includeProjectTree: true,
|
||
|
|
format: 'research',
|
||
|
|
includeTokenCounts: true
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles empty task discovery gracefully when readJSON fails', async () => {
|
||
|
|
mockReadJSON.mockRejectedValueOnce(new Error('File not found'));
|
||
|
|
|
||
|
|
const result = await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'test-tag' },
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
// Should still succeed even if task discovery fails
|
||
|
|
expect(result).toBeDefined();
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalledWith({
|
||
|
|
tasks: [],
|
||
|
|
files: [],
|
||
|
|
customContext: '',
|
||
|
|
includeProjectTree: false,
|
||
|
|
format: 'research',
|
||
|
|
includeTokenCounts: true
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('combines provided taskIds with auto-discovered tasks', async () => {
|
||
|
|
const providedTaskIds = ['3', '4'];
|
||
|
|
const autoDiscoveredIds = ['1', '2'];
|
||
|
|
|
||
|
|
mockFuzzySearch.getTaskIds.mockReturnValue(autoDiscoveredIds);
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{
|
||
|
|
taskIds: providedTaskIds,
|
||
|
|
tag: 'feature-branch'
|
||
|
|
},
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalledWith({
|
||
|
|
tasks: expect.arrayContaining([
|
||
|
|
...providedTaskIds,
|
||
|
|
...autoDiscoveredIds
|
||
|
|
]),
|
||
|
|
files: [],
|
||
|
|
customContext: '',
|
||
|
|
includeProjectTree: false,
|
||
|
|
format: 'research',
|
||
|
|
includeTokenCounts: true
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('removes duplicate tasks when auto-discovered tasks overlap with provided tasks', async () => {
|
||
|
|
const providedTaskIds = ['1', '2'];
|
||
|
|
const autoDiscoveredIds = ['2', '3']; // '2' is duplicate
|
||
|
|
|
||
|
|
mockFuzzySearch.getTaskIds.mockReturnValue(autoDiscoveredIds);
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{
|
||
|
|
taskIds: providedTaskIds,
|
||
|
|
tag: 'feature-branch'
|
||
|
|
},
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalledWith({
|
||
|
|
tasks: ['1', '2', '3'], // Should include '3' but not duplicate '2'
|
||
|
|
files: [],
|
||
|
|
customContext: '',
|
||
|
|
includeProjectTree: false,
|
||
|
|
format: 'research',
|
||
|
|
includeTokenCounts: true
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('tag-aware fuzzy search', () => {
|
||
|
|
it('initializes FuzzyTaskSearch with flattened tasks from correct tag', async () => {
|
||
|
|
const testTag = 'development';
|
||
|
|
const mockFlattenedTasks = [
|
||
|
|
{ id: 1, title: 'Dev Task 1' },
|
||
|
|
{ id: 2, title: 'Dev Task 2' }
|
||
|
|
];
|
||
|
|
|
||
|
|
mockFlattenTasks.mockReturnValue(mockFlattenedTasks);
|
||
|
|
|
||
|
|
await performResearch('Test query', { tag: testTag }, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(mockFlattenTasks).toHaveBeenCalledWith(
|
||
|
|
expect.arrayContaining([
|
||
|
|
expect.objectContaining({ id: 1 }),
|
||
|
|
expect.objectContaining({ id: 2 })
|
||
|
|
])
|
||
|
|
);
|
||
|
|
expect(FuzzyTaskSearch).toHaveBeenCalledWith(
|
||
|
|
mockFlattenedTasks,
|
||
|
|
'research'
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls fuzzy search with correct parameters', async () => {
|
||
|
|
const testQuery = 'authentication implementation';
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
testQuery,
|
||
|
|
{ tag: 'feature-branch' },
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(mockFuzzySearch.findRelevantTasks).toHaveBeenCalledWith(
|
||
|
|
testQuery,
|
||
|
|
{
|
||
|
|
maxResults: 8,
|
||
|
|
includeRecent: true,
|
||
|
|
includeCategoryMatches: true
|
||
|
|
}
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles empty tasks data gracefully', async () => {
|
||
|
|
mockReadJSON.mockResolvedValueOnce({ tasks: [] });
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'empty-tag' },
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
// Should not call FuzzyTaskSearch when no tasks exist
|
||
|
|
expect(FuzzyTaskSearch).not.toHaveBeenCalled();
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalledWith({
|
||
|
|
tasks: [],
|
||
|
|
files: [],
|
||
|
|
customContext: '',
|
||
|
|
includeProjectTree: false,
|
||
|
|
format: 'research',
|
||
|
|
includeTokenCounts: true
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles null tasks data gracefully', async () => {
|
||
|
|
mockReadJSON.mockResolvedValueOnce(null);
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'null-tag' },
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
// Should not call FuzzyTaskSearch when data is null
|
||
|
|
expect(FuzzyTaskSearch).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('error handling for invalid tags', () => {
|
||
|
|
it('continues execution when readJSON throws error for invalid tag', async () => {
|
||
|
|
mockReadJSON.mockRejectedValueOnce(new Error('Tag not found'));
|
||
|
|
|
||
|
|
const result = await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'invalid-tag' },
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
// Should still succeed and return a result
|
||
|
|
expect(result).toBeDefined();
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('logs debug message when task discovery fails', async () => {
|
||
|
|
const mockLog = {
|
||
|
|
debug: jest.fn(),
|
||
|
|
info: jest.fn(),
|
||
|
|
warn: jest.fn(),
|
||
|
|
error: jest.fn(),
|
||
|
|
success: jest.fn()
|
||
|
|
};
|
||
|
|
|
||
|
|
mockReadJSON.mockRejectedValueOnce(new Error('File not found'));
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'error-tag' },
|
||
|
|
{ mcpLog: mockLog },
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(mockLog.debug).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('Could not auto-discover tasks')
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles ContextGatherer constructor errors gracefully', async () => {
|
||
|
|
ContextGatherer.mockImplementationOnce(() => {
|
||
|
|
throw new Error('Invalid tag provided');
|
||
|
|
});
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
performResearch('Test query', { tag: 'invalid-tag' }, {}, 'json', false)
|
||
|
|
).rejects.toThrow('Invalid tag provided');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles ContextGatherer.gather errors gracefully', async () => {
|
||
|
|
mockContextGatherer.gather.mockRejectedValueOnce(
|
||
|
|
new Error('Gather failed')
|
||
|
|
);
|
||
|
|
|
||
|
|
await expect(
|
||
|
|
performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'gather-error-tag' },
|
||
|
|
{},
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
)
|
||
|
|
).rejects.toThrow('Gather failed');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('MCP integration with tags', () => {
|
||
|
|
it('uses MCP logger when mcpLog is provided in context', async () => {
|
||
|
|
const mockMCPLog = {
|
||
|
|
debug: jest.fn(),
|
||
|
|
info: jest.fn(),
|
||
|
|
warn: jest.fn(),
|
||
|
|
error: jest.fn(),
|
||
|
|
success: jest.fn()
|
||
|
|
};
|
||
|
|
|
||
|
|
mockReadJSON.mockRejectedValueOnce(new Error('Test error'));
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'mcp-tag' },
|
||
|
|
{ mcpLog: mockMCPLog },
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(mockMCPLog.debug).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('Could not auto-discover tasks')
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('passes session to generateTextService when provided', async () => {
|
||
|
|
const mockSession = { userId: 'test-user', env: {} };
|
||
|
|
|
||
|
|
await performResearch(
|
||
|
|
'Test query',
|
||
|
|
{ tag: 'session-tag' },
|
||
|
|
{ session: mockSession },
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(generateTextService).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
session: mockSession
|
||
|
|
})
|
||
|
|
);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('output format handling with tags', () => {
|
||
|
|
it('displays UI banner only in text format', async () => {
|
||
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||
|
|
|
||
|
|
await performResearch('Test query', { tag: 'ui-tag' }, {}, 'text', false);
|
||
|
|
|
||
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('🔍 AI Research Query')
|
||
|
|
);
|
||
|
|
|
||
|
|
consoleSpy.mockRestore();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('does not display UI banner in json format', async () => {
|
||
|
|
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||
|
|
|
||
|
|
await performResearch('Test query', { tag: 'ui-tag' }, {}, 'json', false);
|
||
|
|
|
||
|
|
expect(consoleSpy).not.toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('🔍 AI Research Query')
|
||
|
|
);
|
||
|
|
|
||
|
|
consoleSpy.mockRestore();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('comprehensive tag integration test', () => {
|
||
|
|
it('performs complete research flow with tag-aware functionality', async () => {
|
||
|
|
const testOptions = {
|
||
|
|
taskIds: ['1', '2'],
|
||
|
|
filePaths: ['src/main.js'],
|
||
|
|
customContext: 'Testing tag integration',
|
||
|
|
includeProjectTree: true,
|
||
|
|
detailLevel: 'high',
|
||
|
|
tag: 'integration-test',
|
||
|
|
projectRoot: '/custom/root'
|
||
|
|
};
|
||
|
|
|
||
|
|
const testContext = {
|
||
|
|
session: { userId: 'test-user' },
|
||
|
|
mcpLog: {
|
||
|
|
debug: jest.fn(),
|
||
|
|
info: jest.fn(),
|
||
|
|
warn: jest.fn(),
|
||
|
|
error: jest.fn(),
|
||
|
|
success: jest.fn()
|
||
|
|
},
|
||
|
|
commandName: 'test-research',
|
||
|
|
outputType: 'mcp'
|
||
|
|
};
|
||
|
|
|
||
|
|
// Mock successful task discovery
|
||
|
|
mockFuzzySearch.getTaskIds.mockReturnValue(['3', '4']);
|
||
|
|
|
||
|
|
const result = await performResearch(
|
||
|
|
'Integration test query',
|
||
|
|
testOptions,
|
||
|
|
testContext,
|
||
|
|
'json',
|
||
|
|
false
|
||
|
|
);
|
||
|
|
|
||
|
|
// Verify ContextGatherer was initialized with correct tag
|
||
|
|
expect(ContextGatherer).toHaveBeenCalledWith(
|
||
|
|
'/custom/root',
|
||
|
|
'integration-test'
|
||
|
|
);
|
||
|
|
|
||
|
|
// Verify readJSON was called with correct parameters
|
||
|
|
expect(mockReadJSON).toHaveBeenCalledWith(
|
||
|
|
expect.stringContaining('tasks.json'),
|
||
|
|
'/custom/root',
|
||
|
|
'integration-test'
|
||
|
|
);
|
||
|
|
|
||
|
|
// Verify context gathering was called with combined tasks
|
||
|
|
expect(mockContextGatherer.gather).toHaveBeenCalledWith({
|
||
|
|
tasks: ['1', '2', '3', '4'],
|
||
|
|
files: ['src/main.js'],
|
||
|
|
customContext: 'Testing tag integration',
|
||
|
|
includeProjectTree: true,
|
||
|
|
format: 'research',
|
||
|
|
includeTokenCounts: true
|
||
|
|
});
|
||
|
|
|
||
|
|
// Verify AI service was called with session
|
||
|
|
expect(generateTextService).toHaveBeenCalledWith(
|
||
|
|
expect.objectContaining({
|
||
|
|
session: testContext.session,
|
||
|
|
role: 'research'
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(result).toBeDefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|