Phase 37 — Integration Fixes & E2E Validation¶
Status: Complete Block: 4 (Tightening) Tests: 70 new (24 Python unit + 41 E2E parametrized + 5 frontend vitest) Files: 6 modified + 6 new
Summary¶
Phase 37 fixes three critical integration bugs that surface during real web UI use and adds an E2E smoke test across all 41 scenarios. Zero new engine subsystems — purely wiring and fixing existing code.
What Was Built¶
37a: Bug Fixes (6 modified files)¶
Bug 1 — config_overrides never applied (api/run_manager.py):
- _run_sync() received config_overrides but never merged them into the YAML dict
- Added _apply_overrides() static method for recursive deep merge (nested dicts merge, scalars replace, lists replace)
- Called after yaml.safe_load() and before ScenarioLoader.load()
Bug 2 — Reinforcement events not published (stochastic_warfare/simulation/campaign.py + frontend/src/lib/eventProcessing.ts):
- check_reinforcements() logged arrivals but published no event — frontend force charts never increased
- Added ReinforcementArrivedEvent frozen dataclass (side, unit_count, unit_types) in campaign.py
- Published via EventBus with safe getattr(ctx, "clock", None) fallback for backward compat with SimpleNamespace test mocks
- Frontend buildForceTimeSeries() now handles both destruction and reinforcement events in a single sorted pass
Bug 3 — DEW battle loop wiring (stochastic_warfare/simulation/battle.py + stochastic_warfare/combat/engagement.py):
- _execute_engagements() called execute_engagement() directly — DEW weapons used ballistic physics instead of Beer-Lambert
- Changed to call route_engagement() which dispatches to DEW engine for DIRECTED_ENERGY weapons
- Detects DEW via parsed_category() == WeaponCategory.DIRECTED_ENERGY + beam_power_kw > 0 (laser) vs == 0 (HPM)
- Updated route_engagement() to propagate HitResult from DEW engine results (was missing — needed for damage chain)
- DEW hits in battle loop result in destruction (thermal/EMP kill)
37b: Tests (4 new test files, 29 tests)¶
| File | Tests | Covers |
|---|---|---|
tests/api/test_config_overrides.py |
8 | _apply_overrides deep merge (flat, nested, list, empty, type changes) |
tests/unit/test_reinforcement_events.py |
8 | ReinforcementArrivedEvent publishing (arrival, source, waves, no-clock fallback) |
tests/unit/test_dew_battle_wiring.py |
8 | DEW routing via route_engagement, HitResult propagation, category detection |
frontend/src/__tests__/lib/eventProcessing.reinforcement.test.ts |
5 | Force chart reinforcement handling (increment, mixed, unknown side, defaults) |
37c: E2E Smoke Test (1 new test file, 41 parametrized tests)¶
tests/e2e/test_scenario_smoke.py— all 41 scenarios submitted via API- 33 scenarios complete successfully within 20 ticks
- 8 scenarios xfail due to legacy YAML format (missing campaign schema fields: sides, terrain, date)
- Registered
e2emarker inpyproject.toml, excluded from default test runs
Design Decisions¶
-
Event placed in campaign.py, not separate events file:
ReinforcementArrivedEventlives alongside the code that publishes it. Avoids creating a new file for a single dataclass. -
getattr(ctx, "clock", None)for backward compat: Existing tests useSimpleNamespacemocks withoutclock. Safe attribute access preventsAttributeErrorwithout requiring test updates. Follows Phase 25 pattern. -
HitResult propagation in route_engagement(): The DEW wrapping in
route_engagement()previously didn't populatehit_result. This meant the battle loop damage chain (which checksresult.hit_result.hit) would never trigger for DEW. Fixed by creating aHitResultfromdew_result.pkanddew_result.hit. -
E2E xfail for legacy scenarios: 8 scenarios use a simpler YAML format (pre-campaign schema). Rather than masking failures, they're marked
xfailto document the gap. Schema migration is a future task. -
Deep merge, not temp file: Config overrides are merged into the dict in-memory rather than writing a temp YAML file. Simpler, no file cleanup needed.
Deviations from Plan¶
The implementation followed a focused plan (3 bugs + E2E) rather than the full Phase 37 doc plan. Descoped items:
| Item | Reason | Status |
|---|---|---|
api/routers/meta.py terrain-types-from-data |
Not in focused plan | Deferred to 39d |
air_defense.py ADUnitType.DEW routing |
Not in focused plan | Deferred |
| Scenario YAML with dew_config | Not in focused plan | Deferred |
| DEWEngagementEvent subscriber | NOT NEEDED — SimulationRecorder subscribes to base Event | Closed |
| Scenario editor E2E tests | Not in focused plan | Deferred to 39a |
Known Limitations¶
-
8 legacy-format scenarios can't run through API: 73_easting, bekaa_valley_1982, cbrn_chemical_defense, cbrn_nuclear_tactical, falklands_naval, golan_heights, gulf_war_ew_1991, test_scenario. These use a simpler YAML format without
sides/terrain/datefields required byCampaignScenarioConfig. -
DEW hit = always destruction: When a DEW weapon hits, the battle loop treats it as destruction. No partial damage / disable path. Acceptable for thermal/EMP kills but could be refined.
-
No scenario exercises DEW in E2E: No scenario YAML includes
dew_config, so the DEW routing path is only tested at unit level, not in full simulation runs.
Lessons Learned¶
ModuleIddoesn't haveSIMULATION: UsedModuleId.COREinstead. Check enum values before using them in new event classes.AmmoStateusesrounds_by_typedict, not positional args: Tests must construct viaAmmoState()then populaterounds_by_type[ammo_id]. Don't guess constructor signatures.- SimpleNamespace test mocks need all accessed fields: When new code accesses
ctx.clock, every test mock must include it.getattr()is the safety net. - E2E tests find real issues: 8 of 41 scenarios actually can't load through the API. This is exactly what smoke tests are for.
route_engagement()wrapping must be complete: When wrapping a domain-specific result (DEWEngagementResult) into a generic result (EngagementResult), all fields needed by downstream consumers must be populated. Thehit_resultgap was a real integration bug.
Postmortem¶
- Scope: Under — focused plan was smaller than full doc plan (3 bugs + E2E vs full DEW wiring + terrain types + E2E)
- Quality: High — tests cover edge cases, real engines used (not just mocks), E2E finds real issues
- Integration: Fully wired for what was delivered. ReinforcementArrivedEvent auto-captured by SimulationRecorder.
- Performance: No regression (125s test suite unchanged)
- Deficits: 1 new (legacy scenario format). 3 deferred from original plan (terrain types, AD DEW routing, scenario dew_config). These remain in the deficit index for future phases.
Deficit Resolution¶
| Deficit | Origin | Status |
|---|---|---|
| route_engagement() not called from battle.py | Phase 28.5 | Resolved |
| config_overrides accepted but not applied | Phase 32 | Resolved |
| Force time series assumes no reinforcements | Phase 34 | Resolved |
| DEWEngagementEvent has zero subscribers | Phase 28.5 | Closed (recorder catches via base Event) |
| dew_engine not used in simulation tick loops | Phase 28.5 | Resolved (routed via battle.py) |
| No scenario YAML references dew_config | Phase 28.5 | Deferred |
| ADUnitType.DEW not handled in air defense | Phase 28.5 | Deferred |
| GET /api/meta/terrain-types hardcoded | Phase 32 | Deferred to 39d |