localgreenchain/__tests__/unit/agents/BaseAgent.test.ts
Claude 78b208b42a
feat: add comprehensive testing and CI/CD infrastructure (agent 5)
- Add GitHub Actions CI workflow with lint, type-check, test, build, and e2e jobs
- Configure Jest for unit and integration tests with coverage reporting
- Create unit tests for BaseAgent, PlantLineageAgent, and AgentOrchestrator
- Add blockchain PlantChain unit tests
- Create API integration tests for plants endpoints
- Configure Cypress for E2E testing with support files and custom commands
- Add E2E tests for home, plant registration, transparency, and vertical farm pages
- Set up Prettier for code formatting with configuration
- Configure Husky pre-commit hooks with lint-staged
- Add commitlint for conventional commit message enforcement
- Update package.json with new scripts and dev dependencies

This implements Agent 5 (Testing & CI/CD) from the deployment plan.
2025-11-23 03:50:36 +00:00

268 lines
8.1 KiB
TypeScript

/**
* BaseAgent Tests
* Tests for the abstract base agent class
*/
import { BaseAgent } from '../../../lib/agents/BaseAgent';
import { AgentConfig, AgentTask, AgentStatus } from '../../../lib/agents/types';
// Concrete implementation for testing
class TestAgent extends BaseAgent {
public runOnceResult: AgentTask | null = null;
public runOnceError: Error | null = null;
public runOnceCallCount = 0;
constructor(config?: Partial<AgentConfig>) {
super({
id: 'test-agent',
name: 'Test Agent',
description: 'Agent for testing',
enabled: true,
intervalMs: 100,
priority: 'medium',
maxRetries: 3,
timeoutMs: 5000,
...config,
});
}
async runOnce(): Promise<AgentTask | null> {
this.runOnceCallCount++;
if (this.runOnceError) {
throw this.runOnceError;
}
return this.runOnceResult;
}
// Expose protected methods for testing
public testCreateAlert(
severity: 'info' | 'warning' | 'error' | 'critical',
title: string,
message: string
) {
return this.createAlert(severity, title, message);
}
public testHandleError(error: Error) {
return this.handleError(error);
}
public testAddTask(type: string, payload: Record<string, any>) {
return this.addTask(type, payload);
}
public testCreateTaskResult(
type: string,
status: 'completed' | 'failed',
result?: any
) {
return this.createTaskResult(type, status, result);
}
}
describe('BaseAgent', () => {
let agent: TestAgent;
beforeEach(() => {
agent = new TestAgent();
jest.clearAllMocks();
});
afterEach(async () => {
if (agent.status === 'running') {
await agent.stop();
}
});
describe('Initialization', () => {
it('should initialize with correct config', () => {
expect(agent.config.id).toBe('test-agent');
expect(agent.config.name).toBe('Test Agent');
expect(agent.config.enabled).toBe(true);
});
it('should initialize with idle status', () => {
expect(agent.status).toBe('idle');
});
it('should initialize with empty metrics', () => {
const metrics = agent.getMetrics();
expect(metrics.tasksCompleted).toBe(0);
expect(metrics.tasksFailed).toBe(0);
expect(metrics.averageExecutionMs).toBe(0);
expect(metrics.errors).toEqual([]);
});
it('should initialize with empty alerts', () => {
expect(agent.getAlerts()).toEqual([]);
});
});
describe('Lifecycle', () => {
it('should start and update status to running', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
expect(agent.status).toBe('running');
});
it('should not start twice', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
const firstCallCount = agent.runOnceCallCount;
await agent.start(); // Second call
expect(agent.runOnceCallCount).toBe(firstCallCount);
});
it('should stop and update status to idle', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
await agent.stop();
expect(agent.status).toBe('idle');
});
it('should pause and resume', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
agent.pause();
expect(agent.status).toBe('paused');
agent.resume();
expect(agent.status).toBe('running');
});
it('should not pause if not running', () => {
agent.pause();
expect(agent.status).toBe('idle');
});
it('should not resume if not paused', () => {
agent.resume();
expect(agent.status).toBe('idle');
});
});
describe('Task Execution', () => {
it('should run task on start', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
expect(agent.runOnceCallCount).toBe(1);
await agent.stop();
});
it('should increment tasksCompleted on success', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed', { data: 'test' });
await agent.start();
await agent.stop();
expect(agent.getMetrics().tasksCompleted).toBe(1);
});
it('should increment tasksFailed on failure', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'failed', null);
await agent.start();
await agent.stop();
expect(agent.getMetrics().tasksFailed).toBe(1);
});
it('should update lastRunAt after execution', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
await agent.stop();
expect(agent.getMetrics().lastRunAt).not.toBeNull();
});
});
describe('Error Handling', () => {
it('should handle thrown errors', async () => {
agent.runOnceError = new Error('Test error');
await agent.start();
await agent.stop();
const metrics = agent.getMetrics();
expect(metrics.errors.length).toBe(1);
expect(metrics.errors[0].message).toBe('Test error');
});
it('should record error timestamp', async () => {
agent.testHandleError(new Error('Test error'));
const metrics = agent.getMetrics();
expect(metrics.lastErrorAt).not.toBeNull();
});
it('should limit errors to 50', () => {
for (let i = 0; i < 60; i++) {
agent.testHandleError(new Error(`Error ${i}`));
}
expect(agent.getMetrics().errors.length).toBe(50);
});
});
describe('Alerts', () => {
it('should create alerts with correct structure', () => {
const alert = agent.testCreateAlert('warning', 'Test Alert', 'Test message');
expect(alert.id).toBeDefined();
expect(alert.severity).toBe('warning');
expect(alert.title).toBe('Test Alert');
expect(alert.message).toBe('Test message');
expect(alert.acknowledged).toBe(false);
});
it('should add alerts to the list', () => {
agent.testCreateAlert('info', 'Alert 1', 'Message 1');
agent.testCreateAlert('warning', 'Alert 2', 'Message 2');
expect(agent.getAlerts().length).toBe(2);
});
it('should limit alerts to 100', () => {
for (let i = 0; i < 110; i++) {
agent.testCreateAlert('info', `Alert ${i}`, 'Message');
}
expect(agent.getAlerts().length).toBe(100);
});
});
describe('Metrics', () => {
it('should calculate uptime when running', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
await new Promise((resolve) => setTimeout(resolve, 50));
const metrics = agent.getMetrics();
expect(metrics.uptime).toBeGreaterThan(0);
await agent.stop();
});
it('should calculate average execution time', async () => {
agent.runOnceResult = agent.testCreateTaskResult('test', 'completed');
await agent.start();
await agent.stop();
const metrics = agent.getMetrics();
expect(metrics.averageExecutionMs).toBeGreaterThanOrEqual(0);
});
});
describe('Task Queue', () => {
it('should add tasks with unique IDs', () => {
const task1 = agent.testAddTask('type1', { key: 'value1' });
const task2 = agent.testAddTask('type2', { key: 'value2' });
expect(task1.id).not.toBe(task2.id);
});
it('should set correct task properties', () => {
const task = agent.testAddTask('test-type', { data: 'value' });
expect(task.type).toBe('test-type');
expect(task.payload).toEqual({ data: 'value' });
expect(task.status).toBe('pending');
expect(task.agentId).toBe('test-agent');
});
});
describe('Task Results', () => {
it('should create completed task result', () => {
const result = agent.testCreateTaskResult('scan', 'completed', { count: 10 });
expect(result.status).toBe('completed');
expect(result.result).toEqual({ count: 10 });
});
it('should create failed task result', () => {
const result = agent.testCreateTaskResult('scan', 'failed', null);
expect(result.status).toBe('failed');
});
});
});