Add REST API endpoints for the PlantLineageAgent: - GET /api/agents/lineage - Agent status and network statistics - POST /api/agents/lineage - Start/stop/pause/resume agent - GET /api/agents/lineage/[plantId] - Lineage analysis for specific plant - GET /api/agents/lineage/anomalies - List detected lineage anomalies Includes comprehensive test suite with 25 passing tests.
259 lines
8 KiB
TypeScript
259 lines
8 KiB
TypeScript
/**
|
|
* PlantLineageAgent API Tests
|
|
* Tests for lineage agent API endpoints
|
|
*/
|
|
|
|
import { PlantLineageAgent, getPlantLineageAgent } from '../../../lib/agents';
|
|
import { getBlockchain, initializeBlockchain } from '../../../lib/blockchain/manager';
|
|
|
|
describe('PlantLineageAgent API', () => {
|
|
let agent: PlantLineageAgent;
|
|
|
|
beforeEach(() => {
|
|
// Get fresh agent instance
|
|
agent = getPlantLineageAgent();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Ensure agent is stopped after each test
|
|
if (agent.status === 'running') {
|
|
await agent.stop();
|
|
}
|
|
});
|
|
|
|
describe('GET /api/agents/lineage', () => {
|
|
it('should return agent status and configuration', () => {
|
|
expect(agent.config.id).toBe('plant-lineage-agent');
|
|
expect(agent.config.name).toBe('Plant Lineage Agent');
|
|
expect(agent.config.description).toBeDefined();
|
|
expect(agent.config.priority).toBe('high');
|
|
expect(agent.config.intervalMs).toBe(60000);
|
|
});
|
|
|
|
it('should return current metrics', () => {
|
|
const metrics = agent.getMetrics();
|
|
|
|
expect(metrics.agentId).toBe('plant-lineage-agent');
|
|
expect(metrics.tasksCompleted).toBeGreaterThanOrEqual(0);
|
|
expect(metrics.tasksFailed).toBeGreaterThanOrEqual(0);
|
|
expect(metrics.averageExecutionMs).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should return network statistics', () => {
|
|
const networkStats = agent.getNetworkStats();
|
|
|
|
expect(networkStats.totalPlants).toBeGreaterThanOrEqual(0);
|
|
expect(networkStats.totalLineages).toBeGreaterThanOrEqual(0);
|
|
expect(networkStats.avgGenerationDepth).toBeGreaterThanOrEqual(0);
|
|
expect(networkStats.avgLineageSize).toBeGreaterThanOrEqual(0);
|
|
expect(networkStats.geographicSpread).toBeGreaterThanOrEqual(0);
|
|
});
|
|
|
|
it('should return anomaly summary', () => {
|
|
const anomalies = agent.getAnomalies();
|
|
|
|
expect(Array.isArray(anomalies)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('POST /api/agents/lineage', () => {
|
|
it('should start agent successfully', async () => {
|
|
expect(agent.status).toBe('idle');
|
|
|
|
await agent.start();
|
|
|
|
expect(agent.status).toBe('running');
|
|
});
|
|
|
|
it('should stop agent successfully', async () => {
|
|
await agent.start();
|
|
expect(agent.status).toBe('running');
|
|
|
|
await agent.stop();
|
|
|
|
expect(agent.status).toBe('idle');
|
|
});
|
|
|
|
it('should pause and resume agent', async () => {
|
|
await agent.start();
|
|
expect(agent.status).toBe('running');
|
|
|
|
agent.pause();
|
|
expect(agent.status).toBe('paused');
|
|
|
|
agent.resume();
|
|
expect(agent.status).toBe('running');
|
|
});
|
|
|
|
it('should handle start when already running', async () => {
|
|
await agent.start();
|
|
const firstStatus = agent.status;
|
|
|
|
await agent.start(); // Should not throw
|
|
|
|
expect(agent.status).toBe(firstStatus);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/agents/lineage/[plantId]', () => {
|
|
it('should return null for non-existent plant analysis', () => {
|
|
const analysis = agent.getLineageAnalysis('non-existent-plant-id');
|
|
|
|
expect(analysis).toBeNull();
|
|
});
|
|
|
|
it('should return analysis structure when available', () => {
|
|
// Analysis would be populated after agent runs
|
|
// For now, test the structure expectations
|
|
const analysis = agent.getLineageAnalysis('test-plant-id');
|
|
|
|
// Should return null for non-cached plant
|
|
expect(analysis).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('GET /api/agents/lineage/anomalies', () => {
|
|
it('should return empty array when no anomalies', () => {
|
|
const anomalies = agent.getAnomalies();
|
|
|
|
expect(Array.isArray(anomalies)).toBe(true);
|
|
});
|
|
|
|
it('should support filtering by severity', () => {
|
|
const allAnomalies = agent.getAnomalies();
|
|
|
|
const highSeverity = allAnomalies.filter(a => a.severity === 'high');
|
|
const mediumSeverity = allAnomalies.filter(a => a.severity === 'medium');
|
|
const lowSeverity = allAnomalies.filter(a => a.severity === 'low');
|
|
|
|
expect(highSeverity.length + mediumSeverity.length + lowSeverity.length).toBe(allAnomalies.length);
|
|
});
|
|
|
|
it('should support filtering by type', () => {
|
|
const allAnomalies = agent.getAnomalies();
|
|
const validTypes = ['orphan', 'circular', 'invalid_generation', 'missing_parent', 'suspicious_location'];
|
|
|
|
for (const anomaly of allAnomalies) {
|
|
expect(validTypes).toContain(anomaly.type);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Agent Lifecycle', () => {
|
|
it('should track uptime correctly', async () => {
|
|
const initialMetrics = agent.getMetrics();
|
|
const initialUptime = initialMetrics.uptime;
|
|
|
|
await agent.start();
|
|
|
|
// Wait a bit
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
const runningMetrics = agent.getMetrics();
|
|
expect(runningMetrics.uptime).toBeGreaterThan(initialUptime);
|
|
});
|
|
|
|
it('should maintain metrics across start/stop cycles', async () => {
|
|
await agent.start();
|
|
await agent.stop();
|
|
|
|
const metrics = agent.getMetrics();
|
|
expect(metrics.uptime).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('Alert Management', () => {
|
|
it('should return alerts array', () => {
|
|
const alerts = agent.getAlerts();
|
|
|
|
expect(Array.isArray(alerts)).toBe(true);
|
|
});
|
|
|
|
it('should have proper alert structure', () => {
|
|
const alerts = agent.getAlerts();
|
|
|
|
for (const alert of alerts) {
|
|
expect(alert.id).toBeDefined();
|
|
expect(alert.agentId).toBe('plant-lineage-agent');
|
|
expect(alert.severity).toBeDefined();
|
|
expect(alert.title).toBeDefined();
|
|
expect(alert.message).toBeDefined();
|
|
expect(alert.timestamp).toBeDefined();
|
|
expect(typeof alert.acknowledged).toBe('boolean');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle agent operations gracefully', async () => {
|
|
// Test that agent doesn't throw on basic operations
|
|
expect(() => agent.getMetrics()).not.toThrow();
|
|
expect(() => agent.getAnomalies()).not.toThrow();
|
|
expect(() => agent.getNetworkStats()).not.toThrow();
|
|
expect(() => agent.getAlerts()).not.toThrow();
|
|
});
|
|
|
|
it('should handle pause on non-running agent', () => {
|
|
expect(agent.status).toBe('idle');
|
|
|
|
agent.pause(); // Should not throw
|
|
|
|
// Status should remain idle (only pauses if running)
|
|
expect(agent.status).toBe('idle');
|
|
});
|
|
|
|
it('should handle resume on non-paused agent', () => {
|
|
expect(agent.status).toBe('idle');
|
|
|
|
agent.resume(); // Should not throw
|
|
|
|
// Status should remain idle (only resumes if paused)
|
|
expect(agent.status).toBe('idle');
|
|
});
|
|
});
|
|
|
|
describe('Response Format', () => {
|
|
it('should return consistent metrics format', () => {
|
|
const metrics = agent.getMetrics();
|
|
|
|
expect(typeof metrics.agentId).toBe('string');
|
|
expect(typeof metrics.tasksCompleted).toBe('number');
|
|
expect(typeof metrics.tasksFailed).toBe('number');
|
|
expect(typeof metrics.averageExecutionMs).toBe('number');
|
|
expect(typeof metrics.uptime).toBe('number');
|
|
expect(Array.isArray(metrics.errors)).toBe(true);
|
|
});
|
|
|
|
it('should return consistent network stats format', () => {
|
|
const stats = agent.getNetworkStats();
|
|
|
|
expect(typeof stats.totalPlants).toBe('number');
|
|
expect(typeof stats.totalLineages).toBe('number');
|
|
expect(typeof stats.avgGenerationDepth).toBe('number');
|
|
expect(typeof stats.avgLineageSize).toBe('number');
|
|
expect(typeof stats.geographicSpread).toBe('number');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('PlantLineageAgent Integration', () => {
|
|
it('should be accessible via singleton', () => {
|
|
const agent1 = getPlantLineageAgent();
|
|
const agent2 = getPlantLineageAgent();
|
|
|
|
expect(agent1).toBe(agent2);
|
|
});
|
|
|
|
it('should have correct priority', () => {
|
|
const agent = getPlantLineageAgent();
|
|
|
|
expect(agent.config.priority).toBe('high');
|
|
});
|
|
|
|
it('should have correct interval', () => {
|
|
const agent = getPlantLineageAgent();
|
|
|
|
// Should run every minute
|
|
expect(agent.config.intervalMs).toBe(60000);
|
|
});
|
|
});
|