- 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
593 lines
17 KiB
TypeScript
593 lines
17 KiB
TypeScript
/**
|
|
* 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>): 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,
|
|
};
|
|
}
|