localgreenchain/__tests__/integration/seed-to-seed.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

532 lines
18 KiB
TypeScript

/**
* Seed-to-Seed Lifecycle Integration Tests
* Tests complete lifecycle from seed acquisition to seed saving
*/
import { TransportChain, setTransportChain } from '../../lib/transport/tracker';
import {
SeedAcquisitionEvent,
PlantingEvent,
GrowingTransportEvent,
HarvestEvent,
SeedSavingEvent,
SeedSharingEvent,
TransportLocation,
} from '../../lib/transport/types';
describe('Seed-to-Seed Lifecycle', () => {
let chain: TransportChain;
beforeEach(() => {
chain = new TransportChain(1); // Low difficulty for faster tests
setTransportChain(chain);
});
describe('Complete Lifecycle', () => {
it('should track complete lifecycle from seed acquisition to seed saving', () => {
const seedBatchId = 'lifecycle-batch-001';
const plantIds = ['plant-lifecycle-001', 'plant-lifecycle-002'];
const newSeedBatchId = 'lifecycle-batch-002';
// Step 1: Seed Acquisition
const seedAcquisition: SeedAcquisitionEvent = {
id: 'lifecycle-seed-001',
timestamp: new Date().toISOString(),
eventType: 'seed_acquisition',
fromLocation: createLocation('seed_bank', 'Heritage Seed Bank'),
toLocation: createLocation('greenhouse', 'Local Grower Greenhouse'),
distanceKm: 50,
durationMinutes: 60,
transportMethod: 'electric_vehicle',
carbonFootprintKg: 0,
senderId: 'seed-bank-heritage',
receiverId: 'grower-local-001',
status: 'verified',
seedBatchId,
sourceType: 'seed_bank',
species: 'Solanum lycopersicum',
variety: 'Brandywine',
quantity: 50,
quantityUnit: 'seeds',
generation: 3,
germinationRate: 92,
certifications: ['heirloom', 'organic'],
};
const block1 = chain.recordEvent(seedAcquisition);
expect(block1.transportEvent.eventType).toBe('seed_acquisition');
// Step 2: Planting
const planting: PlantingEvent = {
id: 'lifecycle-planting-001',
timestamp: new Date(Date.now() + 1000).toISOString(),
eventType: 'planting',
fromLocation: createLocation('greenhouse', 'Local Grower Greenhouse'),
toLocation: createLocation('greenhouse', 'Local Grower Greenhouse'),
distanceKm: 0,
durationMinutes: 30,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'grower-local-001',
receiverId: 'grower-local-001',
status: 'verified',
seedBatchId,
plantIds,
plantingMethod: 'indoor_start',
quantityPlanted: 2,
growingEnvironment: 'greenhouse',
expectedHarvestDate: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(),
};
const block2 = chain.recordEvent(planting);
expect(block2.transportEvent.eventType).toBe('planting');
// Step 3: Growing Transport (transplant to outdoor garden)
const transplant: GrowingTransportEvent = {
id: 'lifecycle-transplant-001',
timestamp: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days later
eventType: 'growing_transport',
fromLocation: createLocation('greenhouse', 'Local Grower Greenhouse'),
toLocation: createLocation('farm', 'Local Community Garden'),
distanceKm: 2,
durationMinutes: 20,
transportMethod: 'bicycle',
carbonFootprintKg: 0,
senderId: 'grower-local-001',
receiverId: 'grower-local-001',
status: 'verified',
plantIds,
reason: 'transplant',
plantStage: 'vegetative',
handlingMethod: 'potted',
rootDisturbance: 'minimal',
acclimatizationRequired: true,
acclimatizationDays: 7,
};
const block3 = chain.recordEvent(transplant);
expect(block3.transportEvent.eventType).toBe('growing_transport');
// Step 4: Harvest
const harvest: HarvestEvent = {
id: 'lifecycle-harvest-001',
timestamp: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString(), // 90 days after planting
eventType: 'harvest',
fromLocation: createLocation('farm', 'Local Community Garden'),
toLocation: createLocation('warehouse', 'Local Co-op Distribution'),
distanceKm: 5,
durationMinutes: 15,
transportMethod: 'electric_vehicle',
carbonFootprintKg: 0,
senderId: 'grower-local-001',
receiverId: 'coop-distribution',
status: 'verified',
plantIds,
harvestBatchId: 'harvest-lifecycle-001',
harvestType: 'full',
produceType: 'Brandywine Tomatoes',
grossWeight: 8,
netWeight: 7.5,
weightUnit: 'kg',
qualityGrade: 'A',
packagingType: 'sustainable_crates',
temperatureRequired: { min: 12, max: 18, optimal: 15, unit: 'celsius' },
shelfLifeHours: 168,
seedsSaved: true,
seedBatchIdCreated: newSeedBatchId,
};
const block4 = chain.recordEvent(harvest);
expect(block4.transportEvent.eventType).toBe('harvest');
// Step 5: Seed Saving
const seedSaving: SeedSavingEvent = {
id: 'lifecycle-seed-saving-001',
timestamp: new Date(Date.now() + 95 * 24 * 60 * 60 * 1000).toISOString(),
eventType: 'seed_saving',
fromLocation: createLocation('farm', 'Local Community Garden'),
toLocation: createLocation('seed_bank', 'Grower Home Seed Storage'),
distanceKm: 1,
durationMinutes: 10,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'grower-local-001',
receiverId: 'grower-local-001',
status: 'verified',
parentPlantIds: plantIds,
newSeedBatchId,
collectionMethod: 'fermentation',
seedCount: 200,
germinationRate: 88,
storageConditions: {
temperature: 10,
humidity: 30,
lightExposure: 'dark',
containerType: 'jar',
desiccant: true,
estimatedViability: 5,
},
storageLocationId: 'grower-home-storage',
newGenerationNumber: 4,
geneticNotes: 'Selected from best performing plants',
availableForSharing: true,
sharingTerms: 'trade',
};
const block5 = chain.recordEvent(seedSaving);
expect(block5.transportEvent.eventType).toBe('seed_saving');
// Verify complete journey
const journey1 = chain.getPlantJourney(plantIds[0]);
expect(journey1).not.toBeNull();
expect(journey1?.events.length).toBe(4); // planting, transplant, harvest, seed_saving
expect(journey1?.currentStage).toBe('seed_saving');
expect(journey1?.descendantSeedBatches).toContain(newSeedBatchId);
// Verify chain integrity
expect(chain.isChainValid()).toBe(true);
// Verify carbon tracking
expect(block5.cumulativeCarbonKg).toBeGreaterThanOrEqual(0);
expect(block5.cumulativeFoodMiles).toBe(
seedAcquisition.distanceKm +
planting.distanceKm +
transplant.distanceKm +
harvest.distanceKm +
seedSaving.distanceKm
);
});
it('should track multiple generations', () => {
// Generation 1 seeds
const gen1Batch = 'gen1-batch';
const gen1Plants = ['gen1-plant-001'];
const gen2Batch = 'gen2-batch';
const gen2Plants = ['gen2-plant-001'];
const gen3Batch = 'gen3-batch';
// Gen 1: Acquire, Plant, Harvest with seed saving
chain.recordEvent(createSeedAcquisition(gen1Batch, 1));
chain.recordEvent(createPlanting(gen1Batch, gen1Plants));
chain.recordEvent(createHarvest(gen1Plants, gen2Batch));
chain.recordEvent(createSeedSaving(gen1Plants, gen2Batch, 2));
// Gen 2: Plant saved seeds, Harvest with seed saving
chain.recordEvent(createSeedAcquisition(gen2Batch, 2, gen1Plants));
chain.recordEvent(createPlanting(gen2Batch, gen2Plants));
chain.recordEvent(createHarvest(gen2Plants, gen3Batch));
chain.recordEvent(createSeedSaving(gen2Plants, gen3Batch, 3));
// Verify chain
expect(chain.isChainValid()).toBe(true);
expect(chain.chain.length).toBe(9); // 1 genesis + 8 events
// Verify lineage
const journey = chain.getPlantJourney('gen2-plant-001');
expect(journey?.generation).toBe(2);
});
});
describe('Seed Sharing', () => {
it('should track seed sharing between growers', () => {
const originalBatch = 'original-batch';
const sharedQuantity = 25;
// Save seeds
const seedSaving: SeedSavingEvent = {
id: 'share-seed-saving',
timestamp: new Date().toISOString(),
eventType: 'seed_saving',
fromLocation: createLocation('farm', 'Grower A Farm'),
toLocation: createLocation('seed_bank', 'Grower A Storage'),
distanceKm: 0,
durationMinutes: 30,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'grower-a',
receiverId: 'grower-a',
status: 'verified',
parentPlantIds: ['parent-001'],
newSeedBatchId: originalBatch,
collectionMethod: 'dry_seed',
seedCount: 100,
storageConditions: {
temperature: 10,
humidity: 30,
lightExposure: 'dark',
containerType: 'envelope',
desiccant: true,
estimatedViability: 4,
},
storageLocationId: 'grower-a-storage',
newGenerationNumber: 2,
availableForSharing: true,
sharingTerms: 'trade',
};
chain.recordEvent(seedSaving);
// Share seeds
const seedSharing: SeedSharingEvent = {
id: 'share-event-001',
timestamp: new Date(Date.now() + 1000).toISOString(),
eventType: 'seed_sharing',
fromLocation: createLocation('seed_bank', 'Grower A Storage'),
toLocation: createLocation('greenhouse', 'Grower B Greenhouse'),
distanceKm: 10,
durationMinutes: 20,
transportMethod: 'bicycle',
carbonFootprintKg: 0,
senderId: 'grower-a',
receiverId: 'grower-b',
status: 'verified',
seedBatchId: originalBatch,
quantityShared: sharedQuantity,
quantityUnit: 'seeds',
sharingType: 'trade',
tradeDetails: 'Traded for basil seeds',
recipientAgreement: true,
growingCommitment: 'Will grow and save seeds',
reportBackRequired: true,
};
const block = chain.recordEvent(seedSharing);
expect(block.transportEvent.eventType).toBe('seed_sharing');
expect((block.transportEvent as SeedSharingEvent).quantityShared).toBe(sharedQuantity);
});
});
describe('Environmental Impact Across Lifecycle', () => {
it('should calculate cumulative environmental impact', () => {
const userId = 'eco-grower';
// Record full lifecycle with the same user
const events = [
{ distanceKm: 50, method: 'electric_vehicle' as const },
{ distanceKm: 0, method: 'walking' as const },
{ distanceKm: 5, method: 'bicycle' as const },
{ distanceKm: 10, method: 'electric_truck' as const },
{ distanceKm: 1, method: 'walking' as const },
];
let totalDistance = 0;
events.forEach((event, i) => {
chain.recordEvent({
id: `eco-event-${i}`,
timestamp: new Date(Date.now() + i * 1000).toISOString(),
eventType: 'seed_acquisition',
fromLocation: createLocation('farm', 'Location A'),
toLocation: createLocation('farm', 'Location B'),
distanceKm: event.distanceKm,
durationMinutes: 30,
transportMethod: event.method,
carbonFootprintKg: 0,
senderId: userId,
receiverId: userId,
status: 'verified',
seedBatchId: `batch-${i}`,
sourceType: 'previous_harvest',
species: 'Test',
quantity: 10,
quantityUnit: 'seeds',
generation: 1,
} as SeedAcquisitionEvent);
totalDistance += event.distanceKm;
});
const impact = chain.getEnvironmentalImpact(userId);
expect(impact.totalFoodMiles).toBe(totalDistance);
expect(impact.comparisonToConventional.milesSaved).toBeGreaterThan(0);
});
});
describe('QR Code Traceability', () => {
it('should generate traceable QR codes at each stage', () => {
const plantId = 'qr-trace-plant';
const batchId = 'qr-trace-batch';
// Record seed acquisition
chain.recordEvent({
id: 'qr-seed',
timestamp: new Date().toISOString(),
eventType: 'seed_acquisition',
fromLocation: createLocation('seed_bank', 'Bank'),
toLocation: createLocation('greenhouse', 'Greenhouse'),
distanceKm: 10,
durationMinutes: 20,
transportMethod: 'bicycle',
carbonFootprintKg: 0,
senderId: 'seller',
receiverId: 'buyer',
status: 'verified',
seedBatchId: batchId,
sourceType: 'seed_bank',
species: 'Test',
quantity: 10,
quantityUnit: 'seeds',
generation: 1,
} as SeedAcquisitionEvent);
// Record planting
chain.recordEvent({
id: 'qr-planting',
timestamp: new Date(Date.now() + 1000).toISOString(),
eventType: 'planting',
fromLocation: createLocation('greenhouse', 'Greenhouse'),
toLocation: createLocation('greenhouse', 'Greenhouse'),
distanceKm: 0,
durationMinutes: 10,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'buyer',
receiverId: 'buyer',
status: 'verified',
seedBatchId: batchId,
plantIds: [plantId],
plantingMethod: 'indoor_start',
quantityPlanted: 1,
growingEnvironment: 'greenhouse',
} as PlantingEvent);
// Generate QR for plant
const plantQR = chain.generateQRData(plantId, undefined);
expect(plantQR.plantId).toBe(plantId);
expect(plantQR.lastEventType).toBe('planting');
// Generate QR for batch
const batchQR = chain.generateQRData(undefined, batchId);
expect(batchQR.batchId).toBe(batchId);
// Both should have valid verification codes
expect(plantQR.verificationCode).toMatch(/^[A-F0-9]{8}$/);
expect(batchQR.verificationCode).toMatch(/^[A-F0-9]{8}$/);
});
});
});
// Helper functions
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 createSeedAcquisition(
batchId: string,
generation: number,
parentPlantIds?: string[]
): SeedAcquisitionEvent {
return {
id: `seed-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
timestamp: new Date().toISOString(),
eventType: 'seed_acquisition',
fromLocation: createLocation('seed_bank', 'Bank'),
toLocation: createLocation('greenhouse', 'Greenhouse'),
distanceKm: 10,
durationMinutes: 20,
transportMethod: 'electric_vehicle',
carbonFootprintKg: 0,
senderId: 'sender',
receiverId: 'receiver',
status: 'verified',
seedBatchId: batchId,
sourceType: parentPlantIds ? 'previous_harvest' : 'seed_bank',
species: 'Test Species',
quantity: 10,
quantityUnit: 'seeds',
generation,
parentPlantIds,
};
}
function createPlanting(batchId: string, plantIds: string[]): PlantingEvent {
return {
id: `planting-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
timestamp: new Date(Date.now() + 100).toISOString(),
eventType: 'planting',
fromLocation: createLocation('greenhouse', 'Greenhouse'),
toLocation: createLocation('greenhouse', 'Greenhouse'),
distanceKm: 0,
durationMinutes: 10,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'grower',
receiverId: 'grower',
status: 'verified',
seedBatchId: batchId,
plantIds,
plantingMethod: 'indoor_start',
quantityPlanted: plantIds.length,
growingEnvironment: 'greenhouse',
};
}
function createHarvest(plantIds: string[], newSeedBatchId: string): HarvestEvent {
return {
id: `harvest-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
timestamp: new Date(Date.now() + 200).toISOString(),
eventType: 'harvest',
fromLocation: createLocation('farm', 'Farm'),
toLocation: createLocation('warehouse', 'Warehouse'),
distanceKm: 5,
durationMinutes: 20,
transportMethod: 'electric_truck',
carbonFootprintKg: 0,
senderId: 'grower',
receiverId: 'distributor',
status: 'verified',
plantIds,
harvestBatchId: `harvest-batch-${Date.now()}`,
harvestType: 'full',
produceType: 'Test Produce',
grossWeight: 5,
netWeight: 4.5,
weightUnit: 'kg',
packagingType: 'crates',
temperatureRequired: { min: 10, max: 20, optimal: 15, unit: 'celsius' },
shelfLifeHours: 168,
seedsSaved: true,
seedBatchIdCreated: newSeedBatchId,
};
}
function createSeedSaving(
parentPlantIds: string[],
newSeedBatchId: string,
generation: number
): SeedSavingEvent {
return {
id: `save-${Date.now()}-${Math.random().toString(36).substr(2, 5)}`,
timestamp: new Date(Date.now() + 300).toISOString(),
eventType: 'seed_saving',
fromLocation: createLocation('farm', 'Farm'),
toLocation: createLocation('seed_bank', 'Storage'),
distanceKm: 0,
durationMinutes: 30,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'grower',
receiverId: 'grower',
status: 'verified',
parentPlantIds,
newSeedBatchId,
collectionMethod: 'dry_seed',
seedCount: 50,
storageConditions: {
temperature: 10,
humidity: 30,
lightExposure: 'dark',
containerType: 'envelope',
desiccant: true,
estimatedViability: 4,
},
storageLocationId: 'grower-storage',
newGenerationNumber: generation,
availableForSharing: true,
};
}