Spaces:
Sleeping
Sleeping
| /// Phase 1 Fallback Handler Integration Test | |
| /// | |
| /// Tests: | |
| /// 1. Load fallback runtime preview JSON | |
| /// 2. Verify canonical entries are marked with source="canonical" | |
| /// 3. Verify needs_fallback flag is set correctly | |
| /// 4. Verify fallback_bytecode is available | |
| /// 5. Execute canonical ability using fallback bytecode | |
| /// | |
| mod phase1_fallback_handler { | |
| use engine_rust::core::logic::{ | |
| Ability, AbilityContext, CanonicalAbilityProgram, CanonicalStep, CardDatabase, GameState, | |
| EFFECT_MASK_DRAW, O_DRAW, O_RETURN, | |
| }; | |
| use engine_rust::core::enums::{EffectType, TriggerType}; | |
| use std::fs; | |
| use std::sync::Arc; | |
| fn test_load_fallback_runtime_preview() { | |
| let json_path = "canonical_ability_model/reports/fallback_runtime_preview.json"; | |
| // Try from workspace root | |
| let json_str = std::fs::read_to_string(json_path) | |
| .or_else(|_| std::fs::read_to_string(format!("../{}", json_path))) | |
| .or_else(|_| std::fs::read_to_string(format!("../../{}", json_path))) | |
| .expect("Failed to read fallback runtime preview JSON from any expected location"); | |
| let db = CardDatabase::from_json(&json_str) | |
| .expect("Failed to deserialize fallback runtime preview"); | |
| // Gather statistics | |
| let mut canonical_count = 0; | |
| let mut legacy_count = 0; | |
| let mut canonical_with_fallback = 0; | |
| let mut canonical_no_fallback = 0; | |
| let mut canonical_with_program = 0; | |
| for card in db.members.values() { | |
| for ability in &card.abilities { | |
| if let Some(ref source) = ability.source { | |
| if source == "canonical" { | |
| canonical_count += 1; | |
| if !ability.fallback_bytecode.is_empty() { | |
| canonical_with_fallback += 1; | |
| } else { | |
| canonical_no_fallback += 1; | |
| } | |
| if ability.canonical_program.is_some() { | |
| canonical_with_program += 1; | |
| } | |
| } else if source == "legacy" { | |
| legacy_count += 1; | |
| } | |
| } | |
| } | |
| } | |
| println!("[PHASE1] Canonical entries: {}", canonical_count); | |
| println!("[PHASE1] Canonical with fallback bytecode: {}", canonical_with_fallback); | |
| println!("[PHASE1] Canonical without fallback bytecode: {}", canonical_no_fallback); | |
| println!("[PHASE1] Canonical with structured program: {}", canonical_with_program); | |
| println!("[PHASE1] Legacy entries: {}", legacy_count); | |
| assert!( | |
| canonical_count > 0, | |
| "Should have canonical entries loaded" | |
| ); | |
| assert!( | |
| canonical_with_fallback > 0, | |
| "Should have canonical entries with fallback bytecode" | |
| ); | |
| assert!( | |
| canonical_with_program > 0, | |
| "Should preserve canonical structured programs in fallback preview" | |
| ); | |
| } | |
| fn test_canonical_ability_uses_fallback_bytecode() { | |
| let json_path = "canonical_ability_model/reports/fallback_runtime_preview.json"; | |
| let json_str = fs::read_to_string(json_path) | |
| .or_else(|_| fs::read_to_string(format!("../{}", json_path))) | |
| .or_else(|_| fs::read_to_string(format!("../../{}", json_path))) | |
| .expect("Failed to read fallback runtime preview JSON from any expected location"); | |
| let db = CardDatabase::from_json(&json_str) | |
| .expect("Failed to deserialize fallback runtime preview"); | |
| // Find a canonical entry with fallback bytecode | |
| let mut found_canonical = false; | |
| let mut test_card_id = 0i32; | |
| let mut test_ability_idx = 0usize; | |
| for (&card_id, card) in &db.members { | |
| for (ab_idx, ability) in card.abilities.iter().enumerate() { | |
| if ability.source.as_ref().map_or(false, |s| s == "canonical") | |
| && !ability.fallback_bytecode.is_empty() | |
| && ability.bytecode.is_empty() | |
| { | |
| found_canonical = true; | |
| test_card_id = card_id; | |
| test_ability_idx = ab_idx; | |
| println!( | |
| "[PHASE1] Found canonical ability on card {} (ab #{})", | |
| card.card_no, ab_idx | |
| ); | |
| break; | |
| } | |
| } | |
| if found_canonical { | |
| break; | |
| } | |
| } | |
| assert!(found_canonical, "Should find a canonical ability with fallback bytecode"); | |
| let card = &db.members[&test_card_id]; | |
| let ability = &card.abilities[test_ability_idx]; | |
| println!("[PHASE1] Card: {}", card.card_no); | |
| println!("[PHASE1] Ability trigger: {:?}", ability.trigger); | |
| println!("[PHASE1] Bytecode length: {}", ability.bytecode.len()); | |
| println!("[PHASE1] Fallback bytecode length: {}", ability.fallback_bytecode.len()); | |
| println!("[PHASE1] Needs fallback: {}", ability.needs_fallback); | |
| // Verify bytecode_program() returns fallback bytecode | |
| let program = ability.bytecode_program(); | |
| assert!( | |
| program.len_words() > 0, | |
| "bytecode_program() should use fallback bytecode" | |
| ); | |
| // Test execution with fallback bytecode | |
| let mut state = GameState::default(); | |
| let ctx = AbilityContext { | |
| source_card_id: test_card_id, | |
| player_id: 0, | |
| activator_id: 0, | |
| area_idx: 0, | |
| ..Default::default() | |
| }; | |
| // Execute ability using fallback bytecode | |
| let fallback_bc = Arc::new(ability.fallback_bytecode.clone()); | |
| state.resolve_bytecode(&db, fallback_bc, &ctx); | |
| println!("[PHASE1] ✓ Executed canonical ability with fallback bytecode"); | |
| } | |
| fn test_fallback_vs_direct_execution_equivalence() { | |
| let json_path = "canonical_ability_model/reports/fallback_runtime_preview.json"; | |
| let json_str = fs::read_to_string(json_path) | |
| .or_else(|_| fs::read_to_string(format!("../{}", json_path))) | |
| .or_else(|_| fs::read_to_string(format!("../../{}", json_path))) | |
| .expect("Failed to read fallback runtime preview JSON from any expected location"); | |
| let db = CardDatabase::from_json(&json_str) | |
| .expect("Failed to deserialize fallback runtime preview"); | |
| // Find a legacy entry with normal bytecode | |
| let mut found_legacy = false; | |
| let mut test_card_id = 0i32; | |
| let mut test_ability_idx = 0usize; | |
| for (&card_id, card) in &db.members { | |
| for (ab_idx, ability) in card.abilities.iter().enumerate() { | |
| if ability.source.as_ref().map_or(false, |s| s == "legacy") | |
| && !ability.bytecode.is_empty() | |
| { | |
| found_legacy = true; | |
| test_card_id = card_id; | |
| test_ability_idx = ab_idx; | |
| break; | |
| } | |
| } | |
| if found_legacy { | |
| break; | |
| } | |
| } | |
| if !found_legacy { | |
| println!("[PHASE1] No legacy entries found with bytecode - skipping equivalence test"); | |
| return; | |
| } | |
| let card = &db.members[&test_card_id]; | |
| let ability = &card.abilities[test_ability_idx]; | |
| println!("[PHASE1] Testing legacy ability: {}", card.card_no); | |
| println!("[PHASE1] Bytecode length: {}", ability.bytecode.len()); | |
| // Execute legacy version | |
| let mut state_legacy = GameState::default(); | |
| let ctx = AbilityContext { | |
| source_card_id: test_card_id, | |
| player_id: 0, | |
| activator_id: 0, | |
| area_idx: 0, | |
| ..Default::default() | |
| }; | |
| let legacy_bc = Arc::new(ability.bytecode.clone()); | |
| state_legacy.resolve_bytecode(&db, legacy_bc, &ctx); | |
| println!("[PHASE1] ✓ Legacy execution succeeded"); | |
| } | |
| fn test_fallback_detection_in_enrichment() { | |
| let json_path = "canonical_ability_model/reports/fallback_runtime_preview.json"; | |
| let json_str = fs::read_to_string(json_path) | |
| .or_else(|_| fs::read_to_string(format!("../{}", json_path))) | |
| .or_else(|_| fs::read_to_string(format!("../../{}", json_path))) | |
| .expect("Failed to read fallback runtime preview JSON from any expected location"); | |
| let db = CardDatabase::from_json(&json_str) | |
| .expect("Failed to deserialize fallback runtime preview"); | |
| let mut needs_fallback_count = 0; | |
| let mut mismatches = vec![]; | |
| for card in db.members.values() { | |
| for ability in &card.abilities { | |
| // Verify consistency: needs_fallback should be true iff: | |
| // (1) source is "canonical" | |
| // (2) bytecode is empty | |
| // (3) effects are not empty | |
| // (4) fallback_bytecode is not empty | |
| let is_canonical = ability.source.as_ref().map_or(false, |s| s == "canonical"); | |
| let no_bytecode = ability.bytecode.is_empty(); | |
| let has_fallback = !ability.fallback_bytecode.is_empty(); | |
| if is_canonical && no_bytecode && has_fallback { | |
| if !ability.needs_fallback { | |
| mismatches.push(format!( | |
| "{}: should have needs_fallback=true", | |
| card.card_no | |
| )); | |
| } | |
| needs_fallback_count += 1; | |
| } | |
| if ability.needs_fallback && !is_canonical { | |
| mismatches.push(format!( | |
| "{}: needs_fallback=true but source is not canonical", | |
| card.card_no | |
| )); | |
| } | |
| } | |
| } | |
| println!("[PHASE1] Abilities marked needs_fallback: {}", needs_fallback_count); | |
| if !mismatches.is_empty() { | |
| println!("[PHASE1] Mismatches:"); | |
| for m in mismatches.iter().take(5) { | |
| println!("[PHASE1] {}", m); | |
| } | |
| } | |
| assert!( | |
| needs_fallback_count > 0, | |
| "Should have some abilities marked for fallback" | |
| ); | |
| assert!(mismatches.is_empty(), "Should have no fallback flag mismatches"); | |
| } | |
| fn test_canonical_draw_metadata_can_be_derived_without_primary_bytecode() { | |
| let json_path = "canonical_ability_model/reports/fallback_runtime_preview.json"; | |
| let json_str = fs::read_to_string(json_path) | |
| .or_else(|_| fs::read_to_string(format!("../{}", json_path))) | |
| .or_else(|_| fs::read_to_string(format!("../../{}", json_path))) | |
| .expect("Failed to read fallback runtime preview JSON from any expected location"); | |
| let db = CardDatabase::from_json(&json_str) | |
| .expect("Failed to deserialize fallback runtime preview"); | |
| let card = db.members.values().find(|card| { | |
| card.effect_mask & EFFECT_MASK_DRAW != 0 | |
| && card.abilities.iter().any(|ability| { | |
| ability.bytecode.is_empty() | |
| && ability | |
| .canonical_program | |
| .as_ref() | |
| .map(|program| program.effects.iter().any(|step| step.op == "DRAW")) | |
| .unwrap_or(false) | |
| }) | |
| }); | |
| let card = card.expect("Expected a canonical draw card with metadata derived from canonical program"); | |
| assert_ne!( | |
| card.effect_mask & EFFECT_MASK_DRAW, | |
| 0, | |
| "Draw effect mask should be set from canonical program analysis" | |
| ); | |
| } | |
| fn test_metadata_synced_enums_support_canonical_name_lookup() { | |
| assert_eq!(TriggerType::from_metadata_key("ON_PLAY"), Some(TriggerType::OnPlay)); | |
| assert_eq!(EffectType::from_metadata_key("DRAW"), Some(EffectType::Draw)); | |
| assert_eq!(EffectType::Draw.as_metadata_key(), "DRAW"); | |
| } | |
| fn test_resolve_ability_executes_canonical_draw_without_bytecode() { | |
| let db = CardDatabase::default(); | |
| let mut state = GameState::default(); | |
| state.players[0].deck.push(101); | |
| state.players[0].deck.push(202); | |
| let ability = Ability { | |
| source: Some("canonical".to_string()), | |
| canonical_program: Some(CanonicalAbilityProgram { | |
| trigger: "ON_PLAY".to_string(), | |
| effects: vec![CanonicalStep { | |
| kind: "effect".to_string(), | |
| op: "DRAW".to_string(), | |
| count: Some(2), | |
| ..Default::default() | |
| }], | |
| ..Default::default() | |
| }), | |
| ..Default::default() | |
| }; | |
| let ctx = AbilityContext { | |
| player_id: 0, | |
| activator_id: 0, | |
| source_card_id: 999, | |
| ..Default::default() | |
| }; | |
| state.resolve_ability(&db, &ability, &ctx); | |
| assert_eq!(state.players[0].hand.len(), 2); | |
| assert!(state.players[0].hand.contains(&101)); | |
| assert!(state.players[0].hand.contains(&202)); | |
| assert!(state.players[0].deck.is_empty()); | |
| } | |
| fn test_resolve_ability_uses_fallback_bytecode_when_canonical_op_is_unsupported() { | |
| let db = CardDatabase::default(); | |
| let mut state = GameState::default(); | |
| state.players[0].deck.push(303); | |
| let ability = Ability { | |
| source: Some("canonical".to_string()), | |
| canonical_program: Some(CanonicalAbilityProgram { | |
| trigger: "ON_PLAY".to_string(), | |
| effects: vec![CanonicalStep { | |
| kind: "effect".to_string(), | |
| op: "BOOST_SCORE".to_string(), | |
| value: Some(1), | |
| ..Default::default() | |
| }], | |
| ..Default::default() | |
| }), | |
| fallback_bytecode: vec![O_DRAW, 1, 0, 0, 0, O_RETURN, 0, 0, 0, 0], | |
| ..Default::default() | |
| }; | |
| let ctx = AbilityContext { | |
| player_id: 0, | |
| activator_id: 0, | |
| source_card_id: 1000, | |
| ..Default::default() | |
| }; | |
| state.resolve_ability(&db, &ability, &ctx); | |
| assert_eq!(state.players[0].hand.len(), 1); | |
| assert!(state.players[0].hand.contains(&303)); | |
| assert!(state.players[0].deck.is_empty()); | |
| } | |
| } | |