Added complete support for the new type system syntax including: - concept: Base type declarations - sub_concept: Enum and record sub-type definitions - concept_comparison: Compile-time pattern matching with conditional guards Parser changes: - Added VariantPattern, FieldCondition, and Condition AST nodes - Implemented "is" keyword for pattern matching (e.g., "CupType is Glass or CupType is Plastic") - Added Value::Any variant to support universal type matching - Disambiguated enum-like vs record-like sub_concept syntax LSP updates: - Added Value::Any match arms across code_actions, completion, hover, inlay_hints, and semantic_tokens - Type inference and formatting support for Any values Example fixes: - Fixed syntax error in baker-family behaviors (missing closing brace in nested if) - Removed deprecated core_enums.sb file
1029 lines
22 KiB
Rust
1029 lines
22 KiB
Rust
//! 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"));
|
|
}
|