localgreenchain/__tests__/lib/vertical-farming/recipes.test.ts
Claude b8d2d5be5f
Add comprehensive testing suite for transport, demand, and vertical farming systems
- Set up Jest testing framework with TypeScript support
- Add unit tests for TransportChain blockchain (tracker, carbon, types)
- Add unit tests for DemandForecaster (forecaster, aggregation, recommendations)
- Add unit tests for VerticalFarmController (controller, recipes, environment)
- Add API tests for transport, demand, and vertical-farm endpoints
- Add integration tests for full lifecycle workflows:
  - Seed-to-seed lifecycle
  - Demand-to-harvest flow
  - VF batch lifecycle
2025-11-22 18:47:04 +00:00

453 lines
14 KiB
TypeScript

/**
* 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,
};
}