Files
storybook/src/error_showcase_tests.rs
Sienna Meridian Satterwhite a8882eb3ec feat(parser): add action parameters, repeater decorator, and named participants
Add syntax enhancements for more expressive behavior trees and relationships.

Action parameters:
- Support typed/positional parameters: WaitDuration(2.0)
- Support named parameters: SetValue(field: value)
- Enable inline values without field names

Repeater decorator:
- Add * { node } syntax for repeating behavior nodes
- Maps to Decorator("repeat", node)

Named participant blocks:
- Replace self/other blocks with named participant syntax
- Support multi-party relationships
- Example: Alice { role: seeker } instead of self { role: seeker }

Schedule block syntax:
- Require braces for schedule blocks to support narrative fields
- Update tests to use new syntax: 04:00 -> 06:00: Activity { }
2026-02-08 15:45:56 +00:00

514 lines
15 KiB
Rust

//! Functional tests that showcase every error type with its helpful message
//!
//! These tests are designed to:
//! 1. Ensure every error type can be triggered
//! 2. Document what causes each error
//! 3. Verify that error messages are helpful and clear
use std::collections::HashSet;
use crate::{
resolve::{
convert::convert_file,
names::NameTable,
validate::{
validate_behavior_tree_actions,
validate_life_arc_transitions,
validate_relationship_bonds,
validate_schedule_overlaps,
validate_trait_ranges,
},
ErrorCollector,
ResolveError,
},
syntax::{
ast::*,
lexer::Lexer,
FileParser,
},
Project,
};
// ===== Parse Errors =====
#[test]
fn test_unexpected_token_error() {
let source = r#"
character Martha {
age 34
}
"#;
// Missing colon after 'age' - should trigger UnexpectedToken
let lexer = Lexer::new(source);
let result = FileParser::new().parse(lexer);
assert!(result.is_err(), "Should fail with unexpected token");
println!("\n=== UnexpectedToken Error ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
#[test]
fn test_unexpected_eof_error() {
let source = r#"
character Martha {
age: 34
"#;
// Missing closing brace - should trigger UnexpectedEof
let lexer = Lexer::new(source);
let result = FileParser::new().parse(lexer);
assert!(result.is_err(), "Should fail with unexpected EOF");
println!("\n=== UnexpectedEof Error ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
#[test]
fn test_invalid_token_error() {
let source = "character Martha { age: @#$ }";
// Invalid character sequence - should trigger InvalidToken
let lexer = Lexer::new(source);
let result = FileParser::new().parse(lexer);
assert!(result.is_err(), "Should fail with invalid token");
println!("\n=== InvalidToken Error ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
#[test]
fn test_unclosed_prose_block_error() {
let source = r#"
character Martha {
backstory: ---backstory
This is Martha's backstory.
It goes on and on...
But it never closes!
}
"#;
// Prose block never closed - should trigger UnclosedProseBlock
let lexer = Lexer::new(source);
let result = FileParser::new().parse(lexer);
assert!(result.is_err(), "Should fail with unclosed prose block");
println!("\n=== UnclosedProseBlock Error ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
// ===== Resolution Errors =====
#[test]
fn test_name_not_found_error() {
let file = File {
declarations: vec![],
};
let table = NameTable::from_file(&file).unwrap();
let result = table.lookup(&["NonExistent".to_string()]);
assert!(result.is_none(), "Should not find non-existent name");
// Create the actual error
let error = ResolveError::NameNotFound {
name: "NonExistent".to_string(),
suggestion: table.find_suggestion("NonExistent"),
};
println!("\n=== NameNotFound Error ===");
println!("{:?}", error);
}
#[test]
fn test_duplicate_definition_error() {
let file = File {
declarations: vec![
Declaration::Character(Character {
name: "Martha".to_string(),
species: None,
fields: vec![],
template: None,
span: Span::new(0, 10),
}),
Declaration::Character(Character {
name: "Martha".to_string(),
species: None,
fields: vec![],
template: None,
span: Span::new(20, 30),
}),
],
};
let result = NameTable::from_file(&file);
assert!(result.is_err(), "Should fail with duplicate definition");
println!("\n=== DuplicateDefinition Error ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
#[test]
fn test_circular_dependency_error() {
// Manually create a circular dependency error for demonstration
let error = ResolveError::CircularDependency {
cycle: "Template A -> Template B -> Template A".to_string(),
};
println!("\n=== CircularDependency Error ===");
println!("{:?}", error);
}
#[test]
fn test_invalid_field_access_error() {
let error = ResolveError::InvalidFieldAccess {
message: "Field 'nonexistent' does not exist on character 'Martha'".to_string(),
};
println!("\n=== InvalidFieldAccess Error ===");
println!("{:?}", error);
}
#[test]
fn test_type_mismatch_error() {
let error = ResolveError::TypeMismatch {
message: "Expected number for field 'age', but got string \"thirty\"".to_string(),
};
println!("\n=== TypeMismatch Error ===");
println!("{:?}", error);
}
#[test]
fn test_validation_error_generic() {
let error = ResolveError::ValidationError {
message: "Cannot append field 'age': field already exists".to_string(),
help: Some("The 'append' operation is used to add new fields. Use 'set' to update existing fields.".to_string()),
};
println!("\n=== ValidationError Error ===");
println!("{:?}", error);
}
// ===== Validation Errors =====
#[test]
fn test_unknown_life_arc_state_error() {
let life_arc = LifeArc {
name: "Growth".to_string(),
states: vec![
ArcState {
name: "child".to_string(),
on_enter: None,
transitions: vec![Transition {
to: "adult".to_string(), // 'adult' exists
condition: Expr::BoolLit(true),
span: Span::new(0, 10),
}],
span: Span::new(0, 50),
},
ArcState {
name: "adult".to_string(),
on_enter: None,
transitions: vec![Transition {
to: "senior".to_string(), // 'senior' doesn't exist!
condition: Expr::BoolLit(true),
span: Span::new(50, 60),
}],
span: Span::new(50, 100),
},
],
span: Span::new(0, 100),
};
let mut collector = ErrorCollector::new();
validate_life_arc_transitions(&life_arc, &mut collector);
assert!(collector.has_errors(), "Should fail with unknown state");
println!("\n=== UnknownLifeArcState Error ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_trait_out_of_range_error_bond() {
let fields = vec![Field {
name: "bond".to_string(),
value: Value::Float(1.5), // Out of range!
span: Span::new(0, 10),
}];
let mut collector = ErrorCollector::new();
validate_trait_ranges(&fields, &mut collector);
assert!(
collector.has_errors(),
"Should fail with out of range trait"
);
println!("\n=== TraitOutOfRange Error (bond too high) ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_trait_out_of_range_error_age() {
let fields = vec![Field {
name: "age".to_string(),
value: Value::Int(200), // Out of range!
span: Span::new(0, 10),
}];
let mut collector = ErrorCollector::new();
validate_trait_ranges(&fields, &mut collector);
assert!(collector.has_errors(), "Should fail with out of range age");
println!("\n=== TraitOutOfRange Error (age too high) ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_trait_out_of_range_negative() {
let fields = vec![Field {
name: "trust".to_string(),
value: Value::Float(-0.2), // Negative!
span: Span::new(0, 10),
}];
let mut collector = ErrorCollector::new();
validate_trait_ranges(&fields, &mut collector);
assert!(collector.has_errors(), "Should fail with negative trait");
println!("\n=== TraitOutOfRange Error (negative value) ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_schedule_overlap_error() {
let schedule = Schedule {
name: "DailyRoutine".to_string(),
blocks: vec![
ScheduleBlock {
activity: "work".to_string(),
start: Time {
hour: 8,
minute: 0,
second: 0,
},
end: Time {
hour: 12,
minute: 30,
second: 0,
},
fields: vec![],
span: Span::new(0, 50),
},
ScheduleBlock {
activity: "lunch".to_string(),
start: Time {
hour: 12,
minute: 0, // Overlaps with work!
second: 0,
},
end: Time {
hour: 13,
minute: 0,
second: 0,
},
fields: vec![],
span: Span::new(50, 100),
},
],
span: Span::new(0, 100),
};
let mut collector = ErrorCollector::new();
validate_schedule_overlaps(&schedule, &mut collector);
assert!(collector.has_errors(), "Should fail with schedule overlap");
println!("\n=== ScheduleOverlap Error ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_unknown_behavior_action_error() {
let tree = Behavior {
name: "WorkDay".to_string(),
root: BehaviorNode::Action("unknown_action".to_string(), vec![]),
span: Span::new(0, 50),
};
// Create a registry with some known actions (but not "unknown_action")
let mut action_registry = HashSet::new();
action_registry.insert("walk".to_string());
action_registry.insert("work".to_string());
action_registry.insert("eat".to_string());
let mut collector = ErrorCollector::new();
validate_behavior_tree_actions(&tree, &action_registry, &mut collector);
assert!(collector.has_errors(), "Should fail with unknown action");
println!("\n=== UnknownBehaviorAction Error ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_relationship_bond_out_of_range() {
let relationship = Relationship {
name: "Test".to_string(),
participants: vec![],
fields: vec![Field {
name: "bond".to_string(),
value: Value::Float(2.5), // Way out of range!
span: Span::new(0, 10),
}],
span: Span::new(0, 50),
};
let mut collector = ErrorCollector::new();
validate_relationship_bonds(&[relationship], &mut collector);
assert!(collector.has_errors(), "Should fail with bond out of range");
println!("\n=== Relationship Bond Out of Range ===");
if collector.has_errors() {
let result = collector.into_result(());
if let Err(e) = result {
println!("{:?}", e);
}
}
}
#[test]
fn test_duplicate_field_in_convert() {
let character = Character {
name: "Martha".to_string(),
species: None,
fields: vec![
Field {
name: "age".to_string(),
value: Value::Int(34),
span: Span::new(0, 10),
},
Field {
name: "age".to_string(), // Duplicate!
value: Value::Int(35),
span: Span::new(10, 20),
},
],
template: None,
span: Span::new(0, 50),
};
let file = File {
declarations: vec![Declaration::Character(character)],
};
let result = convert_file(&file);
assert!(result.is_err(), "Should fail with duplicate field");
println!("\n=== Duplicate Field Error (in conversion) ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
// ===== Project Errors =====
#[test]
fn test_invalid_project_structure_no_directory() {
let result = Project::load("/nonexistent/path/to/project");
assert!(result.is_err(), "Should fail with invalid structure");
println!("\n=== InvalidStructure Error (directory doesn't exist) ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
#[test]
fn test_invalid_project_structure_not_directory() {
// Try to load a file as if it were a directory
let result = Project::load("Cargo.toml");
assert!(result.is_err(), "Should fail - file not directory");
println!("\n=== InvalidStructure Error (not a directory) ===");
if let Err(e) = result {
println!("{:?}", e);
}
}
// ===== Showcase All Errors =====
#[test]
#[ignore] // Run with: cargo test error_showcase -- --ignored --nocapture
fn error_showcase_all() {
println!("\n\n");
println!("╔════════════════════════════════════════════════════════════════╗");
println!("║ STORYBOOK ERROR MESSAGES SHOWCASE ║");
println!("║ Every error type with helpful hints for users ║");
println!("╚════════════════════════════════════════════════════════════════╝");
test_unexpected_token_error();
test_unexpected_eof_error();
test_invalid_token_error();
test_unclosed_prose_block_error();
test_name_not_found_error();
test_duplicate_definition_error();
test_circular_dependency_error();
test_invalid_field_access_error();
test_type_mismatch_error();
test_validation_error_generic();
test_unknown_life_arc_state_error();
test_trait_out_of_range_error_bond();
test_trait_out_of_range_error_age();
test_trait_out_of_range_negative();
test_schedule_overlap_error();
test_unknown_behavior_action_error();
test_relationship_bond_out_of_range();
test_duplicate_field_in_convert();
test_invalid_project_structure_no_directory();
test_invalid_project_structure_not_directory();
println!("\n\n");
println!("╔════════════════════════════════════════════════════════════════╗");
println!("║ SHOWCASE COMPLETE ║");
println!("╚════════════════════════════════════════════════════════════════╝");
}