/**
* @fileoverview Test suite for submit-plan tool
*/
import { jest } from '@jest/globals';
import * as fs from '../../../src/utils/fs-extra-safe.js';
import { submitPlan } from '../../../src/tools/submit-plan.js';
import { TaskContextManager, PlanSubmissionResult } from '../../../src/core/TaskContextManager.js';
import { AgentCommError, ServerConfig } from '../../../src/types.js';
import { ConnectionManager } from '../../../src/core/ConnectionManager.js';
import { EventLogger } from '../../../src/logging/EventLogger.js';
import path from 'path';
// Mock dependencies
jest.mock('../../../src/core/TaskContextManager.js');
jest.mock('../../../src/utils/fs-extra-safe.js');
const MockedTaskContextManager = TaskContextManager as jest.MockedClass<typeof TaskContextManager>;
describe('submit-plan tool', () => {
const mockConfig = {
commDir: '/test/comm',
archiveDir: '/test/archive',
logDir: '/test/logs',
enableArchiving: false,
connectionManager: {
register: jest.fn(),
getConnection: jest.fn(),
updateActivity: jest.fn(),
getActiveConnections: jest.fn(),
unregister: jest.fn(),
getConnectionsByAgent: jest.fn(),
cleanupStaleConnections: jest.fn(),
getStatistics: jest.fn(),
getConnectionCount: jest.fn(),
hasConnection: jest.fn()
} as unknown as ConnectionManager,
eventLogger: {
logOperation: jest.fn(),
logError: jest.fn(),
getOperationStatistics: jest.fn()
} as unknown as EventLogger
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should submit plan successfully', async () => {
const mockResult: PlanSubmissionResult = {
success: true,
message: 'Plan submitted successfully',
stepsIdentified: 5,
phases: 3,
initialProgress: {
completed: 0,
inProgress: 1,
pending: 4,
blocked: 0
}
};
const mockInstance = {
submitPlan: jest.fn().mockResolvedValue(mockResult as never)
};
(MockedTaskContextManager as unknown as jest.Mock).mockImplementation(() => mockInstance);
const args = {
content: `# Test Plan
## Phase 1
- [ ] **Setup Environment**: Configure test environment
- Action: Install dependencies
- Expected: All packages installed
- Error: Check package.json for conflicts
- [ ] **Run Tests**: Execute test suite
- Action: Run npm test
- Expected: All tests pass
- Error: Fix failing tests`,
agent: 'test-agent',
stepCount: 2 // 2 checkboxes in the plan
};
const result = await submitPlan(mockConfig, args);
expect(result).toEqual(mockResult);
expect(MockedTaskContextManager).toHaveBeenCalledWith(expect.objectContaining({
commDir: mockConfig.commDir
}));
expect(mockInstance.submitPlan).toHaveBeenCalledWith(
expect.stringContaining('# Test Plan'),
expect.objectContaining({
agent: 'test-agent'
})
);
});
it('should throw error for missing content', async () => {
const args = {
agent: 'test-agent'
// content missing
};
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(AgentCommError);
});
it('should throw error for missing agent', async () => {
const args = {
content: '# Test Plan'
// agent missing
};
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(AgentCommError);
});
it('should throw error for plan with no checkboxes', async () => {
const args = {
content: `# Test Plan
This plan has no checkboxes at all.
Just some regular text without any trackable items.`,
agent: 'test-agent'
};
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(/Plan must include at least ONE trackable item/);
});
it('should throw error for plan with forbidden status markers', async () => {
const args = {
content: `# Test Plan
- [ ] **Valid Task**: This is valid
- Action: Do something
- Expected: Success
[PENDING] This should not be here
[COMPLETE] Neither should this`,
agent: 'test-agent'
};
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(/Use checkbox format only.*Remove these status markers/);
});
it('should throw error for checkbox without detail points', async () => {
const args = {
content: `# Test Plan
- [ ] **Missing Details**: This checkbox has no detail points
Some regular text here that is not a detail point.
Another paragraph that doesn't count as details.`,
agent: 'test-agent'
};
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(/Checkbox "Missing Details" missing required detail points/);
});
it('should handle missing configuration components', async () => {
const badConfig = {
commDir: '/test/comm',
archiveDir: '/test/archive',
logDir: '/test/logs',
enableArchiving: false
// Missing connectionManager and eventLogger
};
const args = {
content: `# Test Plan
- [ ] **Test Task**: Test configuration validation
- Action: Validate config
- Expected: Config validation should happen after format validation
- Error: Show configuration error`,
agent: 'test-agent'
};
await expect(submitPlan(badConfig as unknown as ServerConfig, args))
.rejects.toThrow('Configuration missing required components');
});
describe('stepCount parameter tests', () => {
const mockedFs = fs as jest.Mocked<typeof fs>;
const mockTaskPath = '/test/comm/test-agent/test-task';
beforeEach(() => {
jest.clearAllMocks();
// Mock file system operations for metadata
mockedFs.ensureDir.mockResolvedValue(undefined);
mockedFs.writeFile.mockResolvedValue(undefined);
mockedFs.pathExists.mockResolvedValue(true);
});
it('should accept and validate correct stepCount', async () => {
const mockResult: PlanSubmissionResult = {
success: true,
message: 'Plan submitted successfully',
stepsIdentified: 3,
phases: 1,
initialProgress: {
completed: 0,
inProgress: 0,
pending: 3,
blocked: 0
}
};
const mockInstance = {
submitPlan: jest.fn().mockResolvedValue(mockResult as never),
getActiveTaskPath: jest.fn().mockReturnValue(mockTaskPath)
};
(MockedTaskContextManager as unknown as jest.Mock).mockImplementation(() => mockInstance);
const args = {
content: `# Test Plan with Step Count
## Implementation Steps
- [ ] **First Step**: Do something first
- Action: Execute first action
- Expected: First result achieved
- [ ] **Second Step**: Do something second
- Action: Execute second action
- Expected: Second result achieved
- [ ] **Third Step**: Do something third
- Action: Execute third action
- Expected: Third result achieved`,
agent: 'test-agent',
stepCount: 3, // Correct count
taskId: 'test-task' // Match expected path
};
const result = await submitPlan(mockConfig, args);
expect(result).toEqual(mockResult);
// Verify metadata file was written
expect(mockedFs.writeFile).toHaveBeenCalledWith(
path.join(mockTaskPath, 'PLAN.metadata.json'),
expect.stringContaining('"stepCount": 3')
);
});
it('should reject incorrect stepCount', async () => {
const args = {
content: `# Test Plan with Wrong Step Count
## Implementation Steps
- [ ] **First Step**: Do something first
- Action: Execute first action
- Expected: First result achieved
- [ ] **Second Step**: Do something second
- Action: Execute second action
- Expected: Second result achieved`,
agent: 'test-agent',
stepCount: 5 // Wrong count - actual is 2
};
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(/Step count mismatch: expected 5, actual 2/);
});
it('should work without stepCount parameter (backward compatibility)', async () => {
const mockResult: PlanSubmissionResult = {
success: true,
message: 'Plan submitted successfully',
stepsIdentified: 2,
phases: 1,
initialProgress: {
completed: 0,
inProgress: 0,
pending: 2,
blocked: 0
}
};
const mockInstance = {
submitPlan: jest.fn().mockResolvedValue(mockResult as never),
getActiveTaskPath: jest.fn().mockReturnValue(mockTaskPath)
};
(MockedTaskContextManager as unknown as jest.Mock).mockImplementation(() => mockInstance);
const args = {
content: `# Test Plan without Step Count
## Implementation Steps
- [ ] **First Step**: Do something first
- Action: Execute first action
- Expected: First result achieved
- [ ] **Second Step**: Do something second
- Action: Execute second action
- Expected: Second result achieved`,
agent: 'test-agent',
taskId: 'test-task' // Match expected path
// No stepCount provided
};
const result = await submitPlan(mockConfig, args);
expect(result).toEqual(mockResult);
// Should still create metadata file with calculated count
expect(mockedFs.writeFile).toHaveBeenCalledWith(
path.join(mockTaskPath, 'PLAN.metadata.json'),
expect.stringContaining('"stepCount": 2')
);
});
it('should handle stepCount of 0 correctly', async () => {
const args = {
content: `# Empty Plan
No checkboxes here, just text.`,
agent: 'test-agent',
stepCount: 0
};
// Should fail because plan has no checkboxes
await expect(submitPlan(mockConfig, args))
.rejects.toThrow(/Plan must include at least ONE trackable item/);
});
it('should create metadata with all required fields', async () => {
const mockResult: PlanSubmissionResult = {
success: true,
message: 'Plan submitted successfully',
stepsIdentified: 1,
phases: 1,
initialProgress: {
completed: 0,
inProgress: 0,
pending: 1,
blocked: 0
}
};
const mockInstance = {
submitPlan: jest.fn().mockResolvedValue(mockResult as never),
getActiveTaskPath: jest.fn().mockReturnValue(mockTaskPath)
};
(MockedTaskContextManager as unknown as jest.Mock).mockImplementation(() => mockInstance);
const args = {
content: `# Test Plan
- [ ] **Single Step**: Do something
- Action: Execute action
- Expected: Result achieved`,
agent: 'test-agent',
stepCount: 1,
taskId: 'test-task' // Match expected path
};
const result = await submitPlan(mockConfig, args);
expect(result).toEqual(mockResult);
// Verify metadata structure
const expectedMetadata = {
stepCount: 1,
agent: 'test-agent',
taskId: 'test-task',
checkboxPattern: 'markdown',
version: '2.0.0'
};
const writeCall = mockedFs.writeFile.mock.calls.find(
call => call[0] === path.join(mockTaskPath, 'PLAN.metadata.json')
);
expect(writeCall).toBeDefined();
if (writeCall) {
const writtenContent = JSON.parse(writeCall[1] as string);
expect(writtenContent.stepCount).toBe(expectedMetadata.stepCount);
expect(writtenContent.agent).toBe(expectedMetadata.agent);
expect(writtenContent.taskId).toBe(expectedMetadata.taskId);
expect(writtenContent.checkboxPattern).toBe(expectedMetadata.checkboxPattern);
expect(writtenContent.version).toBe(expectedMetadata.version);
expect(writtenContent.createdAt).toBeDefined();
}
});
it('should log performance metrics when operation takes too long', async () => {
// This test would check debug logging but that's implementation-specific
// We'll test the actual validation performance in the implementation
expect(true).toBe(true);
});
});
});