/** * Recipe Stage Logic Tests * Tests for growing recipe and stage management */ import { VerticalFarmController, } from '../../../lib/vertical-farming/controller'; import { GrowingRecipe, GrowthStage, VerticalFarm, GrowingZone, } from '../../../lib/vertical-farming/types'; describe('Growing Recipes', () => { let controller: VerticalFarmController; beforeEach(() => { controller = new VerticalFarmController(); }); describe('Default Recipes', () => { it('should have complete stage definitions', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { expect(recipe.stages.length).toBeGreaterThan(0); expect(recipe.expectedDays).toBeGreaterThan(0); // Stages should cover full duration const lastStage = recipe.stages[recipe.stages.length - 1]; expect(lastStage.daysEnd).toBeLessThanOrEqual(recipe.expectedDays); }); }); it('should have non-overlapping stages', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { for (let i = 1; i < recipe.stages.length; i++) { const prevStage = recipe.stages[i - 1]; const currentStage = recipe.stages[i]; expect(currentStage.daysStart).toBeGreaterThan(prevStage.daysStart); expect(currentStage.daysStart).toBeGreaterThanOrEqual(prevStage.daysEnd); } }); }); it('should have valid environment targets per stage', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { recipe.stages.forEach(stage => { // Temperature expect(stage.temperature.day).toBeGreaterThan(0); expect(stage.temperature.night).toBeGreaterThan(0); expect(stage.temperature.night).toBeLessThanOrEqual(stage.temperature.day); // Humidity expect(stage.humidity.day).toBeGreaterThan(0); expect(stage.humidity.day).toBeLessThanOrEqual(100); expect(stage.humidity.night).toBeGreaterThan(0); expect(stage.humidity.night).toBeLessThanOrEqual(100); // CO2 expect(stage.co2Ppm).toBeGreaterThanOrEqual(0); // Light expect(stage.lightHours).toBeGreaterThanOrEqual(0); expect(stage.lightHours).toBeLessThanOrEqual(24); expect(stage.lightPpfd).toBeGreaterThanOrEqual(0); // Nutrients expect(stage.targetEc).toBeGreaterThanOrEqual(0); expect(stage.targetPh).toBeGreaterThan(0); expect(stage.targetPh).toBeLessThan(14); }); }); }); }); describe('Lettuce Recipe', () => { it('should have correct growth stages', () => { const recipes = controller.getRecipes(); const lettuce = recipes.find(r => r.cropType === 'lettuce')!; expect(lettuce.stages.length).toBeGreaterThanOrEqual(3); const stageNames = lettuce.stages.map(s => s.name); expect(stageNames).toContain('Germination'); expect(stageNames).toContain('Seedling'); }); it('should have approximately 35 day cycle', () => { const recipes = controller.getRecipes(); const lettuce = recipes.find(r => r.cropType === 'lettuce')!; expect(lettuce.expectedDays).toBeGreaterThanOrEqual(30); expect(lettuce.expectedDays).toBeLessThanOrEqual(45); }); it('should have transplant action', () => { const recipes = controller.getRecipes(); const lettuce = recipes.find(r => r.cropType === 'lettuce')!; const hasTransplant = lettuce.stages.some(stage => stage.actions.some(action => action.action === 'transplant') ); expect(hasTransplant).toBe(true); }); }); describe('Basil Recipe', () => { it('should have higher temperature requirements', () => { const recipes = controller.getRecipes(); const basil = recipes.find(r => r.cropType === 'basil')!; const lettuce = recipes.find(r => r.cropType === 'lettuce')!; // Basil prefers warmer temperatures const basilMaxTemp = Math.max(...basil.stages.map(s => s.temperature.day)); const lettuceMaxTemp = Math.max(...lettuce.stages.map(s => s.temperature.day)); expect(basilMaxTemp).toBeGreaterThanOrEqual(lettuceMaxTemp); }); it('should have longer cycle than microgreens', () => { const recipes = controller.getRecipes(); const basil = recipes.find(r => r.cropType === 'basil')!; const microgreens = recipes.find(r => r.cropType === 'microgreens')!; expect(basil.expectedDays).toBeGreaterThan(microgreens.expectedDays); }); }); describe('Microgreens Recipe', () => { it('should have short cycle', () => { const recipes = controller.getRecipes(); const microgreens = recipes.find(r => r.cropType === 'microgreens')!; expect(microgreens.expectedDays).toBeLessThanOrEqual(21); }); it('should start in darkness (sowing stage)', () => { const recipes = controller.getRecipes(); const microgreens = recipes.find(r => r.cropType === 'microgreens')!; const sowingStage = microgreens.stages.find(s => s.name === 'Sowing'); if (sowingStage) { expect(sowingStage.lightHours).toBe(0); expect(sowingStage.lightPpfd).toBe(0); } }); it('should have high humidity in early stages', () => { const recipes = controller.getRecipes(); const microgreens = recipes.find(r => r.cropType === 'microgreens')!; const earlyStage = microgreens.stages[0]; expect(earlyStage.humidity.day).toBeGreaterThanOrEqual(80); }); }); describe('Stage Transitions', () => { it('should update environment targets on stage change', () => { const farm = createVerticalFarm('farm-001'); controller.registerFarm(farm); const recipes = controller.getRecipes(); const recipe = recipes[0]; const batch = controller.startCropBatch( 'farm-001', 'zone-001', recipe.id, 'seed-batch-001', 100 ); // Initial stage const initialStage = recipe.stages[0]; const farmAfter = controller.getFarm('farm-001'); const zone = farmAfter?.zones.find(z => z.id === 'zone-001'); expect(zone?.environmentTargets.temperatureC.target).toBe(initialStage.temperature.day); }); }); describe('Recipe Requirements', () => { it('should specify zone type requirements', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { expect(recipe.requirements.zoneType).toBeDefined(); expect(['NFT', 'DWC', 'ebb_flow', 'aeroponics', 'vertical_towers', 'rack_system']) .toContain(recipe.requirements.zoneType); }); }); it('should specify minimum light requirements', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { expect(recipe.requirements.minimumPpfd).toBeGreaterThan(0); }); }); it('should specify temperature requirements', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { expect(recipe.requirements.idealTemperatureC).toBeGreaterThan(0); expect(recipe.requirements.idealTemperatureC).toBeLessThan(40); }); }); }); describe('Yield Expectations', () => { it('should have reasonable yield estimates', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { expect(recipe.expectedYieldGrams).toBeGreaterThan(0); expect(recipe.expectedYieldPerSqm).toBeGreaterThan(0); }); }); it('should have consistent yield calculations', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { // Yield per sqm should be reasonable multiple of per-plant yield const plantsPerSqm = recipe.expectedYieldPerSqm / recipe.expectedYieldGrams; expect(plantsPerSqm).toBeGreaterThan(0); expect(plantsPerSqm).toBeLessThan(100); // Reasonable plant density }); }); }); describe('Stage Actions', () => { it('should have day specified for each action', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { recipe.stages.forEach(stage => { stage.actions.forEach(action => { expect(action.day).toBeGreaterThanOrEqual(stage.daysStart); expect(action.day).toBeLessThanOrEqual(stage.daysEnd); }); }); }); }); it('should have automation flag for actions', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { recipe.stages.forEach(stage => { stage.actions.forEach(action => { expect(typeof action.automated).toBe('boolean'); }); }); }); }); it('should have descriptions for actions', () => { const recipes = controller.getRecipes(); recipes.forEach(recipe => { recipe.stages.forEach(stage => { stage.actions.forEach(action => { expect(action.description).toBeDefined(); expect(action.description.length).toBeGreaterThan(0); }); }); }); }); }); describe('Custom Recipes', () => { it('should validate custom recipe structure', () => { const customRecipe: GrowingRecipe = { id: 'recipe-custom', name: 'Custom Recipe', cropType: 'cucumber', version: '1.0', stages: [ { name: 'Stage 1', daysStart: 0, daysEnd: 30, temperature: { day: 25, night: 22 }, humidity: { day: 70, night: 75 }, co2Ppm: 1000, lightHours: 16, lightPpfd: 400, nutrientRecipeId: 'nutrient-veg', targetEc: 2.0, targetPh: 5.8, actions: [], }, ], expectedDays: 60, expectedYieldGrams: 500, expectedYieldPerSqm: 10000, requirements: { positions: 1, zoneType: 'NFT', minimumPpfd: 300, idealTemperatureC: 24, }, source: 'internal', timesUsed: 0, }; controller.addRecipe(customRecipe); const recipes = controller.getRecipes(); const found = recipes.find(r => r.id === 'recipe-custom'); expect(found).toBeDefined(); expect(found?.cropType).toBe('cucumber'); }); }); }); // Helper function 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: [ { id: 'zone-001', name: 'Zone A', level: 1, 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', }, ], 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, }; }