Files
storybook/tests/cli_integration.rs

1029 lines
22 KiB
Rust
Raw Permalink Normal View History

//! 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"));
}