task-master-neo-sdlc
Version:
Enhanced task management system with Neo SDLC agents and MCP tools for comprehensive, AI-driven software development lifecycle management.
198 lines (167 loc) • 9.63 kB
JavaScript
import { ProcessManagerAgent } from '../process-manager';
import { KnowledgeGraph } from '../../knowledge-graph'; // Adjust path
import { AgentWorkflowSystem } from '../../agent-workflow'; // Adjust path
// Mocks
const mockKnowledgeGraph = {
addNode: jest.fn(),
findNodes: jest.fn(),
updateContext: jest.fn()
};
const mockWorkflowSystem = {
executeAction: jest.fn()
};
describe('ProcessManagerAgent', () => {
let agent;
beforeEach(() => {
jest.clearAllMocks();
agent = new ProcessManagerAgent(mockKnowledgeGraph, mockWorkflowSystem);
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, 'error').mockImplementation(() => {});
// Use fake timers to control async execution in tests
jest.useFakeTimers();
});
afterEach(() => {
console.log.mockRestore();
console.error.mockRestore();
jest.useRealTimers();
});
it('should start a process and add to KG', async () => {
const workflowId = 'test-workflow';
const initialContext = { inputData: 'test' };
mockKnowledgeGraph.addNode.mockResolvedValue(undefined);
// Spy on executeStep to check if it's called
const executeStepSpy = jest.spyOn(agent, 'executeStep').mockResolvedValue(undefined);
const processId = await agent.startProcess(workflowId, initialContext);
expect(processId).toMatch(/^process_/);
expect(agent.runningProcesses.has(processId)).toBe(true);
const processState = agent.runningProcesses.get(processId);
expect(processState.definitionId).toBe(workflowId);
expect(processState.status).toBe('running');
expect(processState.currentStepId).toBe('step1'); // Based on placeholder definition
expect(processState.context).toEqual(initialContext);
expect(mockKnowledgeGraph.addNode).toHaveBeenCalledWith(expect.objectContaining({
id: `processInstance:${processId}`,
type: 'process_instance',
data: expect.objectContaining({ definitionId: workflowId, status: 'running' })
}));
// Check that executeStep was scheduled
expect(executeStepSpy).toHaveBeenCalledWith(processId);
executeStepSpy.mockRestore();
});
it('should throw error if workflow definition is invalid', async () => {
// Mock workflowLoader or definition loading logic if implemented
// For now, assumes placeholder definition fails if workflowId is specific
await expect(agent.startProcess('invalid-def')).rejects.toThrow(
'Workflow definition invalid-def is invalid or empty.'
);
});
it('should execute steps sequentially until completion', async () => {
const workflowId = 'sequential-workflow';
mockKnowledgeGraph.addNode.mockResolvedValue(undefined);
mockKnowledgeGraph.updateContext.mockResolvedValue(undefined);
// Mock the action execution
mockWorkflowSystem.executeAction
.mockResolvedValueOnce({ output: 'result-step1' })
.mockResolvedValueOnce({ output: 'result-step2' });
const processId = await agent.startProcess(workflowId);
// Allow async operations triggered by startProcess to run
await jest.runAllImmediates(); // Runs setImmediate
await jest.runAllTimers(); // If any timers were used
// Check step 1 execution (assuming executeStep was called by startProcess)
expect(mockWorkflowSystem.executeAction).toHaveBeenCalledWith('agentA.doSomething', undefined, {}); // Check first call
let processState = await agent.getProcessStatus(processId);
expect(processState.context.step1).toEqual({ output: 'result-step1' });
expect(processState.currentStepId).toBe('step2');
expect(processState.history.some(h => h.stepId === 'step1' && h.status === 'completed')).toBe(true);
expect(mockKnowledgeGraph.updateContext).toHaveBeenCalledWith(expect.objectContaining({
id: `processInstance:${processId}`,
data: expect.objectContaining({ currentStepId: 'step2' })
}));
// Allow next step triggered by previous step completion
await jest.runAllImmediates();
await jest.runAllTimers();
// Check step 2 execution
expect(mockWorkflowSystem.executeAction).toHaveBeenCalledWith('agentB.doAnotherThing', { input: '${step1.output}' }, expect.any(Object)); // Check second call
processState = await agent.getProcessStatus(processId);
// Process should be removed from running map upon completion
expect(agent.runningProcesses.has(processId)).toBe(false);
// Check final state in KG (mocking findNodes for getProcessStatus fallback)
mockKnowledgeGraph.findNodes.mockResolvedValue([{ id: `processInstance:${processId}`, data: { status: 'completed' } }]);
const finalState = await agent.getProcessStatus(processId);
expect(finalState.status).toBe('completed');
expect(mockKnowledgeGraph.updateContext).toHaveBeenCalledWith(expect.objectContaining({
id: `processInstance:${processId}`,
data: expect.objectContaining({ status: 'completed' })
}));
});
it('should handle execution errors and fail the process', async () => {
const workflowId = 'error-workflow';
const error = new Error('Action failed!');
mockKnowledgeGraph.addNode.mockResolvedValue(undefined);
mockKnowledgeGraph.updateContext.mockResolvedValue(undefined);
mockWorkflowSystem.executeAction.mockRejectedValue(error); // First action fails
const processId = await agent.startProcess(workflowId);
await jest.runAllImmediates();
await jest.runAllTimers();
expect(mockWorkflowSystem.executeAction).toHaveBeenCalledTimes(1);
expect(agent.runningProcesses.has(processId)).toBe(false);
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Error executing step step1'), error);
// Check final state in KG
mockKnowledgeGraph.findNodes.mockResolvedValue([{ id: `processInstance:${processId}`, data: { status: 'failed' } }]);
const finalState = await agent.getProcessStatus(processId);
expect(finalState.status).toBe('failed');
expect(mockKnowledgeGraph.updateContext).toHaveBeenCalledWith(expect.objectContaining({
id: `processInstance:${processId}`,
data: expect.objectContaining({ status: 'failed', history: expect.arrayContaining([expect.objectContaining({ status: 'failed' })]) })
}));
});
it('should handle missing step definitions and fail the process', async () => {
const workflowId = 'missing-step-def';
mockKnowledgeGraph.addNode.mockResolvedValue(undefined);
mockKnowledgeGraph.updateContext.mockResolvedValue(undefined);
// Modify the agent's internal logic or mock definition loading to simulate a missing step
const originalExecuteStep = agent.executeStep;
agent.executeStep = async (instanceId) => {
const processState = agent.runningProcesses.get(instanceId);
if (processState && processState.currentStepId === 'step1') {
// Simulate step1 completion but step2 definition is missing
processState.context.step1 = { output: 'data' };
processState.currentStepId = 'step2'; // Next step doesn't exist in placeholder def
await agent._updateProcessStateInKG(instanceId, processState);
setImmediate(() => originalExecuteStep.call(agent, instanceId));
} else {
await originalExecuteStep.call(agent, instanceId);
}
};
const processId = await agent.startProcess(workflowId);
await jest.runAllImmediates();
await jest.runAllTimers(); // First simulated step completes
await jest.runAllImmediates(); // Second call to executeStep occurs
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Current step step2 not found'));
expect(agent.runningProcesses.has(processId)).toBe(false);
mockKnowledgeGraph.findNodes.mockResolvedValue([{ id: `processInstance:${processId}`, data: { status: 'failed' } }]);
const finalState = await agent.getProcessStatus(processId);
expect(finalState.status).toBe('failed');
expect(mockKnowledgeGraph.updateContext).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({ status: 'failed', history: expect.arrayContaining([expect.objectContaining({ details: 'Step definition not found' })]) })
}));
});
it('should get process status for running and historical processes', async () => {
const runningProcessId = 'process_running';
const historicalProcessId = 'process_historical';
const runningState = { id: runningProcessId, status: 'running', currentStepId: 'stepX' };
const historicalState = { id: historicalProcessId, status: 'completed', currentStepId: null };
agent.runningProcesses.set(runningProcessId, runningState);
mockKnowledgeGraph.findNodes
.mockResolvedValueOnce([]) // Running process not found in KG history yet
.mockResolvedValueOnce([{ id: `processInstance:${historicalProcessId}`, data: historicalState }]); // Historical found in KG
const statusRunning = await agent.getProcessStatus(runningProcessId);
const statusHistorical = await agent.getProcessStatus(historicalProcessId);
const statusNotFound = await agent.getProcessStatus('not_a_process');
expect(statusRunning).toEqual(runningState);
expect(statusHistorical).toEqual(historicalState);
expect(statusNotFound).toBeNull();
expect(mockKnowledgeGraph.findNodes).toHaveBeenCalledWith({ id: `processInstance:${historicalProcessId}` });
expect(mockKnowledgeGraph.findNodes).toHaveBeenCalledWith({ id: `processInstance:not_a_process` });
});
});