//! Integration tests for the CLI tool //! //! These tests verify that the `sb` command-line tool works correctly //! by testing it against real project files. use std::{ fs, path::PathBuf, process::Command, }; use tempfile::TempDir; /// Helper to get the path to the compiled sb binary fn sb_binary() -> PathBuf { let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); path.push("target"); path.push("debug"); path.push("sb"); path } /// Helper to create a temporary test project fn create_test_project() -> TempDir { let dir = TempDir::new().unwrap(); // Create a valid character file fs::write( dir.path().join("test.sb"), r#" character Martha { age: 34 trust: 0.8 } character David { age: 42 health: 0.9 } "#, ) .unwrap(); dir } /// Helper to create a project with errors fn create_invalid_project() -> TempDir { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("errors.sb"), r#" character Martha { age: 200 trust: 1.5 } "#, ) .unwrap(); dir } #[test] fn test_validate_valid_project() { let project = create_test_project(); let output = Command::new(sb_binary()) .arg("validate") .arg(project.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Validation should succeed for valid project. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 2")); } #[test] fn test_validate_invalid_project() { let project = create_invalid_project(); let output = Command::new(sb_binary()) .arg("validate") .arg(project.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Validation should fail for invalid project" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Found 2 errors")); assert!(stderr.contains("Trait 'age' has value 200")); assert!(stderr.contains("Trait 'trust' has value 1.5")); } #[test] fn test_validate_single_file() { let project = create_test_project(); let file_path = project.path().join("test.sb"); let output = Command::new(sb_binary()) .arg("validate") .arg(&file_path) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Validation should succeed for valid file" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); } #[test] fn test_validate_nonexistent_path() { let output = Command::new(sb_binary()) .arg("validate") .arg("/nonexistent/path/to/project") .output() .expect("Failed to execute sb validate"); assert!(!output.status.success(), "Should fail for nonexistent path"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Path does not exist")); } #[test] fn test_inspect_character() { let project = create_test_project(); let output = Command::new(sb_binary()) .arg("inspect") .arg("Martha") .arg("--path") .arg(project.path()) .output() .expect("Failed to execute sb inspect"); assert!(output.status.success(), "Inspect should succeed"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Character: Martha")); assert!(stdout.contains("age")); assert!(stdout.contains("34")); } #[test] fn test_inspect_nonexistent_entity() { let project = create_test_project(); let output = Command::new(sb_binary()) .arg("inspect") .arg("NonExistent") .arg("--path") .arg(project.path()) .output() .expect("Failed to execute sb inspect"); assert!(output.status.success(), "Inspect runs even if not found"); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("not found")); } #[test] fn test_validate_empty_project() { let dir = TempDir::new().unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!(!output.status.success(), "Should fail for empty project"); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("No .sb files found")); } #[test] fn test_validate_shows_multiple_errors() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("multi_error.sb"), r#" character Alice { age: 200 trust: 1.5 bond: -0.2 } character Bob { age: -10 love: 3.0 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Should fail with validation errors" ); let stderr = String::from_utf8_lossy(&output.stderr); // Should show all 5 errors (non-fail-fast) assert!(stderr.contains("Found 5 errors")); assert!(stderr.contains("age")); // age: 200 assert!(stderr.contains("trust")); // trust: 1.5 assert!(stderr.contains("bond")); // bond: -0.2 assert!(stderr.contains("-10")); // age: -10 assert!(stderr.contains("love")); // love: 3.0 } #[test] fn test_cross_file_name_resolution() { let dir = TempDir::new().unwrap(); // Create multiple files with characters fs::write( dir.path().join("file1.sb"), r#" character Martha { age: 34 trust: 0.8 } "#, ) .unwrap(); fs::write( dir.path().join("file2.sb"), r#" character David { age: 42 health: 0.9 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Should successfully load and validate multiple files" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 2")); } #[test] fn test_cross_file_duplicate_detection() { let dir = TempDir::new().unwrap(); // Create two files with the same character name fs::write( dir.path().join("file1.sb"), r#" character Martha { age: 34 } "#, ) .unwrap(); fs::write( dir.path().join("file2.sb"), r#" character Martha { age: 42 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Should fail with duplicate definition error" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("Duplicate definition of 'Martha'")); } // ===== Template Composition Tests ===== #[test] fn test_template_composition_with_includes() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("templates.sb"), r#" // Base template template Being { alive: true } // Template that includes Being template Human { include Being kind: "human" } // Character with template composition character Martha from Human { firstName: "Martha" age: 34 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Template composition with includes should succeed" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful!")); assert!(stdout.contains("Characters: 1")); } #[test] fn test_template_composition_multiple_templates() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("templates.sb"), r#" template Physical { height: 0 weight: 0 } template Mental { iq: 0 } character David from Physical, Mental { height: 180 weight: 75 iq: 120 firstName: "David" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Multiple template inheritance should succeed" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful!")); } #[test] fn test_strict_template_validation_success() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("strict.sb"), r#" template Person strict { age: 18..100 firstName: "" } character Martha from Person { age: 34 firstName: "Martha" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Strict template with concrete values should succeed" ); } #[test] fn test_strict_template_validation_failure() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("strict_fail.sb"), r#" template Person strict { age: 18..100 } character Martha from Person { firstName: "Martha" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Strict template with range value should fail" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("strict template")); assert!(stderr.contains("range value")); } #[test] fn test_template_chained_includes() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("chained.sb"), r#" template Being { alive: true } template Human { include Being kind: "human" } template Person strict { include Human age: 18..100 } character Martha from Person { age: 34 firstName: "Martha" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Chained template includes should succeed" ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful!")); } #[test] fn test_reserved_keyword_field_name_fails_at_parse() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("reserved.sb"), r#" character Martha { species: "human" age: 34 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( !output.status.success(), "Field with reserved keyword name should fail at parse time" ); let stderr = String::from_utf8_lossy(&output.stderr); // Parser catches this as UnrecognizedToken before validation assert!(stderr.contains("Parse error") || stderr.contains("UnrecognizedToken")); } // ===== Prose Block Tests ===== #[test] fn test_prose_block_in_institution() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("institution.sb"), r#" institution Bakery { type: "business" ---description A cozy neighborhood bakery that has been serving the community for over 30 years. Known for its fresh bread and friendly atmosphere. --- established: 1990 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Institution with prose block should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Institutions: 1")); } #[test] fn test_multiple_prose_blocks_in_same_declaration() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("multi_prose.sb"), r#" character Martha { age: 34 ---backstory Martha grew up in a small town where she learned the art of baking from her grandmother. Her passion for creating delicious pastries has been lifelong. --- trust: 0.8 ---personality Martha is kind-hearted and patient, always willing to help her neighbors. She has a warm smile that makes everyone feel welcome in her bakery. --- ---goals She dreams of expanding her bakery and teaching baking classes to the next generation of bakers in the community. --- } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Character with multiple prose blocks should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 1")); } #[test] fn test_prose_blocks_in_behavior_tree() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("behavior.sb"), r#" behavior WakeUpRoutine { ---description A simple morning routine that characters follow when they wake up each day. --- then { WakeUp GetDressed EatBreakfast } } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Behavior tree with prose block should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Behaviors: 1")); } #[test] fn test_prose_blocks_in_life_arc() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("life_arc.sb"), r#" life_arc BakerCareer { ---description The journey of becoming a master baker, from apprentice to owning one's own bakery. --- state Apprentice { ---details Learning the basics of bread-making and pastry under the guidance of an experienced baker. --- on experience > 5 -> Journeyman } state Journeyman { ---details Working independently and developing signature recipes while saving to open a bakery. --- on savings > 50000 -> MasterBaker } state MasterBaker { ---details Running a successful bakery and mentoring the next generation of bakers. --- } } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Life arc with prose blocks should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Life Arcs: 1")); } #[test] fn test_prose_blocks_in_schedule() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("schedule.sb"), r#" schedule DailyRoutine { ---description A typical weekday schedule for bakery workers who start their day before dawn. --- 04:00 -> 06:00: PrepareDough { } 06:00 -> 08:00: BakeBread { } 08:00 -> 12:00: ServCustomers { } 12:00 -> 13:00: LunchBreak { } 13:00 -> 17:00: ServCustomers { } 17:00 -> 18:00: Cleanup { } } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Schedule with prose block should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Schedules: 1")); } #[test] fn test_prose_blocks_with_special_characters() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("special.sb"), r#" character Alice { age: 7 ---backstory Alice was a curious young girl who often wondered about the world around her. One day, she saw a White Rabbit muttering "Oh dear! Oh dear! I shall be too late!" and followed it down a rabbit-hole. She fell down, down, down--would the fall never come to an end? "I wonder how many miles I've fallen by this time?" she said aloud. --- curiosity: 0.95 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Prose block with quotes, dashes, and punctuation should validate. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); } #[test] fn test_prose_blocks_in_relationship() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("relationship.sb"), r#" character Martha { age: 34 } character David { age: 42 } relationship Spousal { Martha {} David {} ---bond_description Martha and David have been married for 8 years. They share a deep connection built on mutual respect, trust, and shared dreams for the future. --- bond: 0.9 coordination: cohabiting } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Relationship with prose block should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 2")); assert!(stdout.contains("Relationships: 1")); } // ===== Species Tests ===== #[test] fn test_species_with_identity_operator() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("species.sb"), r#" species Human { lifespan: 70..90 intelligence: "high" } character Alice: Human { age: 7 } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Character with species should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 1")); } #[test] fn test_species_composition_with_includes() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("hybrid_species.sb"), r#" species Horse { hoofed: true mane_style: "long" } species Donkey { hoofed: true ear_size: "large" } species Mule { include Horse include Donkey sterile: true } character MyMule: Mule { age: 3 name: "Betsy" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Hybrid species with composition should validate successfully. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 1")); } #[test] fn test_character_without_species() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("no_species.sb"), r#" character TheNarrator { abstract_entity: true } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Character without species should validate (species is optional). Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); } #[test] fn test_species_with_templates() { let dir = TempDir::new().unwrap(); fs::write( dir.path().join("species_and_templates.sb"), r#" species Human { lifespan: 70..90 } template Hero { courage: 0.9 strength: 0.8 } character Alice: Human from Hero { age: 7 name: "Alice" } "#, ) .unwrap(); let output = Command::new(sb_binary()) .arg("validate") .arg(dir.path()) .output() .expect("Failed to execute sb validate"); assert!( output.status.success(), "Character with both species and templates should validate. Stderr: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("✓ Validation successful")); assert!(stdout.contains("Characters: 1")); }