test(engine): regression-guard Sun Titan enters-or-attacks graveyard-return trigger (#5264)#5671
Conversation
|
Note Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported. |
Parse changes introduced by this PR✓ No card-parse changes detected. |
matthewevans
left a comment
There was a problem hiding this comment.
Your investigation is right and useful — but all three tests restate coverage that already exists, and none of them drives the one path that genuinely isn't covered. Redirect them and this becomes worth merging.
✅ Your diagnosis is correct — I confirmed it independently
Sun Titan parses correctly on main. From the generated card data:
"triggers": [{"mode": "EntersOrAttacks", "execute": {"effect": {"type": "ChangeZone",
"origin": "Graveyard", "destination": "Battlefield",
"target": {"type": "Typed", "type_filters": ["Permanent"], "controller": "You",
"properties": [{"type": "Cmc", "comparator": "LE", "value": {"type":"Fixed","value":3}},
{"type": "InZone", "zone": "Graveyard"}]}}}}]EntersOrAttacks ✓, graveyard→battlefield ✓, Permanent + Cmc ≤ 3 + InZone Graveyard + controller: You ✓. There is no engine defect, and your read of the reported symptom (a graveyard holding no MV ≤ 3 permanent) is the right one. That conclusion belongs on #5264 as a comment — it is the valuable output here, and it stands on its own without this PR.
🔴 Blocker — the coverage bar (all three tests are already covered)
Per the repo's test-only bar, a standalone test PR must add coverage that does not already exist. Each of these does:
| Your test | Already covered by |
|---|---|
sun_titan_etb_surfaces_graveyard_target |
tests/integration/issue_3309_rise_etb_returns.rs:237 — drives Sun Titan through the real parse pipeline, fires the ETB, hits WaitingFor::TriggerTargetSelection, chooses a graveyard permanent, and asserts it lands on the battlefield. That is the ControllerRef::You + InZone{Graveyard} path end-to-end, and it is stronger than yours — it asserts the return resolves, not merely that a slot surfaces. |
sun_titan_attack_surfaces_graveyard_target |
tests/integration/ureni_attack_trigger.rs — pins the attack branch of TriggerMode::EntersOrAttacks (incl. CR 506.4, second combat). |
sun_titan_no_legal_target_is_not_placed |
trigger_target_zero_targets_skips, in the very file you're adding to — "trigger with targeting + 0 legal targets → skipped entirely", pinning the same gate at triggers.rs:3179. |
So the doc-comment claim — "the untested combination of ControllerRef::You with FilterProp::InZone { Graveyard }" — is not accurate; issue_3309 exercises exactly that at runtime.
This is also what CLAUDE.md means by "test the building block, not the special case." The trigger-mode dispatch and the target-slot builder are orthogonal — build_target_slots never learns which mode fired — so the Sun-Titan-specific cross-product of two independently-covered axes carries no risk the parts don't already carry. 219 lines into an already-enormous triggers.rs for no new signal is net-negative.
🔴 Blocker — the tests hand-build the trigger, so they cannot prove what they claim
sun_titan_scenario constructs TriggerDefinition::new(TriggerMode::EntersOrAttacks) by hand, and the doc comment asserts it "Builds Sun Titan's exact parsed trigger shape." That assertion is the thing a test should prove, not assume. Nothing here parses Sun Titan's Oracle text, so if the parser stopped producing that shape tomorrow, all three tests stay green while the card is broken — the definition of a non-discriminating guard.
The test that WOULD clear the bar
There is a real gap, and it is not the one you targeted: nothing drives Sun Titan's actual printed Oracle text end-to-end. Verified against Scryfall just now:
Vigilance
Whenever this creature enters or attacks, you may return target permanent card with mana value 3 or less from your graveyard to the battlefield.
Note what issue_3309 feeds the parser (issue_3309_rise_etb_returns.rs:20-21):
const SUN_TITAN_ORACLE: &str =
"Vigilance\nWhen Sun Titan enters, you may return target permanent card with mana value 3 or less from your graveyard to the battlefield.";That is not Sun Titan's text — it is an ETB-only fabrication with the "or attacks" half missing. So our existing "Sun Titan" test parses a card that does not exist, and the attack half of the real card is genuinely unpinned. (Our bug, not yours — I'm filing it.)
Rewrite around a single test that calls parse_oracle_text — the entry point database::synthesis actually uses to build card-data.json — on the verbatim Oracle text above, and asserts both halves fire with the graveyard target. That test would be unique, discriminating, and would have caught the fabricated const. It is the one worth having.
Recommendation
request-changes. Post the "no defect" finding on #5264 (it's the real deliverable). Replace all three tests with one that drives parse_oracle_text on Sun Titan's verbatim Oracle text across both halves. If you'd rather not, closing this and just commenting on #5264 is a perfectly good outcome — the investigation was the valuable part.
…le_text (phase-rs#5264) Address review on phase-rs#5671: the three prior tests hand-built the trigger and restated coverage that already exists (issue_3309 for the ETB→graveyard-return runtime path, ureni_attack_trigger for the attack branch, and trigger_target_zero_targets_skips for the no-legal-target gate), so they proved nothing a regression could break. Replace them with one integration test that runs Sun Titan's verbatim printed Oracle text (Vigilance + "Whenever this creature enters or attacks, you may return target permanent card with mana value 3 or less from your graveyard to the battlefield") through parse_oracle_text — the exact entry point database::synthesis uses to build card-data.json. It pins the single EntersOrAttacks trigger (both halves) and its optional graveyard→battlefield ChangeZone with the controller: You + Permanent + Cmc LE 3 + InZone Graveyard target filter. A parser regression that split, dropped, or retyped either half flips it red — the discriminating guard the hand-built version could not be. Removes the 219-line hand-built block from triggers.rs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Addressed in d6b7051 — rewrote per your review. What changed
This is the discriminating guard the hand-built version couldn't be: if the parser stops producing that shape, it flips red. Two notes on your suggested assertions:
Verification (worktree, direct cargo — Tilt doesn't watch it):
The "no engine defect" finding for #5264 still stands as the real deliverable — will post it there. |
…le_text (phase-rs#5264) Issue phase-rs#5264 reported Sun Titan's "enters or attacks" trigger not firing. Investigation (confirmed with the maintainer on phase-rs#5671) found no engine defect: the symptom is a graveyard holding no permanent card with mana value 3 or less, so the optional targeted trigger correctly finds no legal target. The genuine gap was that nothing drove Sun Titan's real, verbatim printed Oracle text through parse_oracle_text — the entry point database::synthesis uses to build card-data.json. The nearest existing "Sun Titan" fixture (issue_3309) feeds an ETB-only fabrication that drops the "or attacks" half. Add one integration test that parses the verbatim text (Vigilance + "Whenever this creature enters or attacks, you may return target permanent card with mana value 3 or less from your graveyard to the battlefield") and pins the single EntersOrAttacks trigger (both halves) plus its optional graveyard→battlefield ChangeZone with the controller: You + Permanent + Cmc LE 3 + InZone Graveyard target filter. A parser regression that split, dropped, or retyped either half flips it red — the discriminating guard a hand-built trigger could not be. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d6b7051 to
aea1e36
Compare
|
@matthewevans — rewrote per your review; final head is Addressed your blockers
Two notes
Verification (worktree, direct cargo — Tilt doesn't watch it): The "no defect" finding for #5264 is already documented there (the graveyard held no MV≤3 permanent card, so the optional targeted trigger correctly had no legal target). |
matthewevans
left a comment
There was a problem hiding this comment.
Verdict: Blocked — this test now duplicates stronger coverage already on main.
🔴 Blocker
- crates/engine/tests/integration/issue_5264_sun_titan_enters_or_attacks.rs:10 and #L28 say the existing Sun Titan fixture uses an ETB-only fabrication. That is no longer true: issue_3309_rise_etb_returns.rs:21 contains the same verbatim Oracle text, and #L41 already parses it through
parse_oracle_text, verifiesEntersOrAttacks, its optional graveyard-to-battlefield effect, and that the trigger was not split. This PR's AST assertions at #L48-#L113 add no distinct failure mode. - Main also covers the stronger runtime behavior: issue_3309_rise_etb_returns.rs:350 drives the same parsed Sun Titan through combat, target selection, the optional choice, and asserts that the graveyard permanent returns at #L414. An
"or attacks"regression therefore already fails a production-pipeline test. A parser-only duplicate cannot clear the standalone-test value bar.
🟡 Non-blocking
- The previous review's suggested gap was real when it was written, but it was filled on
mainbefore this rewrite. The current head was rebased successfully; the stale premise is the only problem.
✅ Clean
- The test is correctly registered in
tests/integration/main.rsand does not introduce production behavior or unrelated churn.
Recommendation: Request changes — remove the duplicate test (or close this PR) and retain the #5264 investigation as an issue comment rather than adding redundant suite cost.
|
Closing per review: main already has stronger Sun Titan enters-or-attacks parser and runtime coverage in issue_3309_rise_etb_returns.rs, so this PR no longer adds distinct regression value. |
Summary
Regression coverage for Sun Titan (#5264), which was reported as "its triggers don't trigger — ETB or when it attacks."
Investigation found no reproducible engine defect. Sun Titan's
EntersOrAttackstrigger fires on both halves, and itscontroller: You+InZone: Graveyardtarget filter correctly surfaces a your-graveyard permanent (MV ≤ 3) as a legal target (single legal target auto-selects straight onto the stack). The reported symptom is consistent with a graveyard that held no MV ≤ 3 permanent card — instants/sorceries and MV > 3 permanents don't qualify, so the optional targeted trigger correctly offers nothing.This PR locks in the previously-uncovered your-graveyard target path for a marquee card. No engine logic is changed — tests only.
Tests added (
crates/engine/src/game/triggers.rs, inline#[cfg(test)])sun_titan_etb_surfaces_graveyard_target— ETB half: firing a Hand→Battlefield event places the trigger with the your-graveyard MV-2 creature as a legal target.sun_titan_attack_surfaces_graveyard_target— attack half: declaring Sun Titan as an attacker fires the same trigger with the same legal target.sun_titan_no_legal_target_is_not_placed— negative control: with only an Instant, or an MV-4 permanent, in the graveyard, the optional targeted trigger is not placed (neither pending nor on the stack), on both halves. This is what makes the positive assertions discriminating: placement respects target legality.Rules
CR 108.3 / CR 109.4 — cards in a graveyard have an owner, not a controller, so "from your graveyard" resolves against ownership. CR 601.2c / 603.3d — single-legal-target triggers auto-select and go straight to the stack. All citations verified against the Comprehensive Rules.
Closes the investigation for #5264 (recommend requesting the reporter's game state to confirm; no engine change was warranted).
🤖 Generated with Claude Code