- 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.
268 lines
8.1 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|