localgreenchain/__tests__/integration/vf-batch-lifecycle.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

558 lines
17 KiB
TypeScript

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