/** * Environment Alert Tests * Tests for environment monitoring and alert generation */ import { VerticalFarmController, } from '../../../lib/vertical-farming/controller'; import { VerticalFarm, GrowingZone, ZoneEnvironmentReadings, EnvironmentAlert, } from '../../../lib/vertical-farming/types'; describe('Environment Alerts', () => { let controller: VerticalFarmController; beforeEach(() => { controller = new VerticalFarmController(); }); describe('Temperature Alerts', () => { it('should detect low temperature', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 15, // Below min of 18 }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert).toBeDefined(); expect(tempAlert?.type).toBe('low'); }); it('should detect high temperature', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 30, // Above max of 24 }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert).toBeDefined(); expect(tempAlert?.type).toBe('high'); }); it('should detect critical low temperature', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 10, // More than 5 below min }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert?.type).toBe('critical_low'); }); it('should detect critical high temperature', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 35, // More than 5 above max }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert?.type).toBe('critical_high'); }); it('should not alert for normal temperature', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 21, // Within range }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert).toBeUndefined(); }); }); describe('Humidity Alerts', () => { it('should detect low humidity', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ humidityPercent: 50, // Below min of 60 }); const alerts = controller.recordEnvironment('zone-001', readings); const humidityAlert = alerts.find(a => a.parameter === 'humidity'); expect(humidityAlert).toBeDefined(); expect(humidityAlert?.type).toBe('low'); }); it('should detect high humidity', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ humidityPercent: 90, // Above max of 80 }); const alerts = controller.recordEnvironment('zone-001', readings); const humidityAlert = alerts.find(a => a.parameter === 'humidity'); expect(humidityAlert).toBeDefined(); expect(humidityAlert?.type).toBe('high'); }); }); describe('EC Alerts', () => { it('should detect low EC', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ ec: 0.8, // Below min of 1.2 }); const alerts = controller.recordEnvironment('zone-001', readings); const ecAlert = alerts.find(a => a.parameter === 'ec'); expect(ecAlert).toBeDefined(); expect(ecAlert?.type).toBe('low'); }); it('should detect high EC', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ ec: 2.5, // Above max of 1.8 }); const alerts = controller.recordEnvironment('zone-001', readings); const ecAlert = alerts.find(a => a.parameter === 'ec'); expect(ecAlert).toBeDefined(); expect(ecAlert?.type).toBe('high'); }); }); describe('pH Alerts', () => { it('should detect low pH', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ ph: 5.2, // Below min of 5.8 }); const alerts = controller.recordEnvironment('zone-001', readings); const phAlert = alerts.find(a => a.parameter === 'ph'); expect(phAlert).toBeDefined(); expect(phAlert?.type).toBe('low'); }); it('should detect high pH', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ ph: 7.0, // Above max of 6.2 }); const alerts = controller.recordEnvironment('zone-001', readings); const phAlert = alerts.find(a => a.parameter === 'ph'); expect(phAlert).toBeDefined(); expect(phAlert?.type).toBe('high'); }); }); describe('Multiple Alerts', () => { it('should detect multiple issues simultaneously', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 30, humidityPercent: 40, ec: 0.5, ph: 7.5, }); const alerts = controller.recordEnvironment('zone-001', readings); expect(alerts.length).toBeGreaterThanOrEqual(4); expect(alerts.some(a => a.parameter === 'temperature')).toBe(true); expect(alerts.some(a => a.parameter === 'humidity')).toBe(true); expect(alerts.some(a => a.parameter === 'ec')).toBe(true); expect(alerts.some(a => a.parameter === 'ph')).toBe(true); }); }); describe('Alert Properties', () => { it('should include threshold value', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 15, }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert?.threshold).toBeDefined(); expect(tempAlert?.threshold).toBe(18); // Min threshold }); it('should include actual value', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 15, }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert?.value).toBe(15); }); it('should include timestamp', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 15, }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert?.timestamp).toBeDefined(); }); it('should set acknowledged to false by default', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 15, }); const alerts = controller.recordEnvironment('zone-001', readings); const tempAlert = alerts.find(a => a.parameter === 'temperature'); expect(tempAlert?.acknowledged).toBe(false); }); }); describe('Zone Environment Updates', () => { it('should update zone current environment', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 22, humidityPercent: 65, }); controller.recordEnvironment('zone-001', readings); const updatedFarm = controller.getFarm('farm-001'); const zone = updatedFarm?.zones.find(z => z.id === 'zone-001'); expect(zone?.currentEnvironment.temperatureC).toBe(22); expect(zone?.currentEnvironment.humidityPercent).toBe(65); }); it('should store alerts in zone readings', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 30, // Will trigger alert }); controller.recordEnvironment('zone-001', readings); const updatedFarm = controller.getFarm('farm-001'); const zone = updatedFarm?.zones.find(z => z.id === 'zone-001'); expect(zone?.currentEnvironment.alerts.length).toBeGreaterThan(0); }); }); describe('Batch Health Score', () => { it('should decrease health score on alerts', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const initialHealth = batch.healthScore; // Record problematic environment const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 30, // High temp alert }); controller.recordEnvironment('zone-001', readings); // Health should decrease expect(batch.healthScore).toBeLessThan(initialHealth); }); it('should decrease health more for critical alerts', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const initialHealth = batch.healthScore; // Record critical environment issue const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 5, // Critical low temp }); controller.recordEnvironment('zone-001', readings); // Health should decrease significantly expect(batch.healthScore).toBeLessThan(initialHealth - 3); }); it('should not go below 0', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); // Record many critical issues for (let i = 0; i < 50; i++) { const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 5, }); controller.recordEnvironment('zone-001', readings); } expect(batch.healthScore).toBeGreaterThanOrEqual(0); }); }); describe('Environment Logging', () => { it('should log environment readings to batch', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const readings: ZoneEnvironmentReadings = createReadings({}); controller.recordEnvironment('zone-001', readings); expect(batch.environmentLog.length).toBe(1); expect(batch.environmentLog[0].readings).toBeDefined(); }); it('should accumulate environment logs', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'farm-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); for (let i = 0; i < 5; i++) { const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 20 + i, }); controller.recordEnvironment('zone-001', readings); } expect(batch.environmentLog.length).toBe(5); }); }); describe('Unknown Zone', () => { it('should return empty alerts for unknown zone', () => { const readings: ZoneEnvironmentReadings = createReadings({ temperatureC: 5, // Would normally trigger alert }); const alerts = controller.recordEnvironment('unknown-zone', readings); expect(alerts.length).toBe(0); }); }); }); // Helper functions function createVerticalFarm(id: string): VerticalFarm { return { id, name: 'Test Farm', ownerId: 'owner-001', location: { latitude: 40.7128, longitude: -74.006, address: '123 Test St', city: 'New York', country: 'USA', timezone: 'America/New_York', }, specs: { totalAreaSqm: 500, growingAreaSqm: 400, numberOfLevels: 4, ceilingHeightM: 3, totalGrowingPositions: 4000, currentActivePlants: 0, powerCapacityKw: 100, waterStorageL: 5000, backupPowerHours: 24, certifications: [], buildingType: 'warehouse', insulation: 'high_efficiency', }, zones: [createGrowingZone('zone-001', 'Zone A', 1)], environmentalControl: { hvacUnits: [], co2Injection: { type: 'tank', capacityKg: 50, currentLevelKg: 40, injectionRateKgPerHour: 2, status: 'maintaining' }, humidification: { type: 'ultrasonic', capacityLPerHour: 10, status: 'running', currentOutput: 5 }, airCirculation: { fans: [] }, controlMode: 'adaptive', }, irrigationSystem: { type: 'recirculating', freshWaterTankL: 2000, freshWaterLevelL: 1800, nutrientTankL: 500, nutrientLevelL: 450, wasteTankL: 200, wasteLevelL: 50, waterTreatment: { ro: true, uv: true, ozone: false, filtration: '10 micron' }, pumps: [], irrigationSchedule: [], }, lightingSystem: { type: 'LED', fixtures: [], lightSchedules: [], totalWattage: 5000, currentWattage: 3000, efficacyUmolJ: 2.5, }, nutrientSystem: { mixingMethod: 'fully_auto', stockSolutions: [], dosingPumps: [], currentRecipe: { id: 'default', name: 'Default', cropType: 'general', growthStage: 'vegetative', targetEc: 1.5, targetPh: 6.0, ratios: { n: 200, p: 50, k: 200, ca: 200, mg: 50, s: 100, fe: 5, mn: 0.5, zn: 0.3, cu: 0.1, b: 0.5, mo: 0.05 }, dosingRatiosMlPerL: [], }, monitoring: { ec: 1.5, ph: 6.0, lastCalibration: new Date().toISOString(), calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), }, }, automationLevel: 'semi_automated', automationSystems: [], status: 'operational', operationalSince: '2024-01-01', lastMaintenanceDate: new Date().toISOString(), currentCapacityUtilization: 75, averageYieldEfficiency: 85, energyEfficiencyScore: 80, }; } function createGrowingZone(id: string, name: string, level: number): GrowingZone { return { id, name, level, areaSqm: 50, lengthM: 10, widthM: 5, growingMethod: 'NFT', plantPositions: 500, currentCrop: '', plantIds: [], plantingDate: '', expectedHarvestDate: '', environmentTargets: { temperatureC: { min: 18, max: 24, target: 21 }, humidityPercent: { min: 60, max: 80, target: 70 }, co2Ppm: { min: 800, max: 1200, target: 1000 }, lightPpfd: { min: 200, max: 400, target: 300 }, lightHours: 16, nutrientEc: { min: 1.2, max: 1.8, target: 1.5 }, nutrientPh: { min: 5.8, max: 6.2, target: 6.0 }, waterTempC: { min: 18, max: 22, target: 20 }, }, currentEnvironment: { timestamp: new Date().toISOString(), temperatureC: 21, humidityPercent: 70, co2Ppm: 1000, vpd: 1.0, ppfd: 300, dli: 17, waterTempC: 20, ec: 1.5, ph: 6.0, dissolvedOxygenPpm: 8, airflowMs: 0.5, alerts: [], }, status: 'empty', }; } function createReadings(overrides: Partial): ZoneEnvironmentReadings { return { timestamp: new Date().toISOString(), temperatureC: 21, humidityPercent: 70, co2Ppm: 1000, vpd: 1.0, ppfd: 300, dli: 17, waterTempC: 20, ec: 1.5, ph: 6.0, dissolvedOxygenPpm: 8, airflowMs: 0.5, alerts: [], ...overrides, }; }