# ABOUTME: Tests for event delete with projection verification. # ABOUTME: Verifies that deleting events properly reverts projections. import time from animaltrack.db import get_db from animaltrack.events.delete import delete_event from animaltrack.events.enums import LifeStage, Origin, Outcome from animaltrack.events.payloads import AnimalCohortCreatedPayload, AnimalOutcomePayload from animaltrack.events.store import EventStore from animaltrack.migrations import run_migrations from animaltrack.projections import ProjectionRegistry from animaltrack.projections.animal_registry import AnimalRegistryProjection from animaltrack.projections.event_animals import EventAnimalsProjection from animaltrack.projections.event_log import EventLogProjection from animaltrack.projections.feed import FeedInventoryProjection from animaltrack.projections.intervals import IntervalProjection from animaltrack.projections.products import ProductsProjection from animaltrack.seeds import run_seeds from animaltrack.services.animal import AnimalService class TestEventDeleteProjections: """Tests for delete_event with projection updates.""" def test_delete_animal_outcome_reverts_status(self, tmp_path): """Deleting AnimalOutcome should revert animals to alive status.""" db_path = tmp_path / "test.db" # Set up database run_migrations(str(db_path), "migrations", verbose=False) db = get_db(str(db_path)) run_seeds(db) # Create projections and services event_store = EventStore(db) registry = ProjectionRegistry() registry.register(AnimalRegistryProjection(db)) registry.register(IntervalProjection(db)) registry.register(EventAnimalsProjection(db)) registry.register(ProductsProjection(db)) registry.register(FeedInventoryProjection(db)) registry.register(EventLogProjection(db)) animal_service = AnimalService(db, event_store, registry) ts_utc = int(time.time() * 1000) location = db.execute("SELECT id FROM locations LIMIT 1").fetchone()[0] # Create a cohort cohort_payload = AnimalCohortCreatedPayload( species="duck", count=3, origin=Origin.PURCHASED, life_stage=LifeStage.ADULT, location_id=location, ) cohort_event = animal_service.create_cohort( payload=cohort_payload, ts_utc=ts_utc, actor="test", ) # Get animal IDs animal_ids = cohort_event.entity_refs["animal_ids"] # Verify all animals are alive for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() assert row[0] == "alive" # Record outcome (sold) outcome_payload = AnimalOutcomePayload( outcome=Outcome.SOLD, resolved_ids=animal_ids, ) outcome_event = animal_service.record_outcome( payload=outcome_payload, ts_utc=ts_utc + 1000, actor="test", ) # Verify animals are now "sold" for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() assert row[0] == "sold", f"Animal {aid} should be sold, got {row[0]}" # Delete the outcome event deleted_ids = delete_event( db=db, event_store=event_store, event_id=outcome_event.id, actor="test", role="admin", cascade=False, reason="test deletion", registry=registry, ) assert len(deleted_ids) == 1 assert outcome_event.id in deleted_ids # Verify animals are back to "alive" for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() assert row[0] == "alive", f"Animal {aid} should be alive after delete, got {row[0]}" def test_delete_without_registry_does_not_revert(self, tmp_path): """Without registry projections, delete won't revert status (bug demo).""" db_path = tmp_path / "test.db" # Set up database run_migrations(str(db_path), "migrations", verbose=False) db = get_db(str(db_path)) run_seeds(db) # Create projections and services event_store = EventStore(db) registry = ProjectionRegistry() registry.register(AnimalRegistryProjection(db)) registry.register(IntervalProjection(db)) registry.register(EventAnimalsProjection(db)) registry.register(ProductsProjection(db)) registry.register(FeedInventoryProjection(db)) registry.register(EventLogProjection(db)) animal_service = AnimalService(db, event_store, registry) ts_utc = int(time.time() * 1000) location = db.execute("SELECT id FROM locations LIMIT 1").fetchone()[0] # Create a cohort cohort_payload = AnimalCohortCreatedPayload( species="duck", count=2, origin=Origin.PURCHASED, life_stage=LifeStage.ADULT, location_id=location, ) cohort_event = animal_service.create_cohort( payload=cohort_payload, ts_utc=ts_utc, actor="test", ) animal_ids = cohort_event.entity_refs["animal_ids"] # Record outcome (sold) outcome_payload = AnimalOutcomePayload( outcome=Outcome.SOLD, resolved_ids=animal_ids, ) outcome_event = animal_service.record_outcome( payload=outcome_payload, ts_utc=ts_utc + 1000, actor="test", ) # Verify animals are "sold" for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() assert row[0] == "sold" # Delete with EMPTY registry (simulating the bug) empty_registry = ProjectionRegistry() # No projections registered! deleted_ids = delete_event( db=db, event_store=event_store, event_id=outcome_event.id, actor="test", role="admin", cascade=False, reason="test deletion", registry=empty_registry, ) assert len(deleted_ids) == 1 # Bug: Animals are still "sold" because projections weren't reverted for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() # This demonstrates the bug - with empty registry, status is not reverted assert row[0] == "sold", "Without projections, animal should stay sold" def test_delete_death_outcome_reverts_to_alive(self, tmp_path): """Deleting death outcome should revert animals to alive status.""" db_path = tmp_path / "test.db" # Set up database run_migrations(str(db_path), "migrations", verbose=False) db = get_db(str(db_path)) run_seeds(db) # Create projections and services event_store = EventStore(db) registry = ProjectionRegistry() registry.register(AnimalRegistryProjection(db)) registry.register(IntervalProjection(db)) registry.register(EventAnimalsProjection(db)) registry.register(ProductsProjection(db)) registry.register(FeedInventoryProjection(db)) registry.register(EventLogProjection(db)) animal_service = AnimalService(db, event_store, registry) ts_utc = int(time.time() * 1000) location = db.execute("SELECT id FROM locations LIMIT 1").fetchone()[0] # Create a cohort cohort_payload = AnimalCohortCreatedPayload( species="duck", count=2, origin=Origin.PURCHASED, life_stage=LifeStage.ADULT, location_id=location, ) cohort_event = animal_service.create_cohort( payload=cohort_payload, ts_utc=ts_utc, actor="test", ) animal_ids = cohort_event.entity_refs["animal_ids"] # Record death outcome_payload = AnimalOutcomePayload( outcome=Outcome.DEATH, resolved_ids=animal_ids, ) outcome_event = animal_service.record_outcome( payload=outcome_payload, ts_utc=ts_utc + 1000, actor="test", ) # Verify animals are "dead" for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() assert row[0] == "dead" # Delete the outcome event with proper registry deleted_ids = delete_event( db=db, event_store=event_store, event_id=outcome_event.id, actor="test", role="admin", cascade=False, reason="test deletion", registry=registry, ) assert len(deleted_ids) == 1 # Verify animals are back to alive for aid in animal_ids: row = db.execute( "SELECT status FROM animal_registry WHERE animal_id = ?", (aid,), ).fetchone() assert row[0] == "alive", f"Animal {aid} should be alive, got {row[0]}"