test(engine): pin occurrence-exactness of the queued-context witness - #7069
Conversation
Follow-up to #7058. A review comment on that PR asked to carry an action-scoped occurrence identifier through PendingTriggerContext and ConsumedTriggerEventOccurrence so that two equal-valued ZoneChanged entries in one collector slice are consumed as separate occurrences. That remedy is rejected, on evidence: * The identity already exists. ZoneChangeRecord::turn_zone_change_index is a per-turn monotonic index assigned by restrictions::record_zone_change (its sole grower), and it participates in GameEvent's derived PartialEq. Two ZoneChanged values from distinct occurrences therefore cannot be byte-identical. Two that ARE byte-identical denote one occurrence emitted twice, which is exactly what the filter should collapse. * The finding's evidence was a #[cfg(test)] fixture. test_minimal pins the index at 0 for both events; that is a property of the fixture, not a production contract. * The remedy would regress CR 104.4b. deferred_triggers IS compared inside GameState::eq, so injecting a fresh per-instance id there makes two otherwise-identical loop iterations compare unequal: the mandatory-loop draw never fires, the engine burns auto_pass_loop_max_iterations and downgrades to a CR 732.2 halt. This is the same shape that pip_id on ManaUnit caused. The sibling ledger is safe only because consumed_before_priority_trigger_events is excluded from equality -- an exclusion deferred_triggers does not have. So the deliverable is to make the shipped doc comment's asserted premise an enforced one. No type changes; GameState::eq, normalize_for_loop and loop_states_equal are untouched, and ZoneChangeRecord keeps its derive -- keeping it is the mechanism. CR 104.4b is discharged vacuously. Three regression rows pin the invariant, each verified to fail for its stated mechanism when the corresponding authority is falsified: * U5 pins the equality link at the filter authority itself; red when ZoneChangeRecord's PartialEq is hand-written to skip the index. * U6 pins the within-Library reposition family (CR 400.7 zero-bump, per CR 701.20b a reveal is not a zone change) at the real production mover; red when the zones.rs:1820 stamp is zeroed. * N5 pins allocator-to-event fidelity across the search-delivery park path, asserting pre-drain by design; red when the zones.rs:826 stamp is zeroed. It must not pass priority -- a pass runs the drain and makes the assertion vacuous. Also corrects the harness note in search_delivery_observer_dedup.rs, whose universal was already inaccurate before this change: it is scoped to park-path rows asserting what the parked observers did, and N4/N5 assert pre-drain deliberately. Four unrelated engine defects surfaced while establishing the above and are filed rather than fixed here: #7063, #7064, #7065, #7066.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe change documents ChangesZone-change occurrence exactness
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ast-grep (0.45.0)crates/engine/src/game/triggers.rsast-grep timed out on this file Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Generated for head Parse changes introduced by this PR✓ No card-parse changes detected. |
Follow-up to #7058, addressing the CodeRabbit review comment left on it.
The finding, and why its remedy is rejected
The comment asked to carry an action-scoped occurrence identifier through
PendingTriggerContextandConsumedTriggerEventOccurrence, so that two equal-valuedGameEvent::ZoneChangedentries in one collector slice are consumed as separate occurrences (CR 603.2c). It was filed as Major / Heavy lift.The finding names a real property worth guaranteeing. Its remedy is rejected on three independent grounds:
1. The identity already exists.
ZoneChangeRecord::turn_zone_change_indexis a per-turn index assigned byrestrictions::record_zone_change— its sole production grower — and it is a compared field insideGameEvent's derivedPartialEq. TwoZoneChangedvalues from distinct occurrences therefore cannot be byte-identical. Two that are byte-identical denote one occurrence emitted twice, which is precisely whatfilter_already_collected_trigger_events_fromshould collapse.2. The evidence was a test fixture. The comment reasoned from
ZoneChangeRecord::test_minimal, which leaves the index at0for both events. That is a property of the fixture, not a production contract. All four production sites that construct aZoneChangedinto an event buffer ship the allocator's value.3. The remedy would regress CR 104.4b.
deferred_triggersis compared insideGameState::eq. Injecting a fresh per-instance id there makes two otherwise-identical loop iterations compare unequal — the mandatory-loop draw never fires, the engine burnsauto_pass_loop_max_iterations, and a rules-correct draw degrades into a CR 732.2 halt. This is the same shapepip_idonManaUnitcaused. The sibling ledgerconsumed_before_priority_trigger_eventsis safe only because it is excluded from equality — an exclusiondeferred_triggersdoes not have.What this change does instead
Converts the shipped doc comment's asserted premise into an enforced one. No production code changes.
GameState::eq,normalize_for_loopandloop_states_equalare untouched, andZoneChangeRecordkeeps its derive — keeping it is the mechanism, so CR 104.4b is discharged vacuously.Three regression rows, each verified to fail for its stated mechanism under a targeted falsifier, not merely to fail:
U5ZoneChangeRecord'sPartialEqis hand-written to skip the indexU6zones.rslibrary-insert stamp is zeroedN5zones.rsordinary-arm stamp is zeroedN5asserts pre-drain by design and must not pass priority — a pass runs the drain, empties the queue and makes the assertion vacuous. It is guarded byassert_observers_were_parked, which requires a non-empty queue, so a wrongly-shaped slice fails loudly rather than passing silently.Also corrects the harness note in
search_delivery_observer_dedup.rs, whose universal was already inaccurate before this change (N4asserts ondeferred_triggerswithout passing priority). It is now scoped to park-path rows that assert what the parked observers did.Verification
-D warningsexit 0. Run in an isolated target dir at the candidate commit — Tilt watches a different checkout and was not cited.card-data.jsonare byte-identical (a25d3944…, 99,502,331 bytes both sides), produced by two genuinely distinct engine builds from the same pinnedAtomicCards.json. Non-degenerate: 35,657 rows, 35,286 with non-emptyparse_detailson both sides.docs/MagicCompRules.txt.Escalated, not fixed here
Four engine defects surfaced while establishing the above. Each is filed rather than folded in:
ChangesZoneAllobservers over-fire on within-library repositions, and the zone-change ledger is polluted (so gating the matcher alone would be the wrong fix)EffectZoneChoiceSelectCardsarm accepts duplicate card ids, while five sibling validators reject them; duplicates traverse the multiplayer transport intactdeferred_triggerssurvives the turn boundary whileturn_zone_change_indexresets (cross-turn occurrence-witness aliasing)0on restorePlus #7068, a measurement-tooling defect found by this work:
coverage-parse-diffcan report a spuriousoracle_changedand silently skip cards due to nondeterministic array ordering colliding on lowercased names.Known bounds, stated rather than implied
coverage-reportprojects intoparse_details. Replacement-effect details are known not to be projected there, which is why the byte-identity ofcard-data.json— not the comparator's clean verdict — is the load-bearing evidence.Summary by CodeRabbit
Bug Fixes
Tests