/**
* get-full-lifecycle ErrorLogger Integration Tests
* Phase 4: LOW Priority ErrorLogger Implementation
*
* Tests error logging functionality for diagnostic display failures
*/
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import fs from '../../../src/utils/fs-extra-safe.js';
import path from 'path';
import * as os from 'os';
import { testUtils } from '../../utils/testUtils.js';
import { getFullLifecycle } from '../../../src/tools/get-full-lifecycle.js';
import { ServerConfig, GetFullLifecycleArgs } from '../../../src/types.js';
import { ErrorLogger } from '../../../src/logging/ErrorLogger.js';
// Mock the ErrorLogger
jest.mock('../../../src/logging/ErrorLogger.js');
describe('get-full-lifecycle ErrorLogger Integration', () => {
let config: ServerConfig;
let tempDir: string;
let testDir: string;
let mockErrorLogger: jest.Mocked<ErrorLogger>;
beforeEach(async () => {
// Create temporary directory for tests
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'get-lifecycle-error-test-'));
testDir = path.join(tempDir, 'comm', 'test-agent');
// Create mock ErrorLogger instance
mockErrorLogger = {
logError: jest.fn().mockImplementation(() => Promise.resolve()),
waitForWriteQueueEmpty: jest.fn().mockImplementation(() => Promise.resolve()),
close: jest.fn().mockImplementation(() => Promise.resolve())
} as unknown as jest.Mocked<ErrorLogger>;
config = testUtils.createMockConfig({
commDir: path.join(tempDir, 'comm'),
archiveDir: path.join(tempDir, 'comm', '.archive'),
enableArchiving: true,
errorLogger: mockErrorLogger
});
// Ensure directories exist
await fs.ensureDir(config.commDir);
await fs.ensureDir(testDir);
});
afterEach(async () => {
// Clean up test files
if (await fs.pathExists(tempDir)) {
await fs.remove(tempDir);
}
jest.clearAllMocks();
});
describe('Error Logging - Task Not Found', () => {
it('should log error when task directory does not exist', async () => {
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'non-existent-task',
include_progress: true
};
const result = await getFullLifecycle(config, args);
// Verify result indicates task not found
expect(result.taskId).toBe('non-existent-task');
expect(result.agent).toBe('test-agent');
expect(result.lifecycle.init.exists).toBe(false);
expect(result.lifecycle.plan.exists).toBe(false);
expect(result.lifecycle.outcome.type).toBe('pending');
expect(result.summary.final_status).toBe('error');
// Verify ErrorLogger was called with correct parameters
expect(mockErrorLogger.logError).toHaveBeenCalledWith(
expect.objectContaining({
source: 'tool_execution',
operation: 'get_full_lifecycle',
agent: 'test-agent',
taskId: 'non-existent-task',
error: expect.objectContaining({
message: expect.stringContaining('Task not found'),
name: 'TaskNotFoundError'
}),
context: expect.objectContaining({
tool: 'get_full_lifecycle',
parameters: expect.objectContaining({
agent: 'test-agent',
taskId: 'non-existent-task'
})
}),
severity: 'low'
})
);
});
});
describe('Error Logging - File Read Errors', () => {
it('should log error when INIT.md cannot be read', async () => {
const taskDir = path.join(testDir, 'task-with-read-error');
await fs.ensureDir(taskDir);
// Create INIT.md with restricted permissions (simulate read error)
const initPath = path.join(taskDir, 'INIT.md');
await fs.writeFile(initPath, 'Initial content');
// Mock fs.readFile to throw error for INIT.md
const originalReadFile = fs.readFile;
jest.spyOn(fs, 'readFile').mockImplementation(async (filePath: string, encoding?: string) => {
if (filePath === initPath) {
throw new Error('EACCES: permission denied');
}
return originalReadFile(filePath, encoding as BufferEncoding);
});
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'task-with-read-error',
include_progress: true
};
await expect(getFullLifecycle(config, args)).rejects.toThrow('EACCES');
// Verify ErrorLogger was called
expect(mockErrorLogger.logError).toHaveBeenCalledWith(
expect.objectContaining({
source: 'runtime',
operation: 'get_full_lifecycle',
agent: 'test-agent',
taskId: 'task-with-read-error',
error: expect.objectContaining({
message: expect.stringContaining('EACCES'),
name: 'Error'
}),
severity: 'low'
})
);
});
it('should log error when PLAN.md cannot be read', async () => {
const taskDir = path.join(testDir, 'task-with-plan-error');
await fs.ensureDir(taskDir);
// Create valid INIT.md
await fs.writeFile(path.join(taskDir, 'INIT.md'), '# Task Init');
// Create PLAN.md
const planPath = path.join(taskDir, 'PLAN.md');
await fs.writeFile(planPath, '# Task Plan');
// Mock fs.readFile to throw error for PLAN.md
jest.spyOn(fs, 'readFile').mockImplementation(async (filePath: string, _encoding?: string) => {
if (filePath === planPath) {
throw new Error('ENOENT: file not found');
}
// Return empty content for other files
return '';
});
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'task-with-plan-error',
include_progress: true
};
await expect(getFullLifecycle(config, args)).rejects.toThrow('ENOENT');
// Verify ErrorLogger was called
expect(mockErrorLogger.logError).toHaveBeenCalledWith(
expect.objectContaining({
source: 'runtime',
operation: 'get_full_lifecycle',
agent: 'test-agent',
taskId: 'task-with-plan-error',
error: expect.objectContaining({
message: expect.stringContaining('ENOENT'),
name: 'Error'
}),
severity: 'low'
})
);
});
});
describe('Error Logging - Progress Parsing Failures', () => {
it('should log error when progress markers parsing fails', async () => {
const taskDir = path.join(testDir, 'task-with-valid-progress');
await fs.ensureDir(taskDir);
// Create INIT.md
await fs.writeFile(path.join(taskDir, 'INIT.md'), '# Task Init');
// Create PLAN.md with valid checkboxes
const planContent = `# Task Plan
- [x] Step 1 completed
- [ ] Step 2 pending
- [ ] Step 3 pending
`;
await fs.writeFile(path.join(taskDir, 'PLAN.md'), planContent);
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'task-with-valid-progress',
include_progress: true
};
// The tool should handle checkboxes normally
const result = await getFullLifecycle(config, args);
// Should return result successfully
expect(result.lifecycle.plan.exists).toBe(true);
expect(result.taskId).toBe('task-with-valid-progress');
// Test passes if no errors occur during execution
// Progress marker behavior is tested in integration tests
});
});
describe('Error Logging - Lifecycle Reconstruction Errors', () => {
it('should log error when stat operations fail', async () => {
const taskDir = path.join(testDir, 'task-with-stat-error');
await fs.ensureDir(taskDir);
// Create files
await fs.writeFile(path.join(taskDir, 'INIT.md'), '# Task Init');
await fs.writeFile(path.join(taskDir, 'DONE.md'), '# Task Complete');
// Mock fileSystem.getStats instead of fs.stat since that's what the tool uses
const fileSystem = await import('../../../src/utils/file-system.js');
jest.spyOn(fileSystem, 'getStats').mockImplementation(async (filePath: string) => {
if (filePath.includes('DONE.md')) {
throw new Error('EPERM: operation not permitted');
}
// Return a mock stat object for other files
return {
isDirectory: () => false,
isFile: () => true,
mtime: new Date(),
size: 100
} as any;
});
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'task-with-stat-error',
include_progress: false
};
await expect(getFullLifecycle(config, args)).rejects.toThrow('EPERM');
// Verify ErrorLogger was called
expect(mockErrorLogger.logError).toHaveBeenCalledWith(
expect.objectContaining({
source: 'runtime',
operation: 'get_full_lifecycle',
agent: 'test-agent',
taskId: 'task-with-stat-error',
error: expect.objectContaining({
message: expect.stringContaining('EPERM'),
name: 'Error'
}),
severity: 'low'
})
);
});
});
describe('Error Logging - Validation Errors', () => {
it('should log error when agent parameter is missing', async () => {
const args = {
taskId: 'some-task',
include_progress: true
} as unknown as GetFullLifecycleArgs;
await expect(getFullLifecycle(config, args)).rejects.toThrow('agent must be a non-empty string');
// Verify ErrorLogger was called
expect(mockErrorLogger.logError).toHaveBeenCalledWith(
expect.objectContaining({
source: 'validation',
operation: 'get_full_lifecycle',
error: expect.objectContaining({
message: expect.stringContaining('agent must be a non-empty string'),
name: 'ValidationError'
}),
context: expect.objectContaining({
tool: 'get_full_lifecycle',
parameters: expect.objectContaining({
taskId: 'some-task'
})
}),
severity: 'low'
})
);
});
it('should log error when taskId parameter is missing', async () => {
const args = {
agent: 'test-agent',
include_progress: true
} as unknown as GetFullLifecycleArgs;
await expect(getFullLifecycle(config, args)).rejects.toThrow('taskId must be a non-empty string');
// Verify ErrorLogger was called
expect(mockErrorLogger.logError).toHaveBeenCalledWith(
expect.objectContaining({
source: 'validation',
operation: 'get_full_lifecycle',
agent: 'test-agent',
error: expect.objectContaining({
message: expect.stringContaining('taskId must be a non-empty string'),
name: 'ValidationError'
}),
context: expect.objectContaining({
tool: 'get_full_lifecycle',
parameters: expect.objectContaining({
agent: 'test-agent'
})
}),
severity: 'low'
})
);
});
});
describe('Debug Package Integration', () => {
it('should use correct debug namespace', async () => {
// The tool should use debug namespace: agent-comm:tools:getfulllifecycle
// This is verified by the import statement in the tool
const taskDir = path.join(testDir, 'debug-test-task');
await fs.ensureDir(taskDir);
await fs.writeFile(path.join(taskDir, 'INIT.md'), '# Init');
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'debug-test-task',
include_progress: false
};
const result = await getFullLifecycle(config, args);
// Verify successful execution - this validates the debug namespace is working
expect(result.lifecycle.init.exists).toBe(true);
expect(result.taskId).toBe('debug-test-task');
expect(result.agent).toBe('test-agent');
// No errors should be logged for successful operation
expect(mockErrorLogger.logError).not.toHaveBeenCalled();
});
});
describe('Error Severity Verification', () => {
it('should always use LOW severity for all errors', async () => {
const args: GetFullLifecycleArgs = {
agent: 'test-agent',
taskId: 'non-existent',
include_progress: true
};
await getFullLifecycle(config, args);
// All errors should have LOW severity
if (mockErrorLogger.logError.mock.calls.length > 0) {
mockErrorLogger.logError.mock.calls.forEach(call => {
const errorLog = call[0];
expect(errorLog.severity).toBe('low');
});
}
});
});
});