//! Integration test: content loading pipeline. //! //! Tests the full pipeline: discover content → deserialize YAML → spawn ECS entities. //! Uses real content files from content/ directory for structural content, //! and a test fixture for isolated NPC profile spawning. //! //! Runtime validation (TCP boot + tick) is in content_runtime.rs. use bevy_app::prelude::*; use bevy_ecs::prelude::*; use std::collections::BTreeMap; use std::path::PathBuf; use settled_reach_server::content::loader::{load_content, ContentStore}; use settled_reach_server::content::spawn::spawn_content; use settled_reach_server::content::types::*; use settled_reach_server::content::{ContentConfig, ContentPlugin, ContentStoreResource}; use settled_reach_server::knowledge::registry::EntityRegistry; use settled_reach_server::npc; use settled_reach_server::simulation::SimulationPlugin; /// Find the content root relative to the test binary location. fn content_root() -> PathBuf { let manifest_dir = env!("CARGO_MANIFEST_DIR"); PathBuf::from(manifest_dir).join("../content") } // ----------------------------------------------------------------------- // Test: real content discovery and structural loading // ----------------------------------------------------------------------- #[test] fn discover_real_content_structure() { let root = content_root(); if !root.join("content.yaml").exists() { // Skip if content directory is not present (e.g. CI without content) eprintln!("Skipping: content directory not found at {:?}", root); return; } let store = load_content(&root).expect("content loading should succeed"); // Manifest should be present assert!(store.manifest.is_some()); let manifest = store.manifest.as_ref().unwrap(); assert_eq!(manifest.version, "0.1.0"); assert!(!manifest.campaigns.is_empty()); // At least one district should be discovered assert!( !store.districts.is_empty(), "should discover at least one district" ); } #[test] fn load_real_transit_district() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let store = load_content(&root).expect("content loading should succeed"); // The transit district should be discovered let transit = store .districts .get("krenn.sova.transit") .expect("transit district should be discovered"); // District metadata assert!(transit.meta.is_some()); let meta = transit.meta.as_ref().unwrap(); assert_eq!(meta.display_name, "Sova Transit District"); assert_eq!(meta.npc_count, 17); // 5 triangles from ticket #391 assert_eq!(transit.triangles.len(), 5); let triangle_ids: Vec<&str> = transit .triangles .iter() .map(|t| t.canonical_id.as_str()) .collect(); assert!(triangle_ids.contains(&"hub-power")); assert!(triangle_ids.contains(&"worried-knowledge")); assert!(triangle_ids.contains(&"bar-tensions")); assert!(triangle_ids.contains(&"worried-partner")); assert!(triangle_ids.contains(&"informant-question")); // Each triangle should have exactly 3 members for triangle in &transit.triangles { assert_eq!( triangle.members.len(), 3, "Triangle {} should have 3 members", triangle.canonical_id ); } // 5 pools from ticket #389 assert_eq!(transit.pools.len(), 5); let pool_ids: Vec<&str> = transit.pools.iter().map(|p| p.pool_id.as_str()).collect(); assert!(pool_ids.contains(&"transit:friend_smuggler")); assert!(pool_ids.contains(&"transit:friend_detective")); assert!(pool_ids.contains(&"transit:bar_regulars")); assert!(pool_ids.contains(&"transit:compromised_inspector")); assert!(pool_ids.contains(&"transit:primary_contraband")); // 3 templates from ticket #390 assert_eq!(transit.templates.len(), 3); let template_ids: Vec<&str> = transit .templates .iter() .map(|t| t.template_id.as_str()) .collect(); assert!(template_ids.contains(&"logistics-hub")); assert!(template_ids.contains(&"bar")); assert!(template_ids.contains(&"smuggling-ring")); // 23 NPC profiles: 17 NPCs + 2 PC-as-NPC + 1 extended NPC (nils-davan) + 3 Sprint 14 additions // Populated by #398 (wiki→YAML NPC conversion) and Sprint 14 content expansion assert_eq!( transit.npc_profiles.len(), 23, "Expected 23 parseable NPC profiles" ); } #[test] fn verify_triangle_fork_structure() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let store = load_content(&root).expect("content loading should succeed"); let transit = store.districts.get("krenn.sova.transit").unwrap(); // Hub power triangle: should have 1 fork with 3 outcomes let hub_power = transit .triangles .iter() .find(|t| t.canonical_id == "hub-power") .expect("hub-power triangle should exist"); assert_eq!(hub_power.forks.len(), 1); assert_eq!(hub_power.forks[0].id, "volume-escalation"); assert_eq!(hub_power.forks[0].outcomes.len(), 3); let outcome_ids: Vec<&str> = hub_power.forks[0] .outcomes .iter() .filter_map(|o| o.id.as_deref()) .collect(); assert!(outcome_ids.contains(&"escalate")); assert!(outcome_ids.contains(&"stabilize")); assert!(outcome_ids.contains(&"mediate")); // Resolution states assert_eq!(hub_power.resolution_states.len(), 3); } #[test] fn verify_pool_candidates() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let store = load_content(&root).expect("content loading should succeed"); let transit = store.districts.get("krenn.sova.transit").unwrap(); // bar_regulars pool should have 5 candidates let bar_regulars = transit .pools .iter() .find(|p| p.pool_id == "transit:bar_regulars") .expect("bar_regulars pool should exist"); assert_eq!(bar_regulars.candidates.len(), 5); assert_eq!(bar_regulars.category, "npc_group"); } #[test] fn verify_template_role_slots() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let store = load_content(&root).expect("content loading should succeed"); let transit = store.districts.get("krenn.sova.transit").unwrap(); // Logistics hub should have 5 role slots let hub = transit .templates .iter() .find(|t| t.template_id == "logistics-hub") .expect("logistics-hub template should exist"); assert_eq!(hub.role_slots.len(), 5); // Should have v01_assignments assert!(hub.v01_assignments.is_some()); let assignments = hub.v01_assignments.as_ref().unwrap(); assert!(assignments.contains_key("shift-supervisor")); } // ----------------------------------------------------------------------- // Test: NPC spawning pipeline with test fixture data // ----------------------------------------------------------------------- #[test] fn spawn_npc_from_content_store() { let mut world = World::new(); world.init_resource::(); world.init_resource::(); // Create a minimal content store with one test NPC let mut store = ContentStore::default(); let mut district = settled_reach_server::content::loader::DistrictContent::default(); district.npc_profiles.push(NpcProfile { canonical_id: "test-worker".to_string(), display_name: "Test Worker".to_string(), tier: 2, pattern: Some("ANCHOR".to_string()), motivation: Some("CIVILIAN".to_string()), description: Some("A test dock worker".to_string()), want: Some(NpcWant { primary: "Safety".to_string(), intensity: Some(5), description: Some("Wants a quiet life".to_string()), }), secret: None, relationships: vec![], tolerance: Some(NpcTolerance { threshold: Some(70), description: None, }), routine: None, information: None, contentment: Some(NpcContentment { level: Some(30), description: None, }), personality: None, tells: vec![], skills: Some(NpcSkills { combat_trained: Some(false), skills: Some({ let mut m = BTreeMap::new(); m.insert("technical".to_string(), 5); m }), }), triangle_membership: vec![], trust_levels: None, friend_arc: None, dual_lens: None, }); store .districts .insert("test.district".to_string(), district); let result = spawn_content(&mut world, &store); // Verify entity was spawned assert_eq!(result.npcs_spawned, 1); assert!(result.npc_ids.contains_key("test-worker")); // Verify ECS components let stable_id = result.npc_ids["test-worker"]; let entity = world .resource::() .to_entity(&stable_id) .unwrap(); assert!(world.get::(entity).is_some()); let want = world.get::(entity).unwrap(); assert_eq!(want.primary, npc::WantKind::Safety); assert_eq!(want.intensity, 5); let tolerance = world.get::(entity).unwrap(); assert_eq!(tolerance.threshold, 70); let skills = world.get::(entity).unwrap(); assert_eq!(skills.skills[&npc::Skill::Technical], 5); } // ----------------------------------------------------------------------- // Test: ContentPlugin integration with bevy App // ----------------------------------------------------------------------- #[test] fn content_plugin_loads_via_app() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let mut app = App::new(); app.add_plugins(SimulationPlugin); app.add_plugins(settled_reach_server::npc::NpcPlugin); app.insert_resource(ContentConfig { content_root: root, ..Default::default() }); app.add_plugins(ContentPlugin); // Run startup systems app.update(); // ContentStoreResource should be inserted assert!( app.world().contains_resource::(), "ContentStoreResource should be present after startup" ); let store = &app.world().resource::().0; assert!(!store.districts.is_empty()); } // ----------------------------------------------------------------------- // Test: Full spawn pipeline with real content — 10-axis gap closure // ----------------------------------------------------------------------- #[test] fn spawn_real_content_with_relationships_and_secrets() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let mut world = World::new(); world.init_resource::(); world.init_resource::(); world.init_resource::(); let store = load_content(&root).expect("content loading should succeed"); let result = spawn_content(&mut world, &store); // All 23 profiles should spawn assert_eq!(result.npcs_spawned, 23); assert!(result.npc_ids.contains_key("npc:kael-davan")); assert!(result.npc_ids.contains_key("npc:voss")); assert!(result.npc_ids.contains_key("npc:pc-smuggler")); assert!(result.npc_ids.contains_key("npc:nils-davan")); // Verify ALL 20 NPCs have Want components (Option C: exact enum keywords in YAML) let registry = world.resource::(); let mut npcs_with_want = 0; for (canonical_id, stable_id) in &result.npc_ids { let entity = registry .to_entity(stable_id) .unwrap_or_else(|| panic!("{} should have an entity", canonical_id)); assert!( world.get::(entity).is_some(), "NPC {} should have a Want component", canonical_id ); npcs_with_want += 1; } assert_eq!( npcs_with_want, 23, "All 23 NPCs should have Want components" ); // Spot-check specific Want values let kael_entity = registry .to_entity(&result.npc_ids["npc:kael-davan"]) .unwrap(); let kael_want = world .get::(kael_entity) .expect("Kael should have Want"); assert_eq!(kael_want.primary, npc::WantKind::Safety); // Verify Kael has a Secret component let kael_secret = world .get::(kael_entity) .expect("Kael should have Secret"); assert!(kael_secret.description.contains("ring")); assert_eq!(kael_secret.severity, npc::SecretSeverity::Major); // Verify Kael has Relationships (7 defined in YAML) let kael_rels = world .get::(kael_entity) .expect("Kael should have Relationships"); assert!( kael_rels.entries.len() >= 5, "Kael should have at least 5 resolved relationships, got {}", kael_rels.entries.len() ); // Verify Kael has KnowledgeGraph (background facts from information.knows) let kael_kg = world .get::(kael_entity) .expect("Kael should have KnowledgeGraph"); assert!( kael_kg.knows_fact(&settled_reach_server::knowledge::types::FactId( "contraband.ring_exists".to_string() )) ); // Verify global RelationshipGraph was populated let graph = world.resource::(); assert!( graph.edge_count() >= 20, "Expected at least 20 relationship edges, got {}", graph.edge_count() ); // Verify Nils (off-stage) also has correct data let nils_entity = world .resource::() .to_entity(&result.npc_ids["npc:nils-davan"]) .unwrap(); let nils_want = world .get::(nils_entity) .expect("Nils should have Want"); assert_eq!(nils_want.primary, npc::WantKind::Power); } // ----------------------------------------------------------------------- // Test: EntanglementTag assignment from authored content (#176, D-029) // ----------------------------------------------------------------------- #[test] fn entanglement_tags_assigned_from_triangle_membership() { let root = content_root(); if !root.join("content.yaml").exists() { return; } let mut world = World::new(); world.init_resource::(); world.init_resource::(); world.init_resource::(); let store = load_content(&root).expect("content loading should succeed"); let result = spawn_content(&mut world, &store); // Acceptance: 17+ NPCs spawn (we have 23 authored profiles) assert!( result.npcs_spawned >= 17, "Expected 17+ NPCs, got {}", result.npcs_spawned ); let registry = world.resource::(); // Every NPC must have an EntanglementTag let mut intrigue_count = 0u32; let mut flat_count = 0u32; for (canonical_id, stable_id) in &result.npc_ids { let entity = registry .to_entity(stable_id) .unwrap_or_else(|| panic!("{} should have an entity", canonical_id)); let tag = world .get::(entity) .unwrap_or_else(|| panic!("{} must have EntanglementTag", canonical_id)); match tag { npc::EntanglementTag::Intrigue => intrigue_count += 1, npc::EntanglementTag::Flat => flat_count += 1, npc::EntanglementTag::Mundane => {} // reserved for procedural NPCs } } // Acceptance: at least one EntanglementTag::Intrigue entity assert!( intrigue_count >= 1, "At least one NPC must be EntanglementTag::Intrigue, got 0" ); // Stronger assertion: we know 13 authored NPCs have non-empty triangle_membership assert!( intrigue_count >= 10, "Expected 10+ Intrigue NPCs (authored triangle members), got {}", intrigue_count ); // Some NPCs should be Flat (no triangle membership) assert!( flat_count >= 1, "At least one NPC should be EntanglementTag::Flat, got 0" ); // Spot-check: Kael (triangle member) must be Intrigue let kael_entity = registry .to_entity(&result.npc_ids["npc:kael-davan"]) .unwrap(); assert_eq!( *world.get::(kael_entity).unwrap(), npc::EntanglementTag::Intrigue, "Kael (triangle member) must be Intrigue" ); // Spot-check: Devra (empty triangle_membership) must be Flat let devra_entity = registry .to_entity(&result.npc_ids["npc:devra"]) .unwrap(); assert_eq!( *world.get::(devra_entity).unwrap(), npc::EntanglementTag::Flat, "Devra (no triangle membership) must be Flat" ); } // ----------------------------------------------------------------------- // Test: Authored triangle instantiation (#188, D-087) // ----------------------------------------------------------------------- #[test] fn authored_triangles_instantiated_from_content() { use settled_reach_server::content::template::{TriangleClassification, TriangleState}; use settled_reach_server::simulation::tier::ActiveSim; let root = content_root(); if !root.join("content.yaml").exists() { return; } let mut world = World::new(); world.init_resource::(); world.init_resource::(); world.init_resource::(); let store = load_content(&root).expect("content loading should succeed"); let result = spawn_content(&mut world, &store); // Query all TriangleState entities (clone to release world borrow) let triangles: Vec = { let mut q = world.query::<&TriangleState>(); q.iter(&world).cloned().collect() }; // Acceptance: exactly 5 authored triangles assert_eq!( triangles.len(), 5, "Expected 5 authored triangles, got {}", triangles.len() ); // Count by classification (D-087) let active_count = triangles .iter() .filter(|t| t.classification == TriangleClassification::ActiveFork) .count(); let passive_count = triangles .iter() .filter(|t| t.classification == TriangleClassification::PassiveTension) .count(); assert_eq!( active_count, 3, "Expected 3 ActiveFork triangles, got {}", active_count ); assert_eq!( passive_count, 2, "Expected 2 PassiveTension triangles, got {}", passive_count ); // All 5 must have exactly 3 role assignments (triangle = 3 NPCs) for triangle in &triangles { assert_eq!( triangle.role_assignments.len(), 3, "Triangle {:?} should have 3 role assignments, got {}", triangle.triangle_id, triangle.role_assignments.len() ); } // All role assignments must point to valid NPC entities in the registry let registry = world.resource::(); for triangle in &triangles { for (role, stable_id) in &triangle.role_assignments { assert!( registry.to_entity(stable_id).is_some(), "Triangle {:?} role '{}' points to StableId {:?} with no entity", triangle.triangle_id, role.0, stable_id, ); } } // All triangle entities must have ActiveSim marker let mut active_query = world.query::<(&TriangleState, &ActiveSim)>(); let active_triangles: Vec<_> = active_query.iter(&world).collect(); assert_eq!( active_triangles.len(), 5, "All 5 triangles must have ActiveSim marker" ); // Verify StableIds in role assignments correspond to spawned NPC canonical_ids let all_npc_stable_ids: std::collections::BTreeSet<_> = result.npc_ids.values().copied().collect(); for triangle in &triangles { for (role, stable_id) in &triangle.role_assignments { assert!( all_npc_stable_ids.contains(stable_id), "Triangle {:?} role '{}' StableId {:?} not in spawned NPC set", triangle.triangle_id, role.0, stable_id, ); } } } // Runtime validation test (boot + tick 10 + snapshot) moved to // server/tests/content_runtime.rs per architectural review.