- 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.
241 lines
7.3 KiB
TypeScript
241 lines
7.3 KiB
TypeScript
/**
|
|
* 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);
|
|
});
|
|
});
|
|
});
|