From 155828c84fdb56abd70460e1312fa1f9a5609e46 Mon Sep 17 00:00:00 2001 From: Clayton Date: Tue, 28 Jul 2026 14:24:36 -0500 Subject: [PATCH] fix(engine): Mockingbird copy ceiling uses cast-payment stamp (#6440) Resolve AmountSpentToCastSource ceilings from the entering object's mana_spent_to_cast_amount (objects/liminal dual lookup), including Some(0) when never cast. Stop reading PendingSpellResolution.actual_mana_spent so Chord put-into-play cannot open an unconstrained MV filter. Co-authored-by: Cursor --- crates/engine/src/ai_support/copy.rs | 21 ++ crates/engine/src/game/engine_replacement.rs | 291 ++++++++++++++++- ...ue_6440_mockingbird_uncast_copy_ceiling.rs | 296 ++++++++++++++++++ crates/engine/tests/integration/main.rs | 1 + 4 files changed, 603 insertions(+), 6 deletions(-) create mode 100644 crates/engine/tests/integration/issue_6440_mockingbird_uncast_copy_ceiling.rs diff --git a/crates/engine/src/ai_support/copy.rs b/crates/engine/src/ai_support/copy.rs index 70e1be4b49..0d6af00d1f 100644 --- a/crates/engine/src/ai_support/copy.rs +++ b/crates/engine/src/ai_support/copy.rs @@ -12,6 +12,10 @@ pub fn copy_target_filter(effect_def: &AbilityDefinition) -> Option<&TargetFilte } pub fn copy_target_mana_value_ceiling( + // Callers must pass the *entering object's* cast-payment stamp + // (`GameObject::mana_spent_to_cast_amount`, including 0 when never cast), + // or a pre-cast AI projection of that stamp — never an unrelated resolving + // spell's `PendingSpellResolution.actual_mana_spent` (issue #6440). actual_mana_spent: u32, effect_def: &AbilityDefinition, ) -> Option { @@ -90,6 +94,23 @@ mod tests { assert_eq!(copy_target_mana_value_ceiling(4, &effect), Some(4)); } + #[test] + fn typed_copy_limit_zero_stamp_is_some_zero_not_unconstrained() { + // Issue #6440: uncast entry stamp is 0 — ceiling must be Some(0), never + // collapsing to None (which find_copy_targets treats as unconstrained). + let effect = AbilityDefinition::new( + AbilityKind::Spell, + Effect::BecomeCopy { + target: TargetFilter::Any, + recipient: TargetFilter::SelfRef, + duration: Some(Duration::Permanent), + mana_value_limit: Some(CopyManaValueLimit::AmountSpentToCastSource), + additional_modifications: Vec::new(), + }, + ); + assert_eq!(copy_target_mana_value_ceiling(0, &effect), Some(0)); + } + #[test] fn generic_clone_text_has_no_mana_ceiling() { let effect = copy_effect( diff --git a/crates/engine/src/game/engine_replacement.rs b/crates/engine/src/game/engine_replacement.rs index 7022560ffd..2d529a37e4 100644 --- a/crates/engine/src/game/engine_replacement.rs +++ b/crates/engine/src/game/engine_replacement.rs @@ -2151,12 +2151,17 @@ pub(super) fn apply_post_replacement_effect( state: &mut GameState, effect_def: &AbilityDefinition, object_id: Option, - spell_resolution: Option<&crate::types::game_state::PendingSpellResolution>, + // CR 400.7d: for AmountSpentToCastSource ceilings the entering object's + // cast-payment stamp is authoritative — this spell-resolution context is + // intentionally unused (issue #6440). Kept so call sites stay stable. + _spell_resolution: Option<&crate::types::game_state::PendingSpellResolution>, event: Option<&ReplacementEvent>, replacement_applied: HashSet, events: &mut Vec, ) -> Option { - let (source_id, controller) = object_id + // Dual objects→liminal lookup (same as source identity): also yields the + // entering object's cast-payment stamp for copy MV ceilings. + let (source_id, controller, mana_spent_stamp) = object_id .and_then(|obj_id| { state .objects @@ -2167,9 +2172,15 @@ pub(super) fn apply_post_replacement_effect( .get(&obj_id) .map(|entry| &entry.object) }) - .map(|obj| (obj_id, super::replacement::replacement_source_player(obj))) + .map(|obj| { + ( + obj_id, + super::replacement::replacement_source_player(obj), + obj.mana_spent_to_cast_amount, + ) + }) }) - .unwrap_or((ObjectId(0), state.active_player)); + .unwrap_or((ObjectId(0), state.active_player, 0)); // CR 614.1c: Walk past modifier-only effects (Tap/Untap/PutCounter/ChangeZone) // in the sub_ability chain to find the real work. Composable replacements like @@ -2181,8 +2192,12 @@ pub(super) fn apply_post_replacement_effect( .unwrap_or(effect_def); if let Effect::BecomeCopy { ref target, .. } = *real_work.effect { - let max_mana_value = spell_resolution - .and_then(|ctx| copy_target_mana_value_ceiling(ctx.actual_mana_spent, real_work)); + // CR 400.7d: a permanent may reference mana spent to cast the spell that + // became it. Uncast put-onto-battlefield (Chord of Calling → Mockingbird, + // issue #6440) never received cast finalization (CR 601.2h is the stamp + // *write* site), so the stamp stays 0 and the ceiling is Some(0) — never + // unconstrained None from a missing PendingSpellResolution. + let max_mana_value = copy_target_mana_value_ceiling(mana_spent_stamp, real_work); let valid_targets = find_copy_targets(state, target, source_id, controller, max_mana_value); if valid_targets.is_empty() { return None; @@ -6773,4 +6788,268 @@ mod tests { "deferred entry must remain empty for an unrelated choice" ); } + + /// Issue #6440 — Mockingbird-class AmountSpentToCastSource ceiling. + fn mockingbird_become_copy() -> AbilityDefinition { + use crate::types::ability::{CopyManaValueLimit, Duration, Effect, TargetFilter}; + AbilityDefinition::new( + AbilityKind::Spell, + Effect::BecomeCopy { + target: TargetFilter::Any, + recipient: TargetFilter::SelfRef, + duration: Some(Duration::Permanent), + mana_value_limit: Some(CopyManaValueLimit::AmountSpentToCastSource), + additional_modifications: Vec::new(), + }, + ) + } + + fn clone_become_copy() -> AbilityDefinition { + use crate::types::ability::{Duration, Effect, TargetFilter}; + AbilityDefinition::new( + AbilityKind::Spell, + Effect::BecomeCopy { + target: TargetFilter::Any, + recipient: TargetFilter::SelfRef, + duration: Some(Duration::Permanent), + mana_value_limit: None, + additional_modifications: Vec::new(), + }, + ) + } + + fn make_creature_with_mv( + state: &mut GameState, + owner: PlayerId, + name: &str, + mv: u32, + ) -> ObjectId { + let id = make_creature(state, owner, name); + let obj = state.objects.get_mut(&id).unwrap(); + obj.mana_cost = crate::types::mana::ManaCost::generic(mv); + id + } + + fn chord_like_spell_resolution( + chord_id: ObjectId, + actual_mana_spent: u32, + ) -> crate::types::game_state::PendingSpellResolution { + use crate::types::game_state::{CastingVariant, PendingSpellResolution}; + PendingSpellResolution { + object_id: chord_id, + controller: PlayerId(0), + casting_variant: CastingVariant::Normal, + cast_from_zone: None, + cast_controller: None, + cast_timing_permission: None, + spell_targets: vec![], + actual_mana_spent, + kickers_paid: vec![], + additional_cost_payment_count: 0, + additional_cost_payments: vec![], + convoked_creatures: vec![], + } + } + + /// Hostile: uncast stamp 0 + Chord-like ctx spent 5 → ceiling Some(0); MV2 excluded. + #[test] + fn issue_6440_uncast_stamp_ignores_spell_resolution_mana_spent() { + let mut state = GameState::new_two_player(42); + let mv0 = make_creature_with_mv(&mut state, PlayerId(0), "Mv0", 0); + let mv2 = make_creature_with_mv(&mut state, PlayerId(0), "Mv2", 2); + let mockingbird = make_creature(&mut state, PlayerId(0), "Mockingbird"); + // Uncast: stamp stays at default 0. + assert_eq!(state.objects[&mockingbird].mana_spent_to_cast_amount, 0); + + let chord = create_object( + &mut state, + CardId(99), + PlayerId(0), + "Chord of Calling".to_string(), + Zone::Stack, + ); + let hostile_ctx = chord_like_spell_resolution(chord, 5); + let mut events = Vec::new(); + let waiting = apply_post_replacement_effect( + &mut state, + &mockingbird_become_copy(), + Some(mockingbird), + Some(&hostile_ctx), + None, + Default::default(), + &mut events, + ); + + match waiting { + Some(WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + .. + }) => { + assert_eq!( + max_mana_value, + Some(0), + "uncast stamp must yield Some(0), not unconstrained None" + ); + assert!( + valid_targets.contains(&mv0), + "MV 0 must remain legal; got {valid_targets:?}" + ); + assert!( + !valid_targets.contains(&mv2), + "MV 2 must be excluded under ceiling 0; got {valid_targets:?}" + ); + } + other => panic!("expected CopyTargetChoice, got {other:?}"), + } + } + + /// Cast stamp 2 → ceiling Some(2); ctx spent 99 must not override. + #[test] + fn issue_6440_cast_stamp_drives_ceiling_not_ctx() { + let mut state = GameState::new_two_player(42); + let mv2 = make_creature_with_mv(&mut state, PlayerId(0), "Mv2", 2); + let mv3 = make_creature_with_mv(&mut state, PlayerId(0), "Mv3", 3); + let mockingbird = make_creature(&mut state, PlayerId(0), "Mockingbird"); + state + .objects + .get_mut(&mockingbird) + .unwrap() + .mana_spent_to_cast_amount = 2; + + let chord = create_object( + &mut state, + CardId(99), + PlayerId(0), + "Unrelated Spell".to_string(), + Zone::Stack, + ); + let hostile_ctx = chord_like_spell_resolution(chord, 99); + let mut events = Vec::new(); + let waiting = apply_post_replacement_effect( + &mut state, + &mockingbird_become_copy(), + Some(mockingbird), + Some(&hostile_ctx), + None, + Default::default(), + &mut events, + ); + + match waiting { + Some(WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + .. + }) => { + assert_eq!(max_mana_value, Some(2)); + assert!(valid_targets.contains(&mv2)); + assert!(!valid_targets.contains(&mv3)); + } + other => panic!("expected CopyTargetChoice, got {other:?}"), + } + } + + /// Liminal-only source: stamp read via dual lookup (Gap 1). + #[test] + fn issue_6440_liminal_stamp_drives_ceiling() { + use crate::types::game_state::{LiminalEntry, LiminalEntryKind}; + use crate::types::zones::EtbTapState; + + let mut state = GameState::new_two_player(42); + let mv0 = make_creature_with_mv(&mut state, PlayerId(0), "Mv0", 0); + let mv2 = make_creature_with_mv(&mut state, PlayerId(0), "Mv2", 2); + + let liminal_id = ObjectId(state.next_object_id); + state.next_object_id += 1; + let mut liminal = GameObject::new( + liminal_id, + CardId(50), + PlayerId(0), + "Liminal Mockingbird".to_string(), + Zone::Battlefield, + ); + liminal.card_types.core_types.push(CoreType::Creature); + liminal.mana_spent_to_cast_amount = 0; + assert!(!state.objects.contains_key(&liminal_id)); + state.liminal_entries.insert( + liminal_id, + LiminalEntry { + object: liminal, + name: "Liminal Mockingbird".to_string(), + source_id: ObjectId(999), + controller: PlayerId(0), + enters_attacking: false, + attach_to: None, + sacrifice_at: None, + remaining_count: 0, + created_ids: Vec::new(), + copy_resume: None, + spec_resume: None, + enter_tapped: EtbTapState::Unspecified, + enter_with_counters: Vec::new(), + kind: LiminalEntryKind::Token, + replacement_applied: HashSet::new(), + }, + ); + + let mut events = Vec::new(); + let waiting = apply_post_replacement_effect( + &mut state, + &mockingbird_become_copy(), + Some(liminal_id), + None, + None, + Default::default(), + &mut events, + ); + + match waiting { + Some(WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + .. + }) => { + assert_eq!(max_mana_value, Some(0)); + assert!(valid_targets.contains(&mv0)); + assert!(!valid_targets.contains(&mv2)); + } + other => panic!("expected CopyTargetChoice from liminal stamp, got {other:?}"), + } + } + + /// Clone sibling: no mana_value_limit → unconstrained even when uncast. + #[test] + fn issue_6440_clone_without_limit_stays_unconstrained() { + let mut state = GameState::new_two_player(42); + let mv2 = make_creature_with_mv(&mut state, PlayerId(0), "Mv2", 2); + let clone = make_creature(&mut state, PlayerId(0), "Clone"); + assert_eq!(state.objects[&clone].mana_spent_to_cast_amount, 0); + + let mut events = Vec::new(); + let waiting = apply_post_replacement_effect( + &mut state, + &clone_become_copy(), + Some(clone), + Some(&chord_like_spell_resolution(ObjectId(1), 5)), + None, + Default::default(), + &mut events, + ); + + match waiting { + Some(WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + .. + }) => { + assert_eq!(max_mana_value, None); + assert!( + valid_targets.contains(&mv2), + "Clone without limit must still allow MV 2" + ); + } + other => panic!("expected CopyTargetChoice, got {other:?}"), + } + } } diff --git a/crates/engine/tests/integration/issue_6440_mockingbird_uncast_copy_ceiling.rs b/crates/engine/tests/integration/issue_6440_mockingbird_uncast_copy_ceiling.rs new file mode 100644 index 0000000000..5b473440e1 --- /dev/null +++ b/crates/engine/tests/integration/issue_6440_mockingbird_uncast_copy_ceiling.rs @@ -0,0 +1,296 @@ +//! Issue #6440: Mockingbird put onto the battlefield via Chord of Calling +//! (never cast) must not treat "mana spent to cast this creature" as open / +//! Chord's payment. Ceiling is the entering object's cast-payment stamp +//! (default 0 when never cast) → `CopyTargetChoice.max_mana_value == Some(0)`. +//! +//! Verbatim Oracle (Scryfall): +//! Mockingbird — Flying. You may have this creature enter as a copy of any +//! creature on the battlefield with mana value less than or equal to the +//! amount of mana spent to cast this creature, except it's a Bird in +//! addition to its other types and it has flying. +//! Chord of Calling — Convoke. Search your library for a creature card with +//! mana value X or less, put it onto the battlefield, then shuffle. + +use engine::game::scenario::{GameRunner, GameScenario, P0}; +use engine::types::actions::GameAction; +use engine::types::game_state::WaitingFor; +use engine::types::identifiers::ObjectId; +use engine::types::mana::{ManaCost, ManaCostShard, ManaType, ManaUnit}; +use engine::types::phase::Phase; + +const CHORD_ORACLE: &str = "Convoke (Your creatures can help cast this spell. Each \ +creature you tap while casting this spell pays for {1} or one mana of that creature's \ +color.)\nSearch your library for a creature card with mana value X or less, put it \ +onto the battlefield, then shuffle."; + +const MOCKINGBIRD_ORACLE: &str = "Flying\nYou may have this creature enter as a copy of any \ +creature on the battlefield with mana value less than or equal to the amount of mana \ +spent to cast this creature, except it's a Bird in addition to its other types and it \ +has flying."; + +const CLONE_ORACLE: &str = + "You may have this creature enter as a copy of any creature on the battlefield."; + +fn chord_cost() -> ManaCost { + ManaCost::Cost { + shards: vec![ + ManaCostShard::X, + ManaCostShard::Green, + ManaCostShard::Green, + ManaCostShard::Green, + ], + generic: 0, + } +} + +fn mockingbird_cost() -> ManaCost { + ManaCost::Cost { + shards: vec![ManaCostShard::X, ManaCostShard::Blue], + generic: 0, + } +} + +fn add_mana(runner: &mut GameRunner, ty: ManaType, count: usize) { + for _ in 0..count { + let unit = ManaUnit::new(ty, ObjectId(0), false, vec![]); + runner.state_mut().players[0].mana_pool.add(unit); + } +} + +fn add_bf_creature_with_mv(scenario: &mut GameScenario, name: &str, mv: u32) -> ObjectId { + scenario + .add_creature(P0, name, 1, 1) + .with_mana_cost(ManaCost::generic(mv)) + .id() +} + +/// Drive through optional/replacement prompts until CopyTargetChoice, then halt. +fn drive_to_copy_target_choice(runner: &mut GameRunner) -> WaitingFor { + for _ in 0..64 { + match runner.state().waiting_for.clone() { + WaitingFor::CopyTargetChoice { .. } => return runner.state().waiting_for.clone(), + WaitingFor::ReplacementChoice { .. } => { + runner + .act(GameAction::ChooseReplacement { index: 0 }) + .expect("accept enter-as-copy replacement"); + } + WaitingFor::OptionalEffectChoice { .. } => { + runner + .act(GameAction::DecideOptionalEffect { accept: true }) + .expect("accept optional enter-as-copy"); + } + WaitingFor::SearchChoice { cards, .. } => { + // Prefer Mockingbird / Clone if present; else first card. + let pick = cards + .iter() + .copied() + .find(|id| { + let name = &runner.state().objects[id].name; + name == "Mockingbird" || name == "Clone" + }) + .unwrap_or(cards[0]); + runner + .act(GameAction::SelectCards { cards: vec![pick] }) + .expect("select tutored creature"); + } + WaitingFor::OrderTriggers { .. } => { + engine::game::triggers::drain_order_triggers_with_identity(runner.state_mut()); + } + WaitingFor::ManaPayment { .. } => { + runner.act(GameAction::PassPriority).expect("pay mana"); + } + WaitingFor::Priority { .. } if runner.state().stack.is_empty() => { + panic!("stack emptied before CopyTargetChoice"); + } + WaitingFor::Priority { .. } => { + runner.act(GameAction::PassPriority).expect("pass priority"); + } + other => panic!("unexpected waiting_for while driving to copy choice: {other:?}"), + } + } + panic!("exhausted drive loop without CopyTargetChoice"); +} + +/// Chord puts Mockingbird onto BF (never cast) → ceiling Some(0); MV2 excluded. +#[test] +fn chord_uncast_mockingbird_ceiling_is_zero() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let mv0 = add_bf_creature_with_mv(&mut scenario, "Mv0 Birdfood", 0); + let mv2 = add_bf_creature_with_mv(&mut scenario, "Mv2 Threat", 2); + + scenario + .add_spell_to_library_top(P0, "Mockingbird", false) + .as_creature() + .with_mana_cost(mockingbird_cost()) + .from_oracle_text(MOCKINGBIRD_ORACLE); + + let mut chord = + scenario.add_spell_to_hand_from_oracle(P0, "Chord of Calling", true, CHORD_ORACLE); + chord.with_mana_cost(chord_cost()); + chord.from_oracle_text_with_keywords(&["Convoke"], CHORD_ORACLE); + let chord_id = chord.id(); + + let mut runner = scenario.build(); + // X=2 covers Mockingbird's printed MV (X+U with X=0 is MV1, but library card + // uses ManaCost with X shard — effective MV for search uses concretized + // value; set library card generic MV via cost: use fixed MV 2 for search). + // Ensure Mockingbird's mana_cost reports MV ≤ X. X-cost cards report MV 0 + // for the variable part when not on stack — use generic(2) for search match. + let mockingbird_id = *runner.state().players[0] + .library + .iter() + .find(|id| runner.state().objects[id].name == "Mockingbird") + .expect("Mockingbird in library"); + runner + .state_mut() + .objects + .get_mut(&mockingbird_id) + .unwrap() + .mana_cost = ManaCost::generic(2); + + add_mana(&mut runner, ManaType::Green, 5); // X=2 + GGG + let _ = runner.cast(chord_id).x(2).resolve(); + let waiting = drive_to_copy_target_choice(&mut runner); + + match waiting { + WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + source_id, + .. + } => { + assert_eq!( + max_mana_value, + Some(0), + "uncast Mockingbird ceiling must be Some(0)" + ); + assert_eq!( + runner.state().objects[&source_id].mana_spent_to_cast_amount, + 0, + "Chord-tutored Mockingbird must not carry a cast stamp" + ); + assert!( + valid_targets.contains(&mv0), + "MV 0 must be legal; got {valid_targets:?}" + ); + assert!( + !valid_targets.contains(&mv2), + "MV 2 must be excluded; got {valid_targets:?}" + ); + } + other => panic!("expected CopyTargetChoice, got {other:?}"), + } +} + +/// Cast Mockingbird paying 2 mana → ceiling Some(2). +#[test] +fn cast_mockingbird_ceiling_matches_stamp() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let mv2 = add_bf_creature_with_mv(&mut scenario, "Mv2", 2); + let mv3 = add_bf_creature_with_mv(&mut scenario, "Mv3", 3); + + let mut bird = + scenario.add_spell_to_hand_from_oracle(P0, "Mockingbird", false, MOCKINGBIRD_ORACLE); + bird.as_creature().with_mana_cost(mockingbird_cost()); + let bird_id = bird.id(); + + let mut runner = scenario.build(); + // Pay X=1 + {U} = 2 mana total. + add_mana(&mut runner, ManaType::Blue, 1); + add_mana(&mut runner, ManaType::Colorless, 1); + + let _ = runner.cast(bird_id).x(1).resolve(); + let waiting = drive_to_copy_target_choice(&mut runner); + + match waiting { + WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + source_id, + .. + } => { + assert_eq!( + runner.state().objects[&source_id].mana_spent_to_cast_amount, + 2, + "cast finalization must stamp spent mana" + ); + assert_eq!(max_mana_value, Some(2)); + assert!(valid_targets.contains(&mv2)); + assert!(!valid_targets.contains(&mv3)); + } + other => panic!("expected CopyTargetChoice, got {other:?}"), + } +} + +/// Clone put onto BF without cast: no mana_value_limit → MV2 still legal. +#[test] +fn clone_uncast_has_no_ceiling() { + let mut scenario = GameScenario::new(); + scenario.at_phase(Phase::PreCombatMain); + + let mv2 = add_bf_creature_with_mv(&mut scenario, "Mv2", 2); + + scenario + .add_spell_to_library_top(P0, "Clone", false) + .as_creature() + .with_mana_cost(ManaCost::generic(3)) + .from_oracle_text(CLONE_ORACLE); + + let mut chord = + scenario.add_spell_to_hand_from_oracle(P0, "Chord of Calling", true, CHORD_ORACLE); + chord.with_mana_cost(chord_cost()); + chord.from_oracle_text_with_keywords(&["Convoke"], CHORD_ORACLE); + let chord_id = chord.id(); + + let mut runner = scenario.build(); + add_mana(&mut runner, ManaType::Green, 6); // X=3 + GGG + let _ = runner.cast(chord_id).x(3).resolve(); + let waiting = drive_to_copy_target_choice(&mut runner); + + match waiting { + WaitingFor::CopyTargetChoice { + max_mana_value, + valid_targets, + .. + } => { + assert_eq!(max_mana_value, None, "Clone has no spent-mana ceiling"); + assert!( + valid_targets.contains(&mv2), + "Clone without limit must allow MV 2" + ); + } + other => panic!("expected CopyTargetChoice, got {other:?}"), + } +} + +/// Reach-guard: Mockingbird still parses AmountSpentToCastSource (parser unchanged). +#[test] +fn mockingbird_oracle_parses_amount_spent_limit() { + use engine::parser::oracle::parse_oracle_text; + use engine::types::ability::{CopyManaValueLimit, Effect}; + + let parsed = parse_oracle_text(MOCKINGBIRD_ORACLE, "Mockingbird", &[], &[], &[]); + let has_limit = parsed.replacements.iter().any(|rd| { + let mut cursor = rd.execute.as_deref(); + while let Some(def) = cursor { + if let Effect::BecomeCopy { + mana_value_limit: Some(CopyManaValueLimit::AmountSpentToCastSource), + .. + } = &*def.effect + { + return true; + } + cursor = def.sub_ability.as_deref(); + } + false + }); + assert!( + has_limit, + "Mockingbird must parse AmountSpentToCastSource; got {:#?}", + parsed.replacements + ); +} diff --git a/crates/engine/tests/integration/main.rs b/crates/engine/tests/integration/main.rs index cf90c0bbf6..7cc51022e9 100644 --- a/crates/engine/tests/integration/main.rs +++ b/crates/engine/tests/integration/main.rs @@ -615,6 +615,7 @@ mod issue_6157_gold_token_auto_mana_payment; mod issue_629_fractured_sanity_cycling; mod issue_6403_moonmist_mass_transform; mod issue_6416_extra_turn_resume_order; +mod issue_6440_mockingbird_uncast_copy_ceiling; mod issue_6498_portent_of_calamity; mod issue_6499_flickering_ward_protection_exemption; mod issue_6500_loreseekers_stone_hand_cost;