/** * Vertical Farm Batch Lifecycle Integration Tests * Tests complete vertical farm batch from start to harvest */ import { VerticalFarmController, } from '../../lib/vertical-farming/controller'; import { TransportChain, setTransportChain } from '../../lib/transport/tracker'; import { VerticalFarm, GrowingZone, ZoneEnvironmentReadings, CropBatch, } from '../../lib/vertical-farming/types'; import { SeedAcquisitionEvent, PlantingEvent, HarvestEvent, TransportLocation, } from '../../lib/transport/types'; describe('Vertical Farm Batch Lifecycle', () => { let controller: VerticalFarmController; let chain: TransportChain; beforeEach(() => { controller = new VerticalFarmController(); chain = new TransportChain(1); setTransportChain(chain); }); describe('Complete Batch Lifecycle', () => { it('should complete full batch lifecycle from planting to harvest', () => { // Step 1: Register farm const farm = createVerticalFarm('vf-lifecycle-001'); controller.registerFarm(farm); // Step 2: Select recipe and start batch const recipes = controller.getRecipes(); const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce')!; const batch = controller.startCropBatch( 'vf-lifecycle-001', 'zone-001', lettuceRecipe.id, 'seed-batch-vf-001', 100 ); expect(batch.status).toBe('germinating'); expect(batch.plantCount).toBe(100); expect(batch.currentStage).toBe(lettuceRecipe.stages[0].name); // Step 3: Verify zone is updated const farmAfterPlanting = controller.getFarm('vf-lifecycle-001')!; const zone = farmAfterPlanting.zones.find(z => z.id === 'zone-001')!; expect(zone.status).toBe('planted'); expect(zone.currentCrop).toBe('lettuce'); expect(zone.plantIds.length).toBe(100); // Step 4: Record environment readings (simulate daily monitoring) for (let day = 0; day < 5; day++) { const readings = createGoodReadings(); const alerts = controller.recordEnvironment('zone-001', readings); expect(alerts.length).toBe(0); // No alerts for good readings } // Step 5: Update batch progress const updatedBatch = controller.updateBatchProgress(batch.id); expect(updatedBatch.currentDay).toBeGreaterThanOrEqual(0); // Step 6: Complete harvest const completedBatch = controller.completeHarvest(batch.id, 18.0, 'A'); expect(completedBatch.status).toBe('completed'); expect(completedBatch.actualYieldKg).toBe(18.0); expect(completedBatch.qualityGrade).toBe('A'); // Step 7: Verify zone is cleared const farmAfterHarvest = controller.getFarm('vf-lifecycle-001')!; const zoneAfterHarvest = farmAfterHarvest.zones.find(z => z.id === 'zone-001')!; expect(zoneAfterHarvest.status).toBe('cleaning'); expect(zoneAfterHarvest.currentCrop).toBe(''); expect(zoneAfterHarvest.plantIds.length).toBe(0); }); }); describe('Environment Monitoring During Growth', () => { it('should track health score throughout growth', () => { const farm = createVerticalFarm('vf-health-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'vf-health-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); const initialHealth = batch.healthScore; expect(initialHealth).toBe(100); // Good readings - health should stay high for (let i = 0; i < 3; i++) { controller.recordEnvironment('zone-001', createGoodReadings()); } expect(batch.healthScore).toBe(100); // Bad readings - health should decrease controller.recordEnvironment('zone-001', createBadReadings()); expect(batch.healthScore).toBeLessThan(100); // Critical readings - health should decrease more controller.recordEnvironment('zone-001', createCriticalReadings()); expect(batch.healthScore).toBeLessThan(95); }); it('should log environment readings to batch', () => { const farm = createVerticalFarm('vf-log-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'vf-log-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); // Record multiple readings for (let i = 0; i < 5; i++) { controller.recordEnvironment('zone-001', createGoodReadings()); } expect(batch.environmentLog.length).toBe(5); }); }); describe('Multi-Zone Operations', () => { it('should manage multiple batches across zones', () => { const farm = createVerticalFarm('vf-multi-001'); farm.zones = [ createGrowingZone('zone-001', 'Zone A', 1), createGrowingZone('zone-002', 'Zone B', 1), createGrowingZone('zone-003', 'Zone C', 2), ]; controller.registerFarm(farm); const recipes = controller.getRecipes(); const lettuceRecipe = recipes.find(r => r.cropType === 'lettuce')!; const basilRecipe = recipes.find(r => r.cropType === 'basil')!; const microgreensRecipe = recipes.find(r => r.cropType === 'microgreens')!; // Start different batches in different zones const batch1 = controller.startCropBatch( 'vf-multi-001', 'zone-001', lettuceRecipe.id, 'seed-001', 100 ); const batch2 = controller.startCropBatch( 'vf-multi-001', 'zone-002', basilRecipe.id, 'seed-002', 50 ); const batch3 = controller.startCropBatch( 'vf-multi-001', 'zone-003', microgreensRecipe.id, 'seed-003', 200 ); expect(batch1.cropType).toBe('lettuce'); expect(batch2.cropType).toBe('basil'); expect(batch3.cropType).toBe('microgreens'); // Verify zones have correct crops const farmAfter = controller.getFarm('vf-multi-001')!; expect(farmAfter.zones.find(z => z.id === 'zone-001')!.currentCrop).toBe('lettuce'); expect(farmAfter.zones.find(z => z.id === 'zone-002')!.currentCrop).toBe('basil'); expect(farmAfter.zones.find(z => z.id === 'zone-003')!.currentCrop).toBe('microgreens'); }); }); describe('Analytics Generation', () => { it('should generate analytics after batch completion', () => { const farm = createVerticalFarm('vf-analytics-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); // Complete multiple batches for (let i = 0; i < 3; i++) { const batch = controller.startCropBatch( 'vf-analytics-001', 'zone-001', recipes[0].id, `seed-batch-${i}`, 100 ); controller.completeHarvest(batch.id, 15 + i, 'A'); } const analytics = controller.generateAnalytics('vf-analytics-001', 30); expect(analytics.cropCyclesCompleted).toBe(3); expect(analytics.totalYieldKg).toBeGreaterThan(0); expect(analytics.gradeAPercent).toBe(100); }); it('should calculate efficiency metrics', () => { const farm = createVerticalFarm('vf-efficiency-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'vf-efficiency-001', 'zone-001', recipes[0].id, 'seed-batch-001', 100 ); controller.completeHarvest(batch.id, 18.0, 'A'); const analytics = controller.generateAnalytics('vf-efficiency-001', 30); expect(analytics.yieldPerSqmPerYear).toBeGreaterThan(0); expect(analytics.averageQualityScore).toBeGreaterThan(0); expect(analytics.cropSuccessRate).toBe(100); }); }); describe('Integration with Transport Chain', () => { it('should track VF produce through transport chain', () => { const farm = createVerticalFarm('vf-transport-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'vf-transport-001', 'zone-001', recipes[0].id, 'seed-transport-001', 100 ); // Record seed acquisition in transport chain const seedEvent: SeedAcquisitionEvent = { id: 'vf-transport-seed-001', timestamp: new Date().toISOString(), eventType: 'seed_acquisition', fromLocation: createLocation('seed_bank', 'Seed Supplier'), toLocation: createLocation('vertical_farm', 'VF Facility'), distanceKm: 20, durationMinutes: 30, transportMethod: 'electric_vehicle', carbonFootprintKg: 0, senderId: 'seed-supplier', receiverId: 'vf-operator', status: 'verified', seedBatchId: 'seed-transport-001', sourceType: 'purchase', species: recipes[0].cropType, quantity: 100, quantityUnit: 'seeds', generation: 1, certifications: ['organic'], }; chain.recordEvent(seedEvent); // Complete VF harvest controller.completeHarvest(batch.id, 18.0, 'A'); // Record harvest in transport chain const harvestEvent: HarvestEvent = { id: 'vf-transport-harvest-001', timestamp: new Date().toISOString(), eventType: 'harvest', fromLocation: createLocation('vertical_farm', 'VF Facility'), toLocation: createLocation('warehouse', 'Distribution Center'), distanceKm: 10, durationMinutes: 20, transportMethod: 'electric_truck', carbonFootprintKg: 0, senderId: 'vf-operator', receiverId: 'distributor', status: 'verified', plantIds: batch.plantIds.slice(0, 10), harvestBatchId: batch.id, harvestType: 'full', produceType: recipes[0].cropType, grossWeight: 20, netWeight: 18, weightUnit: 'kg', qualityGrade: 'A', packagingType: 'sustainable_packaging', temperatureRequired: { min: 2, max: 8, optimal: 4, unit: 'celsius' }, shelfLifeHours: 168, seedsSaved: false, }; chain.recordEvent(harvestEvent); // Verify chain integrity expect(chain.isChainValid()).toBe(true); // Calculate environmental impact const impact = chain.getEnvironmentalImpact('vf-operator'); expect(impact.totalFoodMiles).toBeLessThan(50); // Very low for VF }); }); describe('Recipe Stage Transitions', () => { it('should update environment targets based on stage', () => { const farm = createVerticalFarm('vf-stage-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const recipe = recipes[0]; const batch = controller.startCropBatch( 'vf-stage-001', 'zone-001', recipe.id, 'seed-stage-001', 100 ); // Initial stage should be first stage expect(batch.currentStage).toBe(recipe.stages[0].name); // Get zone and check targets match first stage const farmAfter = controller.getFarm('vf-stage-001')!; const zone = farmAfter.zones.find(z => z.id === 'zone-001')!; const firstStage = recipe.stages[0]; expect(zone.environmentTargets.temperatureC.target).toBe(firstStage.temperature.day); }); }); describe('Batch Failure Handling', () => { it('should track issues that affect batch', () => { const farm = createVerticalFarm('vf-issue-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const batch = controller.startCropBatch( 'vf-issue-001', 'zone-001', recipes[0].id, 'seed-issue-001', 100 ); // Simulate multiple environment issues for (let i = 0; i < 20; i++) { controller.recordEnvironment('zone-001', createCriticalReadings()); } // Health should be significantly reduced expect(batch.healthScore).toBeLessThan(50); }); }); }); // Helper functions function createVerticalFarm(id: string): VerticalFarm { return { id, name: 'Test Vertical Farm', ownerId: 'owner-001', location: { latitude: 40.7128, longitude: -74.006, address: '123 Farm Street', 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: ['organic'], 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 createLocation(type: string, name: string): TransportLocation { return { latitude: 40.7 + Math.random() * 0.1, longitude: -74.0 + Math.random() * 0.1, locationType: type as any, facilityName: name, }; } function createGoodReadings(): 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: [], }; } function createBadReadings(): ZoneEnvironmentReadings { return { timestamp: new Date().toISOString(), temperatureC: 28, // Too high humidityPercent: 55, // Too low co2Ppm: 1000, vpd: 1.0, ppfd: 300, dli: 17, waterTempC: 20, ec: 1.5, ph: 6.0, dissolvedOxygenPpm: 8, airflowMs: 0.5, alerts: [], }; } function createCriticalReadings(): ZoneEnvironmentReadings { return { timestamp: new Date().toISOString(), temperatureC: 35, // Critical high humidityPercent: 40, // Critical low co2Ppm: 500, // Low vpd: 2.5, ppfd: 100, // Low dli: 5, waterTempC: 30, // Too high ec: 0.5, // Too low ph: 7.5, // Too high dissolvedOxygenPpm: 4, airflowMs: 0.1, alerts: [], }; }