localgreenchain/__tests__/lib/demand/forecaster.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

298 lines
9.8 KiB
TypeScript

/**
* DemandForecaster Tests
* Tests for the demand forecasting system
*/
import {
DemandForecaster,
getDemandForecaster,
} from '../../../lib/demand/forecaster';
import {
ConsumerPreference,
SupplyCommitment,
DemandSignal,
PlantingRecommendation,
} from '../../../lib/demand/types';
describe('DemandForecaster', () => {
let forecaster: DemandForecaster;
beforeEach(() => {
forecaster = new DemandForecaster();
});
describe('Initialization', () => {
it('should create empty forecaster', () => {
const json = forecaster.toJSON() as any;
expect(json.preferences).toEqual([]);
expect(json.supplyCommitments).toEqual([]);
expect(json.demandSignals).toEqual([]);
});
});
describe('Consumer Preferences', () => {
it('should register consumer preferences', () => {
const preference = createConsumerPreference('consumer-1');
forecaster.registerPreference(preference);
const json = forecaster.toJSON() as any;
expect(json.preferences.length).toBe(1);
});
it('should update existing preference', () => {
const preference1 = createConsumerPreference('consumer-1');
preference1.householdSize = 2;
forecaster.registerPreference(preference1);
const preference2 = createConsumerPreference('consumer-1');
preference2.householdSize = 4;
forecaster.registerPreference(preference2);
const json = forecaster.toJSON() as any;
expect(json.preferences.length).toBe(1);
expect(json.preferences[0][1].householdSize).toBe(4);
});
it('should register multiple consumers', () => {
forecaster.registerPreference(createConsumerPreference('consumer-1'));
forecaster.registerPreference(createConsumerPreference('consumer-2'));
forecaster.registerPreference(createConsumerPreference('consumer-3'));
const json = forecaster.toJSON() as any;
expect(json.preferences.length).toBe(3);
});
});
describe('Supply Commitments', () => {
it('should register supply commitment', () => {
const commitment = createSupplyCommitment('grower-1', 'lettuce', 50);
forecaster.registerSupply(commitment);
const json = forecaster.toJSON() as any;
expect(json.supplyCommitments.length).toBe(1);
});
it('should track multiple supply commitments', () => {
forecaster.registerSupply(createSupplyCommitment('grower-1', 'lettuce', 50));
forecaster.registerSupply(createSupplyCommitment('grower-2', 'tomato', 100));
const json = forecaster.toJSON() as any;
expect(json.supplyCommitments.length).toBe(2);
});
});
describe('Demand Signal Generation', () => {
it('should generate demand signal for region', () => {
// Add consumers
forecaster.registerPreference(createConsumerPreference('consumer-1', 40.7, -74.0));
forecaster.registerPreference(createConsumerPreference('consumer-2', 40.71, -74.01));
const signal = forecaster.generateDemandSignal(
40.7, -74.0, 50, 'New York Metro', 'summer'
);
expect(signal.id).toBeDefined();
expect(signal.region.name).toBe('New York Metro');
expect(signal.totalConsumers).toBe(2);
});
it('should filter consumers by region radius', () => {
// Consumer inside radius
forecaster.registerPreference(createConsumerPreference('inside', 40.7, -74.0));
// Consumer outside radius (far away)
forecaster.registerPreference(createConsumerPreference('outside', 35.0, -80.0));
const signal = forecaster.generateDemandSignal(
40.7, -74.0, 10, 'Small Region', 'summer'
);
expect(signal.totalConsumers).toBe(1);
});
it('should aggregate demand items', () => {
const pref1 = createConsumerPreference('consumer-1');
pref1.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false },
{ produceType: 'tomato', category: 'nightshades', priority: 'preferred', weeklyQuantity: 2, seasonalOnly: false },
];
forecaster.registerPreference(pref1);
const signal = forecaster.generateDemandSignal(
40.7, -74.0, 100, 'Test Region', 'summer'
);
expect(signal.demandItems.length).toBeGreaterThan(0);
});
it('should calculate supply status', () => {
// Add consumer demand
forecaster.registerPreference(createConsumerPreference('consumer-1'));
// No supply - should show shortage
let signal = forecaster.generateDemandSignal(
40.7, -74.0, 100, 'Test', 'summer'
);
expect(['shortage', 'critical', 'balanced']).toContain(signal.supplyStatus);
});
it('should include seasonal period', () => {
forecaster.registerPreference(createConsumerPreference('consumer-1'));
const signal = forecaster.generateDemandSignal(
40.7, -74.0, 100, 'Test', 'winter'
);
expect(signal.seasonalPeriod).toBe('winter');
});
it('should calculate weekly demand correctly', () => {
const pref = createConsumerPreference('consumer-1');
pref.householdSize = 4;
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false },
];
forecaster.registerPreference(pref);
const signal = forecaster.generateDemandSignal(
40.7, -74.0, 100, 'Test', 'summer'
);
const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce');
// Weekly demand = weeklyQuantity * householdSize = 2 * 4 = 8
expect(lettuceItem?.weeklyDemandKg).toBe(8);
});
});
describe('Demand Forecasting', () => {
it('should generate forecast for region', () => {
// Setup some historical data
forecaster.registerPreference(createConsumerPreference('consumer-1'));
forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test Region', 'summer');
const forecast = forecaster.generateForecast('Test Region', 12);
expect(forecast.id).toBeDefined();
expect(forecast.region).toBe('Test Region');
expect(forecast.forecasts).toBeDefined();
});
it('should include confidence intervals', () => {
forecaster.registerPreference(createConsumerPreference('consumer-1'));
forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test', 'summer');
const forecast = forecaster.generateForecast('Test', 12);
forecast.forecasts.forEach(f => {
expect(f.confidenceInterval.low).toBeLessThanOrEqual(f.predictedDemandKg);
expect(f.confidenceInterval.high).toBeGreaterThanOrEqual(f.predictedDemandKg);
});
});
it('should identify trends', () => {
forecaster.registerPreference(createConsumerPreference('consumer-1'));
forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test', 'summer');
const forecast = forecaster.generateForecast('Test', 12);
forecast.forecasts.forEach(f => {
expect(['increasing', 'stable', 'decreasing']).toContain(f.trend);
});
});
});
describe('Singleton', () => {
it('should return same instance from getDemandForecaster', () => {
const forecaster1 = getDemandForecaster();
const forecaster2 = getDemandForecaster();
expect(forecaster1).toBe(forecaster2);
});
});
describe('Serialization', () => {
it('should export to JSON', () => {
forecaster.registerPreference(createConsumerPreference('consumer-1'));
const json = forecaster.toJSON();
expect(json).toHaveProperty('preferences');
expect(json).toHaveProperty('supplyCommitments');
expect(json).toHaveProperty('demandSignals');
});
it('should import from JSON', () => {
forecaster.registerPreference(createConsumerPreference('consumer-1'));
forecaster.registerSupply(createSupplyCommitment('grower-1', 'lettuce', 50));
const json = forecaster.toJSON();
const restored = DemandForecaster.fromJSON(json);
const restoredJson = restored.toJSON() as any;
expect(restoredJson.preferences.length).toBe(1);
expect(restoredJson.supplyCommitments.length).toBe(1);
});
});
});
// Helper functions
function createConsumerPreference(
consumerId: string,
lat: number = 40.7128,
lon: number = -74.006
): ConsumerPreference {
return {
consumerId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
location: {
latitude: lat,
longitude: lon,
maxDeliveryRadiusKm: 25,
city: 'New York',
},
dietaryType: ['omnivore'],
allergies: [],
dislikes: [],
preferredCategories: ['leafy_greens', 'nightshades'],
preferredItems: [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false },
{ produceType: 'tomato', category: 'nightshades', priority: 'preferred', weeklyQuantity: 2, seasonalOnly: false },
],
certificationPreferences: ['organic', 'local'],
freshnessImportance: 5,
priceImportance: 3,
sustainabilityImportance: 4,
deliveryPreferences: {
method: ['home_delivery'],
frequency: 'weekly',
preferredDays: ['saturday'],
},
householdSize: 3,
weeklyBudget: 100,
currency: 'USD',
};
}
function createSupplyCommitment(
growerId: string,
produceType: string,
quantity: number
): SupplyCommitment {
return {
id: `supply-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
growerId,
timestamp: new Date().toISOString(),
produceType,
committedQuantityKg: quantity,
availableFrom: new Date().toISOString(),
availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
pricePerKg: 5,
currency: 'USD',
minimumOrderKg: 1,
certifications: ['organic'],
freshnessGuaranteeHours: 48,
deliveryRadiusKm: 50,
deliveryMethods: ['grower_delivery', 'customer_pickup'],
status: 'available',
remainingKg: quantity,
};
}