- 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
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
/**
|
|
* Carbon Calculation Tests
|
|
* Tests for carbon footprint calculation logic
|
|
*/
|
|
|
|
import { TransportChain } from '../../../lib/transport/tracker';
|
|
import {
|
|
CARBON_FACTORS,
|
|
TransportMethod,
|
|
TransportLocation,
|
|
} from '../../../lib/transport/types';
|
|
|
|
describe('Carbon Footprint Calculation', () => {
|
|
let chain: TransportChain;
|
|
|
|
beforeEach(() => {
|
|
chain = new TransportChain(1);
|
|
});
|
|
|
|
describe('CARBON_FACTORS', () => {
|
|
it('should have zero emissions for walking', () => {
|
|
expect(CARBON_FACTORS.walking).toBe(0);
|
|
});
|
|
|
|
it('should have zero emissions for bicycle', () => {
|
|
expect(CARBON_FACTORS.bicycle).toBe(0);
|
|
});
|
|
|
|
it('should have low emissions for electric vehicles', () => {
|
|
expect(CARBON_FACTORS.electric_vehicle).toBeLessThan(CARBON_FACTORS.gasoline_vehicle);
|
|
expect(CARBON_FACTORS.electric_truck).toBeLessThan(CARBON_FACTORS.diesel_truck);
|
|
});
|
|
|
|
it('should have high emissions for air transport', () => {
|
|
expect(CARBON_FACTORS.air).toBeGreaterThan(CARBON_FACTORS.diesel_truck);
|
|
expect(CARBON_FACTORS.air).toBeGreaterThan(CARBON_FACTORS.ship);
|
|
});
|
|
|
|
it('should have moderate emissions for refrigerated trucks', () => {
|
|
expect(CARBON_FACTORS.refrigerated_truck).toBeGreaterThan(CARBON_FACTORS.diesel_truck);
|
|
});
|
|
|
|
it('should have lowest emissions for rail and ship', () => {
|
|
expect(CARBON_FACTORS.rail).toBeLessThan(CARBON_FACTORS.diesel_truck);
|
|
expect(CARBON_FACTORS.ship).toBeLessThan(CARBON_FACTORS.diesel_truck);
|
|
});
|
|
|
|
it('should define all transport methods', () => {
|
|
const methods: TransportMethod[] = [
|
|
'walking', 'bicycle', 'electric_vehicle', 'hybrid_vehicle',
|
|
'gasoline_vehicle', 'diesel_truck', 'electric_truck',
|
|
'refrigerated_truck', 'rail', 'ship', 'air', 'drone',
|
|
'local_delivery', 'customer_pickup'
|
|
];
|
|
|
|
methods.forEach(method => {
|
|
expect(CARBON_FACTORS[method]).toBeDefined();
|
|
expect(typeof CARBON_FACTORS[method]).toBe('number');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('calculateCarbon', () => {
|
|
it('should calculate zero carbon for zero distance', () => {
|
|
const carbon = chain.calculateCarbon('diesel_truck', 0, 10);
|
|
expect(carbon).toBe(0);
|
|
});
|
|
|
|
it('should calculate zero carbon for zero weight', () => {
|
|
const carbon = chain.calculateCarbon('diesel_truck', 100, 0);
|
|
expect(carbon).toBe(0);
|
|
});
|
|
|
|
it('should calculate zero carbon for walking', () => {
|
|
const carbon = chain.calculateCarbon('walking', 10, 5);
|
|
expect(carbon).toBe(0);
|
|
});
|
|
|
|
it('should calculate correct carbon for known values', () => {
|
|
// diesel_truck = 0.15 kg CO2 per km per kg cargo
|
|
const carbon = chain.calculateCarbon('diesel_truck', 100, 10);
|
|
expect(carbon).toBe(0.15 * 100 * 10);
|
|
});
|
|
|
|
it('should increase linearly with distance', () => {
|
|
const carbon1 = chain.calculateCarbon('diesel_truck', 50, 10);
|
|
const carbon2 = chain.calculateCarbon('diesel_truck', 100, 10);
|
|
expect(carbon2).toBe(carbon1 * 2);
|
|
});
|
|
|
|
it('should increase linearly with weight', () => {
|
|
const carbon1 = chain.calculateCarbon('diesel_truck', 100, 5);
|
|
const carbon2 = chain.calculateCarbon('diesel_truck', 100, 10);
|
|
expect(carbon2).toBe(carbon1 * 2);
|
|
});
|
|
|
|
it('should compare transport methods correctly', () => {
|
|
const distance = 100;
|
|
const weight = 10;
|
|
|
|
const walking = chain.calculateCarbon('walking', distance, weight);
|
|
const electric = chain.calculateCarbon('electric_vehicle', distance, weight);
|
|
const gasoline = chain.calculateCarbon('gasoline_vehicle', distance, weight);
|
|
const diesel = chain.calculateCarbon('diesel_truck', distance, weight);
|
|
const air = chain.calculateCarbon('air', distance, weight);
|
|
|
|
expect(walking).toBe(0);
|
|
expect(electric).toBeLessThan(gasoline);
|
|
expect(gasoline).toBeLessThan(diesel);
|
|
expect(diesel).toBeLessThan(air);
|
|
});
|
|
});
|
|
|
|
describe('Distance Calculation', () => {
|
|
it('should calculate zero distance for same location', () => {
|
|
const location: TransportLocation = {
|
|
latitude: 40.7128,
|
|
longitude: -74.006,
|
|
locationType: 'farm',
|
|
};
|
|
|
|
const distance = TransportChain.calculateDistance(location, location);
|
|
expect(distance).toBe(0);
|
|
});
|
|
|
|
it('should calculate correct distance for known locations', () => {
|
|
// New York to Los Angeles is approximately 3940 km
|
|
const newYork: TransportLocation = {
|
|
latitude: 40.7128,
|
|
longitude: -74.006,
|
|
locationType: 'warehouse',
|
|
};
|
|
|
|
const losAngeles: TransportLocation = {
|
|
latitude: 34.0522,
|
|
longitude: -118.2437,
|
|
locationType: 'warehouse',
|
|
};
|
|
|
|
const distance = TransportChain.calculateDistance(newYork, losAngeles);
|
|
expect(distance).toBeGreaterThan(3900);
|
|
expect(distance).toBeLessThan(4000);
|
|
});
|
|
|
|
it('should calculate short distances accurately', () => {
|
|
// Two points 1km apart (approximately)
|
|
const point1: TransportLocation = {
|
|
latitude: 40.7128,
|
|
longitude: -74.006,
|
|
locationType: 'farm',
|
|
};
|
|
|
|
const point2: TransportLocation = {
|
|
latitude: 40.7218, // ~1km north
|
|
longitude: -74.006,
|
|
locationType: 'farm',
|
|
};
|
|
|
|
const distance = TransportChain.calculateDistance(point1, point2);
|
|
expect(distance).toBeGreaterThan(0.9);
|
|
expect(distance).toBeLessThan(1.1);
|
|
});
|
|
|
|
it('should be symmetric', () => {
|
|
const pointA: TransportLocation = {
|
|
latitude: 40.7128,
|
|
longitude: -74.006,
|
|
locationType: 'farm',
|
|
};
|
|
|
|
const pointB: TransportLocation = {
|
|
latitude: 34.0522,
|
|
longitude: -118.2437,
|
|
locationType: 'warehouse',
|
|
};
|
|
|
|
const distanceAB = TransportChain.calculateDistance(pointA, pointB);
|
|
const distanceBA = TransportChain.calculateDistance(pointB, pointA);
|
|
|
|
expect(distanceAB).toBeCloseTo(distanceBA, 5);
|
|
});
|
|
|
|
it('should handle crossing the prime meridian', () => {
|
|
const london: TransportLocation = {
|
|
latitude: 51.5074,
|
|
longitude: -0.1278,
|
|
locationType: 'warehouse',
|
|
};
|
|
|
|
const paris: TransportLocation = {
|
|
latitude: 48.8566,
|
|
longitude: 2.3522,
|
|
locationType: 'warehouse',
|
|
};
|
|
|
|
const distance = TransportChain.calculateDistance(london, paris);
|
|
// London to Paris is approximately 344 km
|
|
expect(distance).toBeGreaterThan(330);
|
|
expect(distance).toBeLessThan(360);
|
|
});
|
|
|
|
it('should handle crossing the equator', () => {
|
|
const north: TransportLocation = {
|
|
latitude: 10,
|
|
longitude: 0,
|
|
locationType: 'farm',
|
|
};
|
|
|
|
const south: TransportLocation = {
|
|
latitude: -10,
|
|
longitude: 0,
|
|
locationType: 'farm',
|
|
};
|
|
|
|
const distance = TransportChain.calculateDistance(north, south);
|
|
// 20 degrees of latitude is approximately 2222 km
|
|
expect(distance).toBeGreaterThan(2200);
|
|
expect(distance).toBeLessThan(2250);
|
|
});
|
|
});
|
|
|
|
describe('Carbon Tracking in Events', () => {
|
|
it('should auto-calculate carbon if not provided', () => {
|
|
const event = {
|
|
id: 'test-carbon-auto',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition' as const,
|
|
fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const },
|
|
toLocation: { latitude: 41, longitude: -74, locationType: 'farm' as const },
|
|
distanceKm: 100,
|
|
durationMinutes: 120,
|
|
transportMethod: 'diesel_truck' as const,
|
|
carbonFootprintKg: 0, // Will be auto-calculated
|
|
senderId: 'sender',
|
|
receiverId: 'receiver',
|
|
status: 'verified' as const,
|
|
seedBatchId: 'batch-001',
|
|
sourceType: 'seed_bank' as const,
|
|
species: 'Test',
|
|
quantity: 100,
|
|
quantityUnit: 'seeds' as const,
|
|
generation: 1,
|
|
};
|
|
|
|
const block = chain.recordEvent(event);
|
|
expect(block.transportEvent.carbonFootprintKg).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should accumulate carbon across chain', () => {
|
|
const event1 = {
|
|
id: 'carbon-chain-1',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition' as const,
|
|
fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const },
|
|
toLocation: { latitude: 41, longitude: -74, locationType: 'farm' as const },
|
|
distanceKm: 100,
|
|
durationMinutes: 120,
|
|
transportMethod: 'diesel_truck' as const,
|
|
carbonFootprintKg: 0,
|
|
senderId: 'sender',
|
|
receiverId: 'receiver',
|
|
status: 'verified' as const,
|
|
seedBatchId: 'batch-001',
|
|
sourceType: 'seed_bank' as const,
|
|
species: 'Test',
|
|
quantity: 100,
|
|
quantityUnit: 'seeds' as const,
|
|
generation: 1,
|
|
};
|
|
|
|
const event2 = { ...event1, id: 'carbon-chain-2', distanceKm: 200 };
|
|
|
|
const block1 = chain.recordEvent(event1);
|
|
const block2 = chain.recordEvent(event2);
|
|
|
|
expect(block2.cumulativeCarbonKg).toBeGreaterThan(block1.cumulativeCarbonKg);
|
|
expect(block2.cumulativeCarbonKg).toBeCloseTo(
|
|
block1.cumulativeCarbonKg + block2.transportEvent.carbonFootprintKg,
|
|
5
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Environmental Impact Calculations', () => {
|
|
it('should calculate carbon savings vs conventional', () => {
|
|
const userId = 'eco-user';
|
|
|
|
const event = {
|
|
id: 'eco-event',
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition' as const,
|
|
fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const },
|
|
toLocation: { latitude: 40.1, longitude: -74, locationType: 'farm' as const },
|
|
distanceKm: 10, // Very short local distance
|
|
durationMinutes: 20,
|
|
transportMethod: 'bicycle' as const, // Zero carbon
|
|
carbonFootprintKg: 0,
|
|
senderId: userId,
|
|
receiverId: userId,
|
|
status: 'verified' as const,
|
|
seedBatchId: 'batch-eco',
|
|
sourceType: 'seed_bank' as const,
|
|
species: 'Test',
|
|
quantity: 1000,
|
|
quantityUnit: 'grams' as const,
|
|
generation: 1,
|
|
};
|
|
|
|
chain.recordEvent(event);
|
|
const impact = chain.getEnvironmentalImpact(userId);
|
|
|
|
// Local, zero-carbon transport should save significantly vs conventional
|
|
expect(impact.comparisonToConventional.carbonSaved).toBeGreaterThan(0);
|
|
expect(impact.comparisonToConventional.milesSaved).toBeGreaterThan(0);
|
|
expect(impact.comparisonToConventional.percentageReduction).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should break down impact by transport method', () => {
|
|
const userId = 'breakdown-user';
|
|
|
|
// Record multiple events with different methods
|
|
const methods: TransportMethod[] = ['bicycle', 'electric_vehicle', 'diesel_truck'];
|
|
|
|
methods.forEach((method, i) => {
|
|
chain.recordEvent({
|
|
id: `breakdown-${i}`,
|
|
timestamp: new Date().toISOString(),
|
|
eventType: 'seed_acquisition' as const,
|
|
fromLocation: { latitude: 40, longitude: -74, locationType: 'seed_bank' as const },
|
|
toLocation: { latitude: 41, longitude: -74, locationType: 'farm' as const },
|
|
distanceKm: 50,
|
|
durationMinutes: 60,
|
|
transportMethod: method,
|
|
carbonFootprintKg: 0,
|
|
senderId: userId,
|
|
receiverId: userId,
|
|
status: 'verified' as const,
|
|
seedBatchId: `batch-${i}`,
|
|
sourceType: 'seed_bank' as const,
|
|
species: 'Test',
|
|
quantity: 100,
|
|
quantityUnit: 'seeds' as const,
|
|
generation: 1,
|
|
});
|
|
});
|
|
|
|
const impact = chain.getEnvironmentalImpact(userId);
|
|
|
|
expect(impact.breakdownByMethod['bicycle']).toBeDefined();
|
|
expect(impact.breakdownByMethod['electric_vehicle']).toBeDefined();
|
|
expect(impact.breakdownByMethod['diesel_truck']).toBeDefined();
|
|
|
|
// Bicycle should have zero carbon
|
|
expect(impact.breakdownByMethod['bicycle'].carbon).toBe(0);
|
|
|
|
// Diesel should have highest carbon
|
|
expect(impact.breakdownByMethod['diesel_truck'].carbon)
|
|
.toBeGreaterThan(impact.breakdownByMethod['electric_vehicle'].carbon);
|
|
});
|
|
});
|
|
});
|