feat: Complete generateObject migration with JSON mode support

This commit is contained in:
Ben Vargas
2025-07-22 15:59:15 -06:00
committed by Ralph Khreish
parent 604b94baa9
commit b16023ab2f
35 changed files with 1621 additions and 960 deletions

View File

@@ -50,7 +50,7 @@ jest.unstable_mockModule(
() => ({
generateObjectService: jest.fn().mockResolvedValue({
mainResult: {
tasks: []
complexityAnalysis: []
},
telemetryData: {
timestamp: new Date().toISOString(),
@@ -307,10 +307,15 @@ describe('analyzeTaskComplexity', () => {
return { task: task || null, originalSubtaskCount: null };
});
generateTextService.mockResolvedValue(sampleApiResponse);
generateObjectService.mockResolvedValue({
mainResult: {
complexityAnalysis: JSON.parse(sampleApiResponse.mainResult).tasks
},
telemetryData: sampleApiResponse.telemetryData
});
});
test('should call generateTextService with the correct parameters', async () => {
test('should call generateObjectService with the correct parameters', async () => {
// Arrange
const options = {
file: 'tasks/tasks.json',
@@ -338,7 +343,7 @@ describe('analyzeTaskComplexity', () => {
'/mock/project/root',
undefined
);
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
expect(mockWriteFileSync).toHaveBeenCalledWith(
expect.stringContaining('task-complexity-report.json'),
expect.stringContaining('"thresholdScore": 5'),
@@ -369,7 +374,7 @@ describe('analyzeTaskComplexity', () => {
});
// Assert
expect(generateTextService).toHaveBeenCalledWith(
expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({
role: 'research' // This should be present when research is true
})
@@ -454,7 +459,7 @@ describe('analyzeTaskComplexity', () => {
// Assert
// Check if the prompt sent to AI doesn't include the completed task (id: 3)
expect(generateTextService).toHaveBeenCalledWith(
expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.not.stringContaining('"id": 3')
})
@@ -471,7 +476,7 @@ describe('analyzeTaskComplexity', () => {
};
// Force API error
generateTextService.mockRejectedValueOnce(new Error('API Error'));
generateObjectService.mockRejectedValueOnce(new Error('API Error'));
const mockMcpLog = {
info: jest.fn(),

View File

@@ -196,9 +196,62 @@ jest.unstable_mockModule(
currency: 'USD'
}
}),
generateObjectService: jest.fn().mockResolvedValue({
mainResult: {
object: {
generateObjectService: jest.fn().mockImplementation((params) => {
const commandName = params?.commandName || 'default';
if (commandName === 'analyze-complexity') {
// Check if this is for a specific tag test by looking at the prompt
const isFeatureTag =
params?.prompt?.includes('feature') || params?.role === 'feature';
const isMasterTag =
params?.prompt?.includes('master') || params?.role === 'master';
let taskTitle = 'Test Task';
if (isFeatureTag) {
taskTitle = 'Feature Task 1';
} else if (isMasterTag) {
taskTitle = 'Master Task 1';
}
return Promise.resolve({
mainResult: {
complexityAnalysis: [
{
taskId: 1,
taskTitle: taskTitle,
complexityScore: 7,
recommendedSubtasks: 4,
expansionPrompt: 'Break down this task',
reasoning: 'This task is moderately complex'
},
{
taskId: 2,
taskTitle: 'Task 2',
complexityScore: 5,
recommendedSubtasks: 3,
expansionPrompt: 'Break down this task with a focus on task 2.',
reasoning:
'Automatically added due to missing analysis in AI response.'
}
]
},
telemetryData: {
timestamp: new Date().toISOString(),
commandName: 'analyze-complexity',
modelUsed: 'claude-3-5-sonnet',
providerName: 'anthropic',
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
totalCost: 0.012414,
currency: 'USD'
}
});
}
// Default response for expand-task and others
return Promise.resolve({
mainResult: {
subtasks: [
{
id: 1,
@@ -210,19 +263,19 @@ jest.unstable_mockModule(
testStrategy: 'Test strategy'
}
]
},
telemetryData: {
timestamp: new Date().toISOString(),
commandName: 'expand-task',
modelUsed: 'claude-3-5-sonnet',
providerName: 'anthropic',
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
totalCost: 0.012414,
currency: 'USD'
}
},
telemetryData: {
timestamp: new Date().toISOString(),
commandName: 'expand-task',
modelUsed: 'claude-3-5-sonnet',
providerName: 'anthropic',
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
totalCost: 0.012414,
currency: 'USD'
}
});
})
})
);
@@ -421,9 +474,8 @@ const { readJSON, writeJSON, getTagAwareFilePath } = await import(
'../../../../../scripts/modules/utils.js'
);
const { generateTextService, streamTextService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
const { generateTextService, generateObjectService, streamTextService } =
await import('../../../../../scripts/modules/ai-services-unified.js');
// Import the modules under test
const { default: analyzeTaskComplexity } = await import(

View File

@@ -65,8 +65,8 @@ jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
jest.unstable_mockModule(
'../../../../../scripts/modules/ai-services-unified.js',
() => ({
generateTextService: jest.fn().mockResolvedValue({
mainResult: JSON.stringify({
generateObjectService: jest.fn().mockResolvedValue({
mainResult: {
subtasks: [
{
id: 1,
@@ -101,7 +101,7 @@ jest.unstable_mockModule(
testStrategy: 'UI tests and visual regression testing'
}
]
}),
},
telemetryData: {
timestamp: new Date().toISOString(),
userId: '1234567890',
@@ -213,7 +213,7 @@ const {
findProjectRoot
} = await import('../../../../../scripts/modules/utils.js');
const { generateTextService } = await import(
const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
@@ -373,7 +373,7 @@ describe('expandTask', () => {
'/mock/project/root',
undefined
);
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
expect(writeJSON).toHaveBeenCalledWith(
tasksPath,
expect.objectContaining({
@@ -458,7 +458,7 @@ describe('expandTask', () => {
);
// Assert
expect(generateTextService).toHaveBeenCalledWith(
expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({
role: 'research',
commandName: expect.any(String)
@@ -496,7 +496,7 @@ describe('expandTask', () => {
telemetryData: expect.any(Object)
})
);
expect(generateTextService).toHaveBeenCalled();
expect(generateObjectService).toHaveBeenCalled();
});
});
@@ -743,8 +743,8 @@ describe('expandTask', () => {
// Act
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
// Assert - generateTextService called with systemPrompt for 5 subtasks
const callArg = generateTextService.mock.calls[0][0];
// Assert - generateObjectService called with systemPrompt for 5 subtasks
const callArg = generateObjectService.mock.calls[0][0];
expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
// Assert - Should use complexity-report variant with expansion prompt
@@ -831,7 +831,7 @@ describe('expandTask', () => {
projectRoot: '/mock/project/root'
};
generateTextService.mockRejectedValueOnce(new Error('AI service error'));
generateObjectService.mockRejectedValueOnce(new Error('AI service error'));
// Act & Assert
await expect(
@@ -941,7 +941,7 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, 3, false, '', context, false);
// Assert - Should work with empty context (but may include project context)
expect(generateTextService).toHaveBeenCalledWith(
expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
})
@@ -1074,7 +1074,7 @@ describe('expandTask', () => {
// Assert - Should complete successfully
expect(result).toBeDefined();
expect(generateTextService).toHaveBeenCalled();
expect(generateObjectService).toHaveBeenCalled();
});
test('should use dynamic prompting when numSubtasks is 0', async () => {
@@ -1095,11 +1095,11 @@ describe('expandTask', () => {
// Act
await expandTask(tasksPath, taskId, 0, false, '', context, false);
// Assert - Verify generateTextService was called
expect(generateTextService).toHaveBeenCalled();
// Assert - Verify generateObjectService was called
expect(generateObjectService).toHaveBeenCalled();
// Get the call arguments to verify the system prompt
const callArgs = generateTextService.mock.calls[0][0];
const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain(
'an appropriate number of specific subtasks'
);
@@ -1122,11 +1122,11 @@ describe('expandTask', () => {
// Act
await expandTask(tasksPath, taskId, 5, false, '', context, false);
// Assert - Verify generateTextService was called
expect(generateTextService).toHaveBeenCalled();
// Assert - Verify generateObjectService was called
expect(generateObjectService).toHaveBeenCalled();
// Get the call arguments to verify the system prompt
const callArgs = generateTextService.mock.calls[0][0];
const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('5 specific subtasks');
});
@@ -1151,8 +1151,8 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, -3, false, '', context, false);
// Assert - Should use default value instead of negative
expect(generateTextService).toHaveBeenCalled();
const callArgs = generateTextService.mock.calls[0][0];
expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('4 specific subtasks');
});
@@ -1177,8 +1177,8 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, undefined, false, '', context, false);
// Assert - Should use default value
expect(generateTextService).toHaveBeenCalled();
const callArgs = generateTextService.mock.calls[0][0];
expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('6 specific subtasks');
});
@@ -1203,8 +1203,8 @@ describe('expandTask', () => {
await expandTask(tasksPath, taskId, null, false, '', context, false);
// Assert - Should use default value
expect(generateTextService).toHaveBeenCalled();
const callArgs = generateTextService.mock.calls[0][0];
expect(generateObjectService).toHaveBeenCalled();
const callArgs = generateObjectService.mock.calls[0][0];
expect(callArgs.systemPrompt).toContain('7 specific subtasks');
});
});

View File

@@ -43,7 +43,25 @@ jest.unstable_mockModule(
() => ({
generateTextService: jest
.fn()
.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} })
.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} }),
generateObjectService: jest
.fn()
.mockResolvedValue({
mainResult: {
task: {
id: 1,
title: 'Updated Task',
description: 'Updated description',
status: 'pending',
dependencies: [],
priority: 'medium',
details: null,
testStrategy: null,
subtasks: []
}
},
telemetryData: {}
})
})
);

View File

@@ -30,6 +30,12 @@ jest.unstable_mockModule(
generateTextService: jest.fn().mockResolvedValue({
mainResult: '[]', // mainResult is the text string directly
telemetryData: {}
}),
generateObjectService: jest.fn().mockResolvedValue({
mainResult: {
tasks: [] // generateObject returns structured data
},
telemetryData: {}
})
})
);
@@ -84,7 +90,7 @@ const { readJSON, writeJSON, log } = await import(
'../../../../../scripts/modules/utils.js'
);
const { generateTextService } = await import(
const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
@@ -154,7 +160,9 @@ describe('updateTasks', () => {
];
const mockApiResponse = {
mainResult: JSON.stringify(mockUpdatedTasks), // mainResult is the JSON string directly
mainResult: {
tasks: mockUpdatedTasks // generateObject returns structured data
},
telemetryData: {}
};
@@ -164,7 +172,7 @@ describe('updateTasks', () => {
tag: 'master',
_rawTaggedData: mockInitialTasks
});
generateTextService.mockResolvedValue(mockApiResponse);
generateObjectService.mockResolvedValue(mockApiResponse);
// Act
const result = await updateTasks(
@@ -185,7 +193,7 @@ describe('updateTasks', () => {
);
// 2. AI Service called with correct args
expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
// 3. Write JSON called with correctly merged tasks
expect(writeJSON).toHaveBeenCalledWith(
@@ -252,7 +260,7 @@ describe('updateTasks', () => {
'/mock/path',
'master'
);
expect(generateTextService).not.toHaveBeenCalled();
expect(generateObjectService).not.toHaveBeenCalled();
expect(writeJSON).not.toHaveBeenCalled();
expect(log).toHaveBeenCalledWith(
'info',
@@ -327,8 +335,10 @@ describe('updateTasks', () => {
_rawTaggedData: mockTaggedData
});
generateTextService.mockResolvedValue({
mainResult: JSON.stringify(mockUpdatedTasks),
generateObjectService.mockResolvedValue({
mainResult: {
tasks: mockUpdatedTasks
},
telemetryData: { commandName: 'update-tasks', totalCost: 0.05 }
});