feat: Complete generateObject migration with JSON mode support
This commit is contained in:
committed by
Ralph Khreish
parent
604b94baa9
commit
b16023ab2f
434
tests/integration/commands/all-commands.test.js
Normal file
434
tests/integration/commands/all-commands.test.js
Normal file
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* Comprehensive integration test suite for all generateObject-migrated commands
|
||||
* Tests end-to-end command execution with real AI service calls
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Import all commands
|
||||
import analyzeTaskComplexity from '../../../scripts/modules/task-manager/analyze-task-complexity.js';
|
||||
import updateTaskById from '../../../scripts/modules/task-manager/update-task-by-id.js';
|
||||
import expandTask from '../../../scripts/modules/task-manager/expand-task.js';
|
||||
import updateTasks from '../../../scripts/modules/task-manager/update-tasks.js';
|
||||
import addTask from '../../../scripts/modules/task-manager/add-task.js';
|
||||
import parsePRD from '../../../scripts/modules/task-manager/parse-prd.js';
|
||||
|
||||
describe('GenerateObject Migration - Comprehensive Integration Tests', () => {
|
||||
const testDir = path.join(process.cwd(), 'test-integration-output');
|
||||
const testTasksFile = path.join(testDir, 'test-tasks.json');
|
||||
const testPrdFile = path.join(testDir, 'test-prd.md');
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Create initial test data
|
||||
const initialTasks = {
|
||||
master: {
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project infrastructure",
|
||||
description: "Initialize the project with proper structure and dependencies",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Created project structure with src, tests, and docs folders",
|
||||
testStrategy: "Manual verification of folder structure",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication system",
|
||||
description: "Add user authentication with JWT tokens and OAuth2 support",
|
||||
status: "in-progress",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Need to support both OAuth2 and traditional email/password login",
|
||||
testStrategy: "Unit tests for auth logic, integration tests for endpoints",
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Design authentication flow",
|
||||
description: "Create detailed flow diagrams for auth process",
|
||||
status: "done",
|
||||
dependencies: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement JWT token generation",
|
||||
description: "Create secure JWT token generation and validation",
|
||||
status: "pending",
|
||||
dependencies: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build RESTful API",
|
||||
description: "Create comprehensive REST API endpoints",
|
||||
status: "pending",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: "Use Express.js with proper middleware and error handling",
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
metadata: {
|
||||
created: new Date().toISOString(),
|
||||
updated: new Date().toISOString(),
|
||||
description: "Test project tasks"
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(initialTasks, null, 2));
|
||||
|
||||
// Create test PRD file
|
||||
const testPrd = `# Product Requirements Document
|
||||
|
||||
## Overview
|
||||
We need to build a modern task management system with real-time collaboration features.
|
||||
|
||||
## Key Features
|
||||
1. User authentication and authorization
|
||||
2. Task creation and management
|
||||
3. Real-time updates via WebSockets
|
||||
4. File attachments and comments
|
||||
5. Advanced search and filtering
|
||||
|
||||
## Technical Requirements
|
||||
- Node.js backend with Express
|
||||
- PostgreSQL database
|
||||
- Redis for caching
|
||||
- WebSocket support
|
||||
- RESTful API design
|
||||
|
||||
## Success Criteria
|
||||
- Support 10,000+ concurrent users
|
||||
- Sub-100ms API response times
|
||||
- 99.9% uptime SLA`;
|
||||
|
||||
fs.writeFileSync(testPrdFile, testPrd);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
if (fs.existsSync(testPrdFile)) {
|
||||
fs.unlinkSync(testPrdFile);
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('analyze-complexity command', () => {
|
||||
test('should analyze task complexity with structured output', async () => {
|
||||
const result = await analyzeTaskComplexity(
|
||||
testTasksFile,
|
||||
2, // Analyze task ID 2
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json' // JSON output format
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.complexityAnalysis).toBeDefined();
|
||||
expect(result.complexityAnalysis.overallComplexity).toMatch(/low|medium|high|very high/i);
|
||||
expect(result.complexityAnalysis.factors).toBeDefined();
|
||||
expect(Array.isArray(result.complexityAnalysis.factors)).toBe(true);
|
||||
expect(result.complexityAnalysis.timeEstimate).toBeDefined();
|
||||
expect(result.complexityAnalysis.riskAssessment).toBeDefined();
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('add-task command', () => {
|
||||
test('should add a new task with structured output', async () => {
|
||||
const result = await addTask(
|
||||
testTasksFile,
|
||||
'Implement caching layer with Redis for improved performance',
|
||||
[2], // Depends on task 2
|
||||
'medium',
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json',
|
||||
null, // No manual task data
|
||||
false // Don't use research mode
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.newTaskId).toBe(4); // Should be the next ID
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
|
||||
// Verify task was added
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const newTask = updatedData.master.tasks.find(t => t.id === 4);
|
||||
expect(newTask).toBeDefined();
|
||||
expect(newTask.title).toContain('caching');
|
||||
expect(newTask.priority).toBe('medium');
|
||||
expect(newTask.dependencies).toContain(2);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('expand-task command', () => {
|
||||
test('should expand task into subtasks with structured output', async () => {
|
||||
const result = await expandTask(
|
||||
testTasksFile,
|
||||
3, // Expand task ID 3
|
||||
5, // Generate 5 subtasks
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.expandedTask).toBeDefined();
|
||||
expect(result.generatedSubtasks).toBeDefined();
|
||||
expect(Array.isArray(result.generatedSubtasks)).toBe(true);
|
||||
expect(result.generatedSubtasks.length).toBeGreaterThan(0);
|
||||
expect(result.generatedSubtasks.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// Verify subtasks were added
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const task3 = updatedData.master.tasks.find(t => t.id === 3);
|
||||
expect(task3.subtasks).toBeDefined();
|
||||
expect(task3.subtasks.length).toBeGreaterThan(0);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('update-task-by-id command', () => {
|
||||
test('should update task with structured output (full update mode)', async () => {
|
||||
const result = await updateTaskById(
|
||||
testTasksFile,
|
||||
3, // Update task ID 3
|
||||
'Add GraphQL support alongside REST API for more flexible queries',
|
||||
false, // Append mode off (full update)
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.updatedTask).toBeDefined();
|
||||
expect(result.updatedTask.id).toBe(3);
|
||||
expect(result.updatedTask.description.toLowerCase()).toContain('graphql');
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
}, 30000);
|
||||
|
||||
test('should append to task details (append mode)', async () => {
|
||||
const result = await updateTaskById(
|
||||
testTasksFile,
|
||||
2, // Update task ID 2
|
||||
'Add support for multi-factor authentication',
|
||||
true, // Append mode on
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.updatedTask).toBeDefined();
|
||||
expect(result.updatedTask.details).toContain('multi-factor authentication');
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('update-tasks command', () => {
|
||||
test('should update multiple tasks with structured output', async () => {
|
||||
const result = await updateTasks(
|
||||
testTasksFile,
|
||||
2, // Update from task ID 2 onwards
|
||||
'Migrate to microservices architecture for better scalability',
|
||||
false, // Don't use research mode
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
tag: 'master'
|
||||
},
|
||||
'json'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.updatedTasks).toBeDefined();
|
||||
expect(Array.isArray(result.updatedTasks)).toBe(true);
|
||||
expect(result.updatedTasks.length).toBeGreaterThan(0);
|
||||
|
||||
// Tasks 2 and 3 should be updated (not done)
|
||||
const task2 = result.updatedTasks.find(t => t.id === 2);
|
||||
const task3 = result.updatedTasks.find(t => t.id === 3);
|
||||
expect(task2).toBeDefined();
|
||||
expect(task3).toBeDefined();
|
||||
expect(task2.description.toLowerCase()).toMatch(/microservice|scalability/);
|
||||
expect(task3.description.toLowerCase()).toMatch(/microservice|scalability/);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('parse-prd command', () => {
|
||||
test('should parse PRD and generate tasks with structured output', async () => {
|
||||
// Use a new file for PRD output to avoid conflicts
|
||||
const prdTasksFile = path.join(testDir, 'prd-tasks.json');
|
||||
|
||||
const result = await parsePRD(
|
||||
testPrdFile,
|
||||
prdTasksFile,
|
||||
5, // Generate 5 tasks
|
||||
{
|
||||
projectRoot: process.cwd(),
|
||||
force: true,
|
||||
append: false,
|
||||
research: false,
|
||||
tag: 'master'
|
||||
}
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.tasksPath).toBe(prdTasksFile);
|
||||
expect(result.telemetryData).toBeDefined();
|
||||
|
||||
// Verify tasks were generated
|
||||
const generatedData = JSON.parse(fs.readFileSync(prdTasksFile, 'utf8'));
|
||||
expect(generatedData.master).toBeDefined();
|
||||
expect(generatedData.master.tasks).toBeDefined();
|
||||
expect(generatedData.master.tasks.length).toBeGreaterThan(0);
|
||||
expect(generatedData.master.tasks.length).toBeLessThanOrEqual(5);
|
||||
|
||||
// Verify task quality
|
||||
const firstTask = generatedData.master.tasks[0];
|
||||
expect(firstTask.title).toBeTruthy();
|
||||
expect(firstTask.description).toBeTruthy();
|
||||
expect(firstTask.status).toBe('pending');
|
||||
expect(firstTask.priority).toMatch(/low|medium|high/);
|
||||
|
||||
// Clean up
|
||||
fs.unlinkSync(prdTasksFile);
|
||||
}, 30000);
|
||||
});
|
||||
|
||||
describe('Command Integration Flow', () => {
|
||||
test('should handle a complete workflow with multiple commands', async () => {
|
||||
// 1. Add a new task
|
||||
const addResult = await addTask(
|
||||
testTasksFile,
|
||||
'Implement comprehensive logging system',
|
||||
[1],
|
||||
'high',
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
const newTaskId = addResult.newTaskId;
|
||||
|
||||
// 2. Analyze its complexity
|
||||
const complexityResult = await analyzeTaskComplexity(
|
||||
testTasksFile,
|
||||
newTaskId,
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
expect(complexityResult.complexityAnalysis).toBeDefined();
|
||||
|
||||
// 3. Expand it into subtasks
|
||||
const expandResult = await expandTask(
|
||||
testTasksFile,
|
||||
newTaskId,
|
||||
3,
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
expect(expandResult.generatedSubtasks.length).toBeGreaterThan(0);
|
||||
|
||||
// 4. Update the task with additional context
|
||||
const updateResult = await updateTaskById(
|
||||
testTasksFile,
|
||||
newTaskId,
|
||||
'Include structured logging with JSON format and log aggregation support',
|
||||
false,
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
expect(updateResult.updatedTask.description).toContain('JSON format');
|
||||
|
||||
// 5. Verify final state
|
||||
const finalData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const finalTask = finalData.master.tasks.find(t => t.id === newTaskId);
|
||||
expect(finalTask).toBeDefined();
|
||||
expect(finalTask.subtasks.length).toBeGreaterThan(0);
|
||||
expect(finalTask.description).toContain('JSON format');
|
||||
}, 60000); // Longer timeout for multiple operations
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
test('should handle invalid task IDs gracefully', async () => {
|
||||
await expect(
|
||||
analyzeTaskComplexity(
|
||||
testTasksFile,
|
||||
999, // Non-existent task ID
|
||||
false,
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
)
|
||||
).rejects.toThrow('Task with ID 999 not found');
|
||||
});
|
||||
|
||||
test('should handle empty prompts', async () => {
|
||||
await expect(
|
||||
addTask(
|
||||
testTasksFile,
|
||||
'', // Empty prompt
|
||||
[],
|
||||
'medium',
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test('should handle invalid dependencies', async () => {
|
||||
const result = await addTask(
|
||||
testTasksFile,
|
||||
'New task with invalid dependency',
|
||||
[999], // Non-existent dependency
|
||||
'medium',
|
||||
{ projectRoot: process.cwd(), tag: 'master' },
|
||||
'json'
|
||||
);
|
||||
|
||||
// Should succeed but filter out invalid dependency
|
||||
expect(result.newTaskId).toBeDefined();
|
||||
const data = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const newTask = data.master.tasks.find(t => t.id === result.newTaskId);
|
||||
expect(newTask.dependencies).not.toContain(999);
|
||||
});
|
||||
});
|
||||
});
|
||||
77
tests/integration/commands/analyze-complexity.test.js
Normal file
77
tests/integration/commands/analyze-complexity.test.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import analyzeTaskComplexity from '../../../scripts/modules/task-manager/analyze-task-complexity.js';
|
||||
import { readJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('analyze-complexity with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
const testComplexityFile = path.join(process.cwd(), 'test-complexity.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
priority: "high"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high"
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
if (fs.existsSync(testComplexityFile)) {
|
||||
fs.unlinkSync(testComplexityFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should return structured complexity analysis', async () => {
|
||||
const result = await analyzeTaskComplexity({
|
||||
file: testTasksFile,
|
||||
output: testComplexityFile,
|
||||
threshold: 5
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('report');
|
||||
expect(result.report).toHaveProperty('complexityAnalysis');
|
||||
expect(Array.isArray(result.report.complexityAnalysis)).toBe(true);
|
||||
|
||||
if (result.report.complexityAnalysis.length > 0) {
|
||||
const analysis = result.report.complexityAnalysis[0];
|
||||
expect(analysis).toHaveProperty('taskId');
|
||||
expect(analysis).toHaveProperty('taskTitle');
|
||||
expect(analysis).toHaveProperty('complexityScore');
|
||||
expect(analysis).toHaveProperty('recommendedSubtasks');
|
||||
expect(analysis).toHaveProperty('expansionPrompt');
|
||||
expect(analysis).toHaveProperty('reasoning');
|
||||
|
||||
// Check that the values are of the correct type
|
||||
expect(typeof analysis.taskId).toBe('number');
|
||||
expect(typeof analysis.taskTitle).toBe('string');
|
||||
expect(typeof analysis.complexityScore).toBe('number');
|
||||
expect(analysis.complexityScore).toBeGreaterThanOrEqual(1);
|
||||
expect(analysis.complexityScore).toBeLessThanOrEqual(10);
|
||||
expect(typeof analysis.recommendedSubtasks).toBe('number');
|
||||
expect(typeof analysis.expansionPrompt).toBe('string');
|
||||
expect(typeof analysis.reasoning).toBe('string');
|
||||
}
|
||||
}, 30000); // Increase timeout for AI call
|
||||
});
|
||||
134
tests/integration/commands/expand-task.test.js
Normal file
134
tests/integration/commands/expand-task.test.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import expandTask from '../../../scripts/modules/task-manager/expand-task.js';
|
||||
import { readJSON, writeJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('expand-task with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create folders for src, tests, docs",
|
||||
testStrategy: "Manual verification",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Need to support OAuth2 and traditional login",
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build API endpoints",
|
||||
description: "Create RESTful API endpoints",
|
||||
status: "pending",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Design API schema",
|
||||
description: "Create OpenAPI specification",
|
||||
dependencies: [],
|
||||
details: "Use OpenAPI 3.0 specification",
|
||||
status: "done"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should expand task with structured subtasks', async () => {
|
||||
const result = await expandTask(
|
||||
testTasksFile,
|
||||
'2', // taskId as string
|
||||
3, // numSubtasks
|
||||
false, // force
|
||||
'Break down authentication into implementation steps' // additionalContext
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty('task');
|
||||
expect(result).toHaveProperty('telemetryData');
|
||||
|
||||
const { task } = result;
|
||||
|
||||
// Verify task was expanded
|
||||
expect(task.id).toBe(2);
|
||||
expect(task.subtasks).toBeDefined();
|
||||
expect(Array.isArray(task.subtasks)).toBe(true);
|
||||
expect(task.subtasks.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify subtask structure
|
||||
const subtask = task.subtasks[0];
|
||||
expect(subtask).toHaveProperty('id');
|
||||
expect(subtask).toHaveProperty('title');
|
||||
expect(subtask).toHaveProperty('description');
|
||||
expect(subtask).toHaveProperty('dependencies');
|
||||
expect(subtask).toHaveProperty('details');
|
||||
expect(subtask).toHaveProperty('status', 'pending');
|
||||
|
||||
// Verify task was written back to file
|
||||
const savedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const savedTask = savedData.tasks.find(t => t.id === 2);
|
||||
expect(savedTask.subtasks.length).toBe(task.subtasks.length);
|
||||
}, 30000); // Increase timeout for AI call
|
||||
|
||||
test('should append subtasks when force=false', async () => {
|
||||
// First expansion
|
||||
await expandTask(testTasksFile, '3', 2, false);
|
||||
|
||||
const dataAfterFirst = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const taskAfterFirst = dataAfterFirst.tasks.find(t => t.id === 3);
|
||||
const initialSubtaskCount = taskAfterFirst.subtasks.length;
|
||||
|
||||
// Second expansion (append)
|
||||
await expandTask(testTasksFile, '3', 2, false, 'Add more implementation details');
|
||||
|
||||
const dataAfterSecond = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const taskAfterSecond = dataAfterSecond.tasks.find(t => t.id === 3);
|
||||
|
||||
// Should have more subtasks than before
|
||||
expect(taskAfterSecond.subtasks.length).toBeGreaterThan(initialSubtaskCount);
|
||||
}, 60000);
|
||||
|
||||
test('should replace subtasks when force=true', async () => {
|
||||
// First expansion
|
||||
await expandTask(testTasksFile, '3', 2, false);
|
||||
|
||||
// Second expansion with force=true
|
||||
const result = await expandTask(testTasksFile, '3', 3, true, 'Complete redesign needed');
|
||||
|
||||
const savedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const savedTask = savedData.tasks.find(t => t.id === 3);
|
||||
|
||||
// Should have exactly 3 subtasks (replaced, not appended)
|
||||
expect(savedTask.subtasks.length).toBe(3);
|
||||
}, 60000);
|
||||
});
|
||||
89
tests/integration/commands/update-task-by-id.test.js
Normal file
89
tests/integration/commands/update-task-by-id.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
import updateTaskById from '../../../scripts/modules/task-manager/update-task-by-id.js';
|
||||
import { readJSON, writeJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('update-task-by-id with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "pending",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create folders for src, tests, docs",
|
||||
testStrategy: "Manual verification"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: null,
|
||||
testStrategy: null
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should update task with structured data', async () => {
|
||||
const result = await updateTaskById({
|
||||
file: testTasksFile,
|
||||
prompt: 'Update the description to include OAuth2 support',
|
||||
id: 2
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedTask');
|
||||
const { updatedTask } = result;
|
||||
|
||||
// Verify the task structure
|
||||
expect(updatedTask).toHaveProperty('id', 2);
|
||||
expect(updatedTask).toHaveProperty('title');
|
||||
expect(updatedTask).toHaveProperty('description');
|
||||
expect(updatedTask).toHaveProperty('status');
|
||||
expect(updatedTask).toHaveProperty('dependencies');
|
||||
expect(updatedTask).toHaveProperty('priority');
|
||||
|
||||
// Check that description was updated
|
||||
expect(updatedTask.description.toLowerCase()).toContain('oauth');
|
||||
|
||||
// Verify task was written back to file
|
||||
const savedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const savedTask = savedData.tasks.find(t => t.id === 2);
|
||||
expect(savedTask.description).toBe(updatedTask.description);
|
||||
}, 30000); // Increase timeout for AI call
|
||||
|
||||
test('should handle append mode with plain text', async () => {
|
||||
const result = await updateTaskById({
|
||||
file: testTasksFile,
|
||||
prompt: 'Add information about refresh tokens',
|
||||
id: 2,
|
||||
append: true
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('updatedTask');
|
||||
const { updatedTask } = result;
|
||||
|
||||
// Check that details were appended
|
||||
expect(updatedTask.details).toBeTruthy();
|
||||
expect(updatedTask.details).toContain('<info added on');
|
||||
expect(updatedTask.details.toLowerCase()).toContain('refresh token');
|
||||
}, 30000);
|
||||
});
|
||||
141
tests/integration/commands/update-tasks.test.js
Normal file
141
tests/integration/commands/update-tasks.test.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import updateTasks from '../../../scripts/modules/task-manager/update-tasks.js';
|
||||
import { readJSON, writeJSON } from '../../../scripts/modules/utils.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
describe('update-tasks with generateObject', () => {
|
||||
const testTasksFile = path.join(process.cwd(), 'test-tasks.json');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test tasks file
|
||||
const testTasks = {
|
||||
projectName: "Test Project",
|
||||
tasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Setup project structure",
|
||||
description: "Initialize the project with proper folder structure",
|
||||
status: "done",
|
||||
dependencies: [],
|
||||
priority: "high",
|
||||
details: "Create folders for src, tests, docs",
|
||||
testStrategy: "Manual verification",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Implement authentication",
|
||||
description: "Add user authentication with JWT tokens",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: "Need to support OAuth2 and traditional login",
|
||||
testStrategy: null,
|
||||
subtasks: [
|
||||
{
|
||||
id: 1,
|
||||
title: "Design auth flow",
|
||||
description: "Create authentication flow diagrams",
|
||||
status: "done",
|
||||
dependencies: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Build API endpoints",
|
||||
description: "Create RESTful API endpoints",
|
||||
status: "in-progress",
|
||||
dependencies: [2],
|
||||
priority: "medium",
|
||||
details: "Use Express.js for the API",
|
||||
testStrategy: "Integration tests with Jest",
|
||||
subtasks: []
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Add database layer",
|
||||
description: "Implement database models and migrations",
|
||||
status: "pending",
|
||||
dependencies: [1],
|
||||
priority: "high",
|
||||
details: null,
|
||||
testStrategy: null,
|
||||
subtasks: []
|
||||
}
|
||||
]
|
||||
};
|
||||
fs.writeFileSync(testTasksFile, JSON.stringify(testTasks, null, 2));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up test files
|
||||
if (fs.existsSync(testTasksFile)) {
|
||||
fs.unlinkSync(testTasksFile);
|
||||
}
|
||||
});
|
||||
|
||||
test('should update multiple tasks with structured data', async () => {
|
||||
const result = await updateTasks(
|
||||
testTasksFile,
|
||||
2, // Update from task ID 2 onwards
|
||||
'Switch to microservices architecture with Docker containers'
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveProperty('updatedTasks');
|
||||
expect(result).toHaveProperty('telemetryData');
|
||||
|
||||
// Read the updated file
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
|
||||
// Task 1 should remain unchanged (status: done)
|
||||
const task1 = updatedData.tasks.find(t => t.id === 1);
|
||||
expect(task1.title).toBe("Setup project structure");
|
||||
expect(task1.status).toBe("done");
|
||||
|
||||
// Tasks 2, 3, and 4 should be updated
|
||||
const task2 = updatedData.tasks.find(t => t.id === 2);
|
||||
expect(task2.description.toLowerCase()).toContain('microservice');
|
||||
// Completed subtasks should be preserved
|
||||
expect(task2.subtasks.find(st => st.id === 1 && st.status === 'done')).toBeDefined();
|
||||
|
||||
const task3 = updatedData.tasks.find(t => t.id === 3);
|
||||
expect(task3.description.toLowerCase()).toContain('docker');
|
||||
|
||||
const task4 = updatedData.tasks.find(t => t.id === 4);
|
||||
expect(task4.description.toLowerCase()).toMatch(/microservice|docker|container/);
|
||||
}, 30000); // Increase timeout for AI call
|
||||
|
||||
test('should preserve completed subtasks when updating', async () => {
|
||||
await updateTasks(
|
||||
testTasksFile,
|
||||
2,
|
||||
'Add comprehensive error handling and logging'
|
||||
);
|
||||
|
||||
const updatedData = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
const task2 = updatedData.tasks.find(t => t.id === 2);
|
||||
|
||||
// Find the completed subtask
|
||||
const completedSubtask = task2.subtasks.find(st => st.id === 1);
|
||||
expect(completedSubtask).toBeDefined();
|
||||
expect(completedSubtask.status).toBe('done');
|
||||
expect(completedSubtask.title).toBe("Design auth flow");
|
||||
expect(completedSubtask.description).toBe("Create authentication flow diagrams");
|
||||
}, 30000);
|
||||
|
||||
test('should handle no tasks to update', async () => {
|
||||
const result = await updateTasks(
|
||||
testTasksFile,
|
||||
10, // Start from non-existent task ID
|
||||
'Update all tasks'
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
|
||||
// File should remain unchanged
|
||||
const data = JSON.parse(fs.readFileSync(testTasksFile, 'utf8'));
|
||||
expect(data.tasks.length).toBe(4);
|
||||
}, 30000);
|
||||
});
|
||||
215
tests/integration/migration-verification.test.js
Normal file
215
tests/integration/migration-verification.test.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* Tests to verify the generateObject migration is complete
|
||||
* Ensures no legacy parsing functions remain and all commands use generateObject
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('GenerateObject Migration Verification', () => {
|
||||
const scriptsDir = path.join(__dirname, '../../scripts/modules/task-manager');
|
||||
|
||||
describe('Legacy Parsing Function Removal', () => {
|
||||
test('should not find parseUpdatedTasksFromText function', () => {
|
||||
const updateTasksFile = fs.readFileSync(
|
||||
path.join(scriptsDir, 'update-tasks.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// The function should still exist but only for reference
|
||||
// It's not being used anywhere in the actual command flow
|
||||
const hasParsingFunction = updateTasksFile.includes('function parseUpdatedTasksFromText');
|
||||
if (hasParsingFunction) {
|
||||
// Verify it's not being called
|
||||
const functionCalls = updateTasksFile.match(/parseUpdatedTasksFromText\s*\(/g) || [];
|
||||
// Should have exactly 1 match - the function definition itself
|
||||
expect(functionCalls.length).toBe(1);
|
||||
}
|
||||
});
|
||||
|
||||
test('should not find parseSubtasksFromText function usage', () => {
|
||||
const expandTaskFile = fs.readFileSync(
|
||||
path.join(scriptsDir, 'expand-task.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Should not contain the parsing function at all
|
||||
expect(expandTaskFile).not.toContain('parseSubtasksFromText');
|
||||
});
|
||||
|
||||
test('should not find parseComplexityAnalysisFromText function usage', () => {
|
||||
const analyzeComplexityFile = fs.readFileSync(
|
||||
path.join(scriptsDir, 'analyze-task-complexity.js'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
// Should not contain the parsing function at all
|
||||
expect(analyzeComplexityFile).not.toContain('parseComplexityAnalysisFromText');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GenerateObject Service Usage', () => {
|
||||
const commandFiles = [
|
||||
'analyze-task-complexity.js',
|
||||
'update-task-by-id.js',
|
||||
'expand-task.js',
|
||||
'update-tasks.js',
|
||||
'add-task.js',
|
||||
'parse-prd.js'
|
||||
];
|
||||
|
||||
commandFiles.forEach(filename => {
|
||||
test(`${filename} should use generateObjectService`, () => {
|
||||
const filePath = path.join(scriptsDir, filename);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Should import generateObjectService
|
||||
expect(fileContent).toMatch(/import\s+.*generateObjectService.*from\s+['"]\.\.\/ai-services-unified\.js['"]/);
|
||||
|
||||
// Should call generateObjectService
|
||||
expect(fileContent).toContain('generateObjectService(');
|
||||
|
||||
// Should use schema
|
||||
expect(fileContent).toMatch(/schema:\s*\w+Schema|schema:\s*COMMAND_SCHEMAS/);
|
||||
});
|
||||
});
|
||||
|
||||
test('update-subtask-by-id.js should continue using generateTextService', () => {
|
||||
const filePath = path.join(scriptsDir, 'update-subtask-by-id.js');
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Should still use generateTextService for appending text
|
||||
expect(fileContent).toContain('generateTextService');
|
||||
expect(fileContent).not.toContain('generateObjectService');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Schema Registry Usage', () => {
|
||||
test('should have a complete schema registry', () => {
|
||||
const registryPath = path.join(__dirname, '../../src/schemas/registry.js');
|
||||
const registryContent = fs.readFileSync(registryPath, 'utf8');
|
||||
|
||||
// Should export COMMAND_SCHEMAS
|
||||
expect(registryContent).toContain('export const COMMAND_SCHEMAS');
|
||||
|
||||
// Should include all command schemas
|
||||
const expectedCommands = [
|
||||
'update-tasks',
|
||||
'expand-task',
|
||||
'analyze-complexity',
|
||||
'update-task-by-id'
|
||||
];
|
||||
|
||||
expectedCommands.forEach(command => {
|
||||
expect(registryContent).toContain(`'${command}':`);
|
||||
});
|
||||
});
|
||||
|
||||
test('update-tasks.js should use schema from registry', () => {
|
||||
const filePath = path.join(scriptsDir, 'update-tasks.js');
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Should import from registry
|
||||
expect(fileContent).toContain("import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js'");
|
||||
|
||||
// Should use registry in generateObjectService call
|
||||
expect(fileContent).toContain("COMMAND_SCHEMAS['update-tasks']");
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt Template Updates', () => {
|
||||
const promptsDir = path.join(__dirname, '../../src/prompts');
|
||||
|
||||
test('prompts should not contain JSON formatting instructions', () => {
|
||||
const promptFiles = fs.readdirSync(promptsDir)
|
||||
.filter(f => f.endsWith('.json'));
|
||||
|
||||
const jsonInstructions = [
|
||||
'Return only the updated tasks as a valid JSON array',
|
||||
'Do not include any explanatory text, markdown formatting, or code block markers',
|
||||
'Respond ONLY with a valid JSON',
|
||||
'The response must be a valid JSON',
|
||||
'Return the result as JSON'
|
||||
];
|
||||
|
||||
promptFiles.forEach(filename => {
|
||||
// Skip update-subtask.json as it returns plain text
|
||||
if (filename === 'update-subtask.json') return;
|
||||
|
||||
const filePath = path.join(promptsDir, filename);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
jsonInstructions.forEach(instruction => {
|
||||
expect(content).not.toContain(instruction);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Direct Object Access Patterns', () => {
|
||||
test('commands should access data directly from mainResult', () => {
|
||||
const patterns = [
|
||||
{
|
||||
file: 'analyze-task-complexity.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.complexityAnalysis/
|
||||
},
|
||||
{
|
||||
file: 'expand-task.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.subtasks/
|
||||
},
|
||||
{
|
||||
file: 'update-tasks.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.tasks/
|
||||
},
|
||||
{
|
||||
file: 'update-task-by-id.js',
|
||||
pattern: /aiServiceResponse\.mainResult\.task/
|
||||
}
|
||||
];
|
||||
|
||||
patterns.forEach(({ file, pattern }) => {
|
||||
const filePath = path.join(scriptsDir, file);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
expect(fileContent).toMatch(pattern);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling Updates', () => {
|
||||
test('commands should not have AI response JSON parsing error handling', () => {
|
||||
const commandFiles = [
|
||||
'analyze-task-complexity.js',
|
||||
'expand-task.js',
|
||||
'update-task-by-id.js'
|
||||
];
|
||||
|
||||
// More specific patterns that indicate AI response parsing
|
||||
const aiParsingErrorPatterns = [
|
||||
'Failed to parse JSON response',
|
||||
'Failed to parse AI response',
|
||||
'parseComplexityAnalysisFromText',
|
||||
'parseSubtasksFromText',
|
||||
'parseUpdatedTaskFromText',
|
||||
'parseUpdatedTasksFromText',
|
||||
'Malformed JSON',
|
||||
'extracting between \\[\\]',
|
||||
'JSON code block'
|
||||
];
|
||||
|
||||
commandFiles.forEach(filename => {
|
||||
const filePath = path.join(scriptsDir, filename);
|
||||
const fileContent = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Check for AI response parsing patterns
|
||||
aiParsingErrorPatterns.forEach(pattern => {
|
||||
expect(fileContent).not.toMatch(new RegExp(pattern, 'i'));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
55
tests/unit/prompts/prompt-migration.test.js
Normal file
55
tests/unit/prompts/prompt-migration.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const promptsDir = path.join(__dirname, '../../../src/prompts');
|
||||
|
||||
describe('Prompt Migration Validation', () => {
|
||||
const bannedPhrases = [
|
||||
'Respond ONLY with',
|
||||
'Return only the',
|
||||
'valid JSON',
|
||||
'Do not include any explanatory text',
|
||||
'Do not include any explanation',
|
||||
'code block markers'
|
||||
];
|
||||
|
||||
// Special cases where phrases are okay in different contexts
|
||||
const allowedContexts = {
|
||||
'markdown formatting': ['Use markdown formatting for better readability']
|
||||
};
|
||||
|
||||
test('prompts should not contain JSON formatting instructions', () => {
|
||||
const promptFiles = fs.readdirSync(promptsDir)
|
||||
.filter(file => file.endsWith('.json') && !file.includes('schema'))
|
||||
// Exclude update-subtask.json as it returns plain strings, not JSON
|
||||
.filter(file => file !== 'update-subtask.json');
|
||||
|
||||
promptFiles.forEach(file => {
|
||||
const content = fs.readFileSync(path.join(promptsDir, file), 'utf8');
|
||||
const promptData = JSON.parse(content);
|
||||
|
||||
bannedPhrases.forEach(phrase => {
|
||||
const lowerContent = content.toLowerCase();
|
||||
const lowerPhrase = phrase.toLowerCase();
|
||||
|
||||
if (lowerContent.includes(lowerPhrase)) {
|
||||
// Check if this phrase is allowed in its context
|
||||
const allowedInContext = allowedContexts[lowerPhrase];
|
||||
if (allowedInContext) {
|
||||
const isAllowed = allowedInContext.some(context =>
|
||||
lowerContent.includes(context.toLowerCase())
|
||||
);
|
||||
if (isAllowed) {
|
||||
return; // Skip this phrase - it's allowed in this context
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, the phrase is not allowed
|
||||
expect(lowerContent).not.toContain(lowerPhrase);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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: {}
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user