2026-02-13 21:52:03 +00:00
|
|
|
//! Example Validation Tests
|
|
|
|
|
//!
|
|
|
|
|
//! These tests validate that example storybooks load correctly and demonstrate
|
|
|
|
|
//! documented features. Unlike unit/integration tests that validate specific
|
|
|
|
|
//! functionality, these tests ensure that:
|
|
|
|
|
//! 1. Examples parse and resolve successfully
|
|
|
|
|
//! 2. Examples demonstrate features as documented
|
|
|
|
|
//! 3. Examples serve as reliable educational resources
|
|
|
|
|
//!
|
|
|
|
|
//! Cross-references:
|
|
|
|
|
//! - examples/baker-family/README.md
|
|
|
|
|
//! - DEVELOPER-GUIDE.md
|
|
|
|
|
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
|
|
|
|
|
use storybook::Project;
|
|
|
|
|
|
|
|
|
|
/// Helper to load an example project from the examples/ directory
|
|
|
|
|
fn load_example(name: &str) -> Project {
|
|
|
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
|
path.push("examples");
|
|
|
|
|
path.push(name);
|
|
|
|
|
|
|
|
|
|
assert!(path.exists(), "Example '{}' not found at {:?}", name, path);
|
|
|
|
|
|
2026-02-14 14:45:17 +00:00
|
|
|
Project::load(&path).unwrap_or_else(|e| panic!("Failed to load example '{}': {:?}", name, e))
|
2026-02-13 21:52:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Baker Family Example Tests
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_baker_family_loads_successfully() {
|
|
|
|
|
let project = load_example("baker-family");
|
|
|
|
|
|
2026-02-14 15:49:17 +00:00
|
|
|
// Should have exactly 5 characters
|
2026-02-13 21:52:03 +00:00
|
|
|
let char_count = project.characters().count();
|
2026-02-14 15:49:17 +00:00
|
|
|
assert_eq!(char_count, 5, "Baker family should have 5 characters");
|
2026-02-13 21:52:03 +00:00
|
|
|
|
|
|
|
|
// Verify all characters exist
|
|
|
|
|
assert!(
|
|
|
|
|
project.find_character("Martha").is_some(),
|
|
|
|
|
"Martha should exist"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
project.find_character("Jane").is_some(),
|
|
|
|
|
"Jane should exist"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
project.find_character("Emma").is_some(),
|
|
|
|
|
"Emma should exist"
|
|
|
|
|
);
|
2026-02-14 15:49:17 +00:00
|
|
|
assert!(
|
|
|
|
|
project.find_character("Henry").is_some(),
|
|
|
|
|
"Henry should exist"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
project.find_character("Roland").is_some(),
|
|
|
|
|
"Roland should exist"
|
|
|
|
|
);
|
2026-02-13 21:52:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_baker_family_martha_has_all_template_fields() {
|
|
|
|
|
let project = load_example("baker-family");
|
|
|
|
|
let martha = project
|
|
|
|
|
.find_character("Martha")
|
|
|
|
|
.expect("Martha should exist");
|
|
|
|
|
|
|
|
|
|
// Fields from Person template (base)
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("age"),
|
|
|
|
|
"Should have age from Person"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("energy"),
|
|
|
|
|
"Should have energy from Person"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("mood"),
|
|
|
|
|
"Should have mood from Person"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Fields from Worker template (extends Person)
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("occupation"),
|
|
|
|
|
"Should have occupation from Worker"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("work_ethic"),
|
|
|
|
|
"Should have work_ethic from Worker"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Fields from Baker template (extends Worker)
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("specialty"),
|
|
|
|
|
"Should have specialty from Baker"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("baking_skill"),
|
|
|
|
|
"Should have baking_skill from Baker"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
martha.fields.contains_key("customer_relations"),
|
|
|
|
|
"Should have customer_relations from Baker"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_baker_family_field_values() {
|
|
|
|
|
let project = load_example("baker-family");
|
|
|
|
|
let martha = project.find_character("Martha").unwrap();
|
|
|
|
|
|
|
|
|
|
// Martha sets age: 34 (overriding Person template range 0..100)
|
2026-02-14 14:03:21 +00:00
|
|
|
if let Some(storybook::syntax::ast::Value::Number(age)) = martha.fields.get("age") {
|
2026-02-13 21:52:03 +00:00
|
|
|
assert_eq!(*age, 34, "Martha's age should be 34");
|
|
|
|
|
} else {
|
2026-02-14 14:03:21 +00:00
|
|
|
panic!("age should be a Number");
|
2026-02-13 21:52:03 +00:00
|
|
|
}
|
|
|
|
|
|
feat(examples): rewrite baker-family as coherent Competition Week narrative
Rewrote the baker-family example around a unified story: the annual Harvest
Baking Competition is Saturday. Martha's sourdough starter Old Maggie is
sluggish from a cold snap, Jane is secretly entering the FreeStyle category,
Emma's science fair project is "The Chemistry of Fermentation", Henry is
judging for the first time and worried about impartiality, and Roland is
defending last year's title.
The week's schedules (Mon guild meeting → Tue test bakes → Wed sourcing →
Thu dough prep → Fri science fair → Sat competition → Sun recovery) are
now fully populated with narrative-specific actions.
Files split for composability:
- behaviors/baker_behaviors.sb → 5 focused files by character/domain
- schema/types.sb → 4 files (core, baking, world, social)
- schedules/work_schedules.sb → 4 files (one per schedule)
- relationships/baker_family_relationships.sb → family + bakery
Strong typing: replaced all enumerable string fields with concepts
(BakerSpecialty, Occupation, LocationType, InstitutionType, ParentingStyle,
Intensity, BakeryDomain, BakingDiscipline, ManagementDomain,
CompetitiveAdvantage).
Remove new-syntax-demo.sb (superseded by baker-family example).
2026-02-23 21:51:01 +00:00
|
|
|
// Martha sets specialty: Sourdough (overriding Baker template default Bread)
|
|
|
|
|
if let Some(storybook::syntax::ast::Value::Identifier(parts)) = martha.fields.get("specialty") {
|
2026-02-13 21:52:03 +00:00
|
|
|
assert_eq!(
|
feat(examples): rewrite baker-family as coherent Competition Week narrative
Rewrote the baker-family example around a unified story: the annual Harvest
Baking Competition is Saturday. Martha's sourdough starter Old Maggie is
sluggish from a cold snap, Jane is secretly entering the FreeStyle category,
Emma's science fair project is "The Chemistry of Fermentation", Henry is
judging for the first time and worried about impartiality, and Roland is
defending last year's title.
The week's schedules (Mon guild meeting → Tue test bakes → Wed sourcing →
Thu dough prep → Fri science fair → Sat competition → Sun recovery) are
now fully populated with narrative-specific actions.
Files split for composability:
- behaviors/baker_behaviors.sb → 5 focused files by character/domain
- schema/types.sb → 4 files (core, baking, world, social)
- schedules/work_schedules.sb → 4 files (one per schedule)
- relationships/baker_family_relationships.sb → family + bakery
Strong typing: replaced all enumerable string fields with concepts
(BakerSpecialty, Occupation, LocationType, InstitutionType, ParentingStyle,
Intensity, BakeryDomain, BakingDiscipline, ManagementDomain,
CompetitiveAdvantage).
Remove new-syntax-demo.sb (superseded by baker-family example).
2026-02-23 21:51:01 +00:00
|
|
|
parts,
|
|
|
|
|
&vec!["Sourdough".to_string()],
|
|
|
|
|
"Martha's specialty should be Sourdough"
|
2026-02-13 21:52:03 +00:00
|
|
|
);
|
|
|
|
|
} else {
|
feat(examples): rewrite baker-family as coherent Competition Week narrative
Rewrote the baker-family example around a unified story: the annual Harvest
Baking Competition is Saturday. Martha's sourdough starter Old Maggie is
sluggish from a cold snap, Jane is secretly entering the FreeStyle category,
Emma's science fair project is "The Chemistry of Fermentation", Henry is
judging for the first time and worried about impartiality, and Roland is
defending last year's title.
The week's schedules (Mon guild meeting → Tue test bakes → Wed sourcing →
Thu dough prep → Fri science fair → Sat competition → Sun recovery) are
now fully populated with narrative-specific actions.
Files split for composability:
- behaviors/baker_behaviors.sb → 5 focused files by character/domain
- schema/types.sb → 4 files (core, baking, world, social)
- schedules/work_schedules.sb → 4 files (one per schedule)
- relationships/baker_family_relationships.sb → family + bakery
Strong typing: replaced all enumerable string fields with concepts
(BakerSpecialty, Occupation, LocationType, InstitutionType, ParentingStyle,
Intensity, BakeryDomain, BakingDiscipline, ManagementDomain,
CompetitiveAdvantage).
Remove new-syntax-demo.sb (superseded by baker-family example).
2026-02-23 21:51:01 +00:00
|
|
|
panic!("specialty should be an Identifier");
|
2026-02-13 21:52:03 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_baker_family_has_expected_behaviors() {
|
|
|
|
|
let project = load_example("baker-family");
|
|
|
|
|
|
|
|
|
|
let behaviors: Vec<_> = project.behaviors().collect();
|
|
|
|
|
let behavior_names: Vec<_> = behaviors.iter().map(|b| b.name.as_str()).collect();
|
|
|
|
|
|
|
|
|
|
// Core behaviors for the family
|
|
|
|
|
let expected = vec![
|
|
|
|
|
"BakingWork",
|
|
|
|
|
"PrepKitchen",
|
|
|
|
|
"BasicNeeds",
|
|
|
|
|
"SocialInteraction",
|
|
|
|
|
"BakingSkills",
|
|
|
|
|
"CustomerService",
|
|
|
|
|
"PlayBehavior",
|
|
|
|
|
"LearnBehavior",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for name in expected {
|
|
|
|
|
assert!(
|
|
|
|
|
behavior_names.contains(&name),
|
|
|
|
|
"Should have behavior '{}', got: {:?}",
|
|
|
|
|
name,
|
|
|
|
|
behavior_names
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_baker_family_has_expected_schedules() {
|
|
|
|
|
let project = load_example("baker-family");
|
|
|
|
|
|
|
|
|
|
let schedules: Vec<_> = project.schedules().collect();
|
|
|
|
|
assert!(!schedules.is_empty(), "Should have schedules");
|
|
|
|
|
|
|
|
|
|
let schedule_names: Vec<_> = schedules.iter().map(|s| s.name.as_str()).collect();
|
|
|
|
|
|
|
|
|
|
// Should have work schedules
|
|
|
|
|
assert!(
|
|
|
|
|
schedule_names
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|&n| n.contains("Schedule") || n.contains("Week")),
|
|
|
|
|
"Should have schedule definitions, got: {:?}",
|
|
|
|
|
schedule_names
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_baker_family_multi_file_structure() {
|
|
|
|
|
// Baker family is organized across multiple files:
|
feat(examples): rewrite baker-family as coherent Competition Week narrative
Rewrote the baker-family example around a unified story: the annual Harvest
Baking Competition is Saturday. Martha's sourdough starter Old Maggie is
sluggish from a cold snap, Jane is secretly entering the FreeStyle category,
Emma's science fair project is "The Chemistry of Fermentation", Henry is
judging for the first time and worried about impartiality, and Roland is
defending last year's title.
The week's schedules (Mon guild meeting → Tue test bakes → Wed sourcing →
Thu dough prep → Fri science fair → Sat competition → Sun recovery) are
now fully populated with narrative-specific actions.
Files split for composability:
- behaviors/baker_behaviors.sb → 5 focused files by character/domain
- schema/types.sb → 4 files (core, baking, world, social)
- schedules/work_schedules.sb → 4 files (one per schedule)
- relationships/baker_family_relationships.sb → family + bakery
Strong typing: replaced all enumerable string fields with concepts
(BakerSpecialty, Occupation, LocationType, InstitutionType, ParentingStyle,
Intensity, BakeryDomain, BakingDiscipline, ManagementDomain,
CompetitiveAdvantage).
Remove new-syntax-demo.sb (superseded by baker-family example).
2026-02-23 21:51:01 +00:00
|
|
|
// - schema/core_types.sb, baking_types.sb, world_types.sb, social_types.sb
|
|
|
|
|
// - schema/templates.sb, life_arcs.sb
|
|
|
|
|
// - behaviors/baker_behaviors.sb, person_behaviors.sb, child_behaviors.sb
|
|
|
|
|
// - behaviors/competition_behaviors.sb, henry_behaviors.sb
|
|
|
|
|
// - schedules/base_schedule.sb, baker_schedule.sb, school_schedule.sb,
|
|
|
|
|
// retired_schedule.sb
|
|
|
|
|
// - relationships/family_relationships.sb, bakery_relationships.sb
|
2026-02-13 21:52:03 +00:00
|
|
|
// - characters/*.sb
|
|
|
|
|
|
|
|
|
|
let project = load_example("baker-family");
|
|
|
|
|
|
|
|
|
|
// If all declaration types are present, multi-file loading works
|
|
|
|
|
assert!(project.characters().count() > 0, "Should have characters");
|
|
|
|
|
assert!(project.behaviors().count() > 0, "Should have behaviors");
|
|
|
|
|
assert!(project.schedules().count() > 0, "Should have schedules");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
2026-02-14 14:45:17 +00:00
|
|
|
// Generic Validation Tests
|
2026-02-13 21:52:03 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-14 14:45:17 +00:00
|
|
|
fn test_baker_family_all_characters_have_names_and_fields() {
|
|
|
|
|
let project = load_example("baker-family");
|
2026-02-13 21:52:03 +00:00
|
|
|
|
|
|
|
|
for character in project.characters() {
|
|
|
|
|
assert!(!character.name.is_empty(), "Character should have a name");
|
|
|
|
|
assert!(
|
|
|
|
|
!character.fields.is_empty(),
|
|
|
|
|
"Character {} should have fields",
|
|
|
|
|
character.name
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-14 14:45:17 +00:00
|
|
|
fn test_baker_family_schedules_have_valid_blocks() {
|
|
|
|
|
let project = load_example("baker-family");
|
2026-02-13 21:52:03 +00:00
|
|
|
|
|
|
|
|
for schedule in project.schedules() {
|
|
|
|
|
if schedule.blocks.is_empty() {
|
|
|
|
|
continue; // Some schedules might be empty/templates
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for block in &schedule.blocks {
|
|
|
|
|
// Time ranges should be valid: start < end (or midnight wrap)
|
|
|
|
|
let start_mins = block.start.hour as u32 * 60 + block.start.minute as u32;
|
|
|
|
|
let end_mins = block.end.hour as u32 * 60 + block.end.minute as u32;
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
start_mins < end_mins || end_mins == 0,
|
|
|
|
|
"Block in {} should have valid time: {:02}:{:02}-{:02}:{:02}",
|
|
|
|
|
schedule.name,
|
|
|
|
|
block.start.hour,
|
|
|
|
|
block.start.minute,
|
|
|
|
|
block.end.hour,
|
|
|
|
|
block.end.minute
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-14 14:45:17 +00:00
|
|
|
fn test_baker_family_behaviors_have_valid_structure() {
|
|
|
|
|
let project = load_example("baker-family");
|
2026-02-13 21:52:03 +00:00
|
|
|
|
|
|
|
|
for behavior in project.behaviors() {
|
|
|
|
|
assert!(!behavior.name.is_empty(), "Behavior should have a name");
|
|
|
|
|
// Just verify root node exists
|
|
|
|
|
let _ = &behavior.root;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
2026-02-14 14:45:17 +00:00
|
|
|
// Example Validation
|
2026-02-13 21:52:03 +00:00
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-02-14 14:45:17 +00:00
|
|
|
fn test_baker_family_example_loads_without_errors() {
|
|
|
|
|
// Test that the baker-family example loads successfully
|
|
|
|
|
let result = std::panic::catch_unwind(|| {
|
|
|
|
|
load_example("baker-family");
|
|
|
|
|
});
|
2026-02-13 21:52:03 +00:00
|
|
|
|
2026-02-14 14:45:17 +00:00
|
|
|
assert!(
|
|
|
|
|
result.is_ok(),
|
|
|
|
|
"Baker family example should load without panicking"
|
|
|
|
|
);
|
2026-02-13 21:52:03 +00:00
|
|
|
}
|