/** * PlantLineageAgent Tests * Tests for the plant lineage tracking agent */ import { PlantLineageAgent, getPlantLineageAgent } from '../../../lib/agents/PlantLineageAgent'; // Mock the blockchain manager jest.mock('../../../lib/blockchain/manager', () => ({ getBlockchain: jest.fn(() => ({ getChain: jest.fn(() => [ // Genesis block { index: 0, timestamp: '2024-01-01T00:00:00Z', plant: { id: 'genesis' }, previousHash: '0', hash: 'genesis-hash', nonce: 0, }, // First generation plant { index: 1, timestamp: '2024-01-02T00:00:00Z', plant: { id: 'plant-1', name: 'Original Tomato', species: 'Tomato', variety: 'Cherry', generation: 1, propagationType: 'original', parentPlantId: undefined, childPlants: ['plant-2'], status: 'thriving', dateAcquired: '2024-01-02', location: { latitude: 40.7128, longitude: -74.006 }, environment: { light: 'full_sun' }, growthMetrics: { height: 50 }, }, previousHash: 'genesis-hash', hash: 'hash-1', nonce: 1, }, // Second generation plant { index: 2, timestamp: '2024-01-15T00:00:00Z', plant: { id: 'plant-2', name: 'Cloned Tomato', species: 'Tomato', variety: 'Cherry', generation: 2, propagationType: 'cutting', parentPlantId: 'plant-1', childPlants: ['plant-3'], status: 'healthy', dateAcquired: '2024-01-15', location: { latitude: 40.73, longitude: -73.99 }, environment: { light: 'partial_sun' }, growthMetrics: { height: 30 }, }, previousHash: 'hash-1', hash: 'hash-2', nonce: 2, }, // Third generation plant { index: 3, timestamp: '2024-02-01T00:00:00Z', plant: { id: 'plant-3', name: 'Third Gen Tomato', species: 'Tomato', variety: 'Cherry', generation: 3, propagationType: 'seed', parentPlantId: 'plant-2', childPlants: [], status: 'healthy', dateAcquired: '2024-02-01', location: { latitude: 40.75, longitude: -73.98 }, environment: { light: 'full_sun' }, growthMetrics: { height: 20 }, }, previousHash: 'hash-2', hash: 'hash-3', nonce: 3, }, ]), })), })); describe('PlantLineageAgent', () => { let agent: PlantLineageAgent; beforeEach(() => { agent = new PlantLineageAgent(); }); afterEach(async () => { if (agent.status === 'running') { await agent.stop(); } }); describe('Initialization', () => { it('should initialize with correct config', () => { expect(agent.config.id).toBe('plant-lineage-agent'); expect(agent.config.name).toBe('Plant Lineage Agent'); expect(agent.config.priority).toBe('high'); expect(agent.config.intervalMs).toBe(60000); }); it('should start in idle status', () => { expect(agent.status).toBe('idle'); }); }); describe('runOnce', () => { it('should complete a scan cycle', async () => { const result = await agent.runOnce(); expect(result).not.toBeNull(); expect(result?.status).toBe('completed'); expect(result?.type).toBe('lineage_scan'); }); it('should scan plants and update cache', async () => { await agent.runOnce(); expect(agent.getLineageAnalysis('plant-1')).not.toBeNull(); expect(agent.getLineageAnalysis('plant-2')).not.toBeNull(); expect(agent.getLineageAnalysis('plant-3')).not.toBeNull(); }); it('should return scan statistics', async () => { const result = await agent.runOnce(); expect(result?.result).toHaveProperty('plantsScanned'); expect(result?.result).toHaveProperty('anomaliesFound'); expect(result?.result).toHaveProperty('cacheSize'); expect(result?.result.plantsScanned).toBe(3); }); }); describe('Lineage Analysis', () => { beforeEach(async () => { await agent.runOnce(); }); it('should find ancestors correctly', () => { const analysis = agent.getLineageAnalysis('plant-3'); expect(analysis?.ancestors).toContain('plant-2'); expect(analysis?.ancestors).toContain('plant-1'); expect(analysis?.ancestors.length).toBe(2); }); it('should find descendants correctly', () => { const analysis = agent.getLineageAnalysis('plant-1'); expect(analysis?.descendants).toContain('plant-2'); expect(analysis?.descendants).toContain('plant-3'); }); it('should track generation depth', () => { const analysis1 = agent.getLineageAnalysis('plant-1'); const analysis3 = agent.getLineageAnalysis('plant-3'); expect(analysis1?.generation).toBe(1); expect(analysis3?.generation).toBe(3); }); it('should calculate lineage size', () => { const analysis = agent.getLineageAnalysis('plant-2'); // plant-2 has 1 ancestor (plant-1) and 1 descendant (plant-3) + itself = 3 expect(analysis?.totalLineageSize).toBe(3); }); it('should build propagation chain', () => { const analysis = agent.getLineageAnalysis('plant-3'); expect(analysis?.propagationChain).toEqual(['original', 'cutting', 'seed']); }); it('should calculate health score', () => { const analysis = agent.getLineageAnalysis('plant-1'); expect(analysis?.healthScore).toBeGreaterThan(0); expect(analysis?.healthScore).toBeLessThanOrEqual(100); }); it('should return null for non-existent plant', () => { const analysis = agent.getLineageAnalysis('non-existent'); expect(analysis).toBeNull(); }); }); describe('Network Statistics', () => { beforeEach(async () => { await agent.runOnce(); }); it('should calculate total plants', () => { const stats = agent.getNetworkStats(); expect(stats.totalPlants).toBe(3); }); it('should calculate total lineages (root plants)', () => { const stats = agent.getNetworkStats(); expect(stats.totalLineages).toBe(1); // Only plant-1 has no ancestors }); it('should calculate average generation depth', () => { const stats = agent.getNetworkStats(); // (1 + 2 + 3) / 3 = 2 expect(stats.avgGenerationDepth).toBe(2); }); it('should return empty stats when no data', () => { const emptyAgent = new PlantLineageAgent(); const stats = emptyAgent.getNetworkStats(); expect(stats.totalPlants).toBe(0); expect(stats.totalLineages).toBe(0); }); }); describe('Anomaly Detection', () => { it('should return empty anomalies initially', () => { expect(agent.getAnomalies()).toEqual([]); }); it('should detect anomalies during scan', async () => { await agent.runOnce(); const anomalies = agent.getAnomalies(); // The mock data is valid, so no anomalies should be detected expect(Array.isArray(anomalies)).toBe(true); }); }); describe('Singleton', () => { it('should return same instance from getPlantLineageAgent', () => { const agent1 = getPlantLineageAgent(); const agent2 = getPlantLineageAgent(); expect(agent1).toBe(agent2); }); }); });