use context_weaver::{AssembledBlock, ChatMessage, ContextWeaver, Entry, Lorebook, Slot};
use weaver_lang::Value;
const WORLD_RULES: &str = r#"---
id: world_rules
name: World Rules
constant: true
priority: 200
slot: preamble
---
$[default_var("state:level", 1)]
$[default_var("state:gold", 0)]
$[default_var("state:xp", 0)]
[World: {{char:name}} the {{char:class}}, Level {{state:level}}]
[[lore_header]]"#;
const LORE_HEADER: &str = r#"---
id: lore_header
name: Lore Header
---
The realm of Eldara awaits. Your quest begins now, {{user:name}}."#;
const FOREST_DESC: &str = r#"---
id: forest_desc
name: Dark Forest Description
keywords: ["dark forest", "shadowed path"]
condition: '{{state:location}} == "forest"'
priority: 150
slot: foundation
---
{# if {{state:level}} > 5 #}
The ancient dark forest recognizes a seasoned adventurer.
{# else #}
The dark forest looms, its shadows deep and menacing.
{# endif #}
Threat level: @[text.upper(text: "high")]
$[set_var("state:visited_forest", true)]"#;
const COMBAT_SYSTEM: &str = r#"---
id: combat_system
name: Combat System
regex: ['\b(attack|fight|strike|slash)\b']
priority: 180
slot: framing
---
=== COMBAT ===
{# if {{state:weapon}} == "none" #}
You have no weapon equipped!
{# else #}
Wielding: {{state:weapon}}
Base damage: @[math.mul(a: {{state:level}}, b: 3)]
{# endif #}
{# foreach item in {{state:inventory}} #}
- {{item}}
{# endforeach #}"#;
const MERCHANT: &str = r#"---
id: merchant_encounter
name: Merchant Encounter
keywords: ["merchant", "shop", "trader"]
priority: 140
slot: context
---
The merchant greets you warmly.
$[set_var("state:quest_started", true)]
$[set_var("state:quest_name", "The Goblin Menace")]
$[set_var("state:quest_reward", 50)]
"Browse my wares, or hear about a job?"
<trigger id="quest_log">"#;
const QUEST_LOG: &str = r#"---
id: quest_log
name: Quest Log
condition: '{{state:quest_started}}'
priority: 130
slot: context
---
--- QUEST ACTIVATED ---
[[quest_details]]"#;
const QUEST_DETAILS: &str = r#"---
id: quest_details
name: Quest Details
---
Quest: {{state:quest_name}}
Reward: {{state:quest_reward}} gold"#;
const LEVEL_UP: &str = r#"---
id: level_up
name: Level Up
keywords: ["level up", "leveled up"]
priority: 160
slot: context
---
$[inc_var("state:level", 1)]
$[inc_var("state:gold", 25)]
$[inc_var("state:xp", 100)]
LEVEL UP! You are now level {{state:level}}.
Power rating: @[math.mul(a: {{state:level}}, b: 10)]
Gold: {{state:gold}}
{# if {{state:level}} >= 3 #}
You've unlocked advanced abilities!
{# endif #}"#;
const INVENTORY: &str = r#"---
id: inventory_entry
name: Inventory
keywords: ["inventory", "items", "backpack"]
priority: 120
slot: context
---
=== INVENTORY (@[array.length(items: {{state:inventory}})] items) ===
{# foreach item in {{state:inventory}} #}
* @[text.upper(text: {{item}})]
{# endforeach #}
All items: @[text.join(items: {{state:inventory}}, separator: " | ")]"#;
const COOLDOWN_ENTRY: &str = r#"---
id: cooldown_entry
name: Special Move
keywords: ["special move"]
cooldown: 2
priority: 110
slot: aftermath
---
You execute a devastating special move!"#;
const STICKY_ENTRY: &str = r#"---
id: sticky_entry
name: Sacred Oath
keywords: ["sacred oath"]
sticky_turns: 2
priority: 105
slot: framing
---
[Oath active] Your sacred oath empowers you."#;
const VETERAN_BONUS: &str = r#"---
id: veteran_bonus
name: Veteran Bonus
constant: true
condition: '({{state:level}} >= 3) && ({{state:visited_forest}})'
priority: 90
slot: reference
---
[Veteran bonus: +10% damage in familiar territory]"#;
const EVENT_LOG: &str = r#"---
id: event_log
name: Event Logger
constant: true
priority: 50
slot: aftermath
---
$[push_var("state:event_log", "turn")]"#;
fn build_lorebook() -> Lorebook {
let mut book = Lorebook::new();
let sources = [
WORLD_RULES,
LORE_HEADER,
FOREST_DESC,
COMBAT_SYSTEM,
MERCHANT,
QUEST_LOG,
QUEST_DETAILS,
LEVEL_UP,
INVENTORY,
COOLDOWN_ENTRY,
STICKY_ENTRY,
VETERAN_BONUS,
EVENT_LOG,
];
for source in sources {
let entry = Entry::parse(source, None).unwrap_or_else(|e| {
panic!(
"Failed to parse entry:\n{}\nError: {}",
source.lines().take(5).collect::<Vec<_>>().join("\n"),
e
)
});
book.add_entry(entry);
}
book
}
fn build_engine() -> ContextWeaver {
let book = build_lorebook();
let mut engine = ContextWeaver::new(book);
engine.set_variable("char", "name", "Kael");
engine.set_variable("char", "class", "Ranger");
engine.set_variable("user", "name", "Alex");
engine.set_variable("state", "location", "town");
engine.set_variable("state", "weapon", "none");
engine.set_variable(
"state",
"inventory",
Value::Array(vec![
Value::String("rope".into()),
Value::String("torch".into()),
Value::String("rations".into()),
]),
);
engine
}
fn find_block<'a>(blocks: &'a [AssembledBlock], entry_id: &str) -> Option<&'a AssembledBlock> {
blocks.iter().find(|b| b.entry_id == entry_id)
}
fn assert_block_contains(blocks: &[AssembledBlock], entry_id: &str, substring: &str) {
let block = find_block(blocks, entry_id)
.unwrap_or_else(|| panic!("expected entry '{}' to be active", entry_id));
assert!(
block.content.contains(substring),
"entry '{}' content does not contain '{}'\nactual content:\n{}",
entry_id,
substring,
block.content
);
}
fn assert_block_absent(blocks: &[AssembledBlock], entry_id: &str) {
assert!(
find_block(blocks, entry_id).is_none(),
"expected entry '{}' to NOT be active, but it was.\ncontent: {}",
entry_id,
find_block(blocks, entry_id).map_or("", |b| &b.content),
);
}
fn active_ids(blocks: &[AssembledBlock]) -> Vec<&str> {
blocks.iter().map(|b| b.entry_id.as_str()).collect()
}
#[test]
fn test_multi_turn_rpg_session() {
let mut engine = build_engine();
engine.set_variable("state", "location", "forest");
let messages = vec![ChatMessage::user("I walk into the dark forest")];
let blocks = engine.assemble(&messages).expect("turn 1 failed");
println!("=== TURN 1 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "world_rules", "Kael the Ranger");
assert_block_contains(&blocks, "world_rules", "Level 1");
assert_block_contains(&blocks, "world_rules", "The realm of Eldara");
assert_block_contains(&blocks, "world_rules", "Alex");
assert_block_contains(&blocks, "forest_desc", "dark forest looms");
assert_block_contains(&blocks, "forest_desc", "Threat level: HIGH");
assert_block_absent(&blocks, "veteran_bonus");
let world_block = find_block(&blocks, "world_rules").unwrap();
assert_eq!(world_block.slot, Slot::Preamble);
let forest_block = find_block(&blocks, "forest_desc").unwrap();
assert_eq!(forest_block.slot, Slot::Foundation);
let state = engine.persistent_state();
assert_eq!(
state.get("visited_forest"),
Some(&Value::Bool(true)),
"forest_desc should have set state:visited_forest"
);
engine.advance_turn().unwrap();
engine.set_variable("state", "location", "town");
let messages = vec![
ChatMessage::user("I walk into the dark forest"),
ChatMessage::user("I head back to town and visit the merchant"),
];
let blocks = engine.assemble(&messages).expect("turn 2 failed");
println!("\n=== TURN 2 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "merchant_encounter", "merchant greets you");
assert_block_contains(&blocks, "merchant_encounter", "Browse my wares");
assert_block_contains(&blocks, "quest_log", "QUEST ACTIVATED");
assert_block_contains(&blocks, "quest_log", "The Goblin Menace");
assert_block_contains(&blocks, "quest_log", "50 gold");
assert_block_absent(&blocks, "forest_desc");
let state = engine.persistent_state();
assert_eq!(state.get("quest_started"), Some(&Value::Bool(true)));
assert_eq!(
state.get("quest_name"),
Some(&Value::String("The Goblin Menace".into()))
);
engine.advance_turn().unwrap();
engine.set_variable("state", "weapon", "longbow");
let messages = vec![
ChatMessage::user("I head back to town and visit the merchant"),
ChatMessage::user("A goblin appears! I attack with my bow!"),
];
let blocks = engine.assemble(&messages).expect("turn 3 failed");
println!("\n=== TURN 3 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "combat_system", "COMBAT");
assert_block_contains(&blocks, "combat_system", "Wielding: longbow");
assert_block_contains(&blocks, "combat_system", "Base damage: 3");
assert_block_contains(&blocks, "combat_system", "- rope");
assert_block_contains(&blocks, "combat_system", "- torch");
assert_block_contains(&blocks, "combat_system", "- rations");
engine.advance_turn().unwrap();
let messages = vec![
ChatMessage::user("A goblin appears! I attack with my bow!"),
ChatMessage::user("Victory! I level up!"),
];
let blocks = engine.assemble(&messages).expect("turn 4 failed");
println!("\n=== TURN 4 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "level_up", "LEVEL UP!");
assert_block_contains(&blocks, "level_up", "level 2");
assert_block_contains(&blocks, "level_up", "Power rating: 20");
assert_block_contains(&blocks, "level_up", "Gold: 25");
assert!(
!find_block(&blocks, "level_up")
.unwrap()
.content
.contains("advanced abilities"),
"should not have advanced abilities at level 2"
);
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("Training pays off. I level up again!")];
let blocks = engine.assemble(&messages).expect("turn 4b failed");
println!("\n=== TURN 4b ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "level_up", "level 3");
assert_block_contains(&blocks, "level_up", "Power rating: 30");
assert_block_contains(&blocks, "level_up", "Gold: 50");
assert_block_contains(&blocks, "level_up", "advanced abilities");
let state = engine.persistent_state();
assert_eq!(Some(&Value::Number(3.0)), state.get("level"));
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("Let me check my inventory")];
let blocks = engine.assemble(&messages).expect("turn 5 failed");
println!("\n=== TURN 5 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "inventory_entry", "INVENTORY");
assert_block_contains(&blocks, "inventory_entry", "3 items");
assert_block_contains(&blocks, "inventory_entry", "ROPE");
assert_block_contains(&blocks, "inventory_entry", "TORCH");
assert_block_contains(&blocks, "inventory_entry", "RATIONS");
assert_block_contains(&blocks, "inventory_entry", "rope | torch | rations");
assert_block_contains(&blocks, "veteran_bonus", "Veteran bonus");
assert_block_contains(&blocks, "veteran_bonus", "+10% damage");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("I perform my special move!")];
let blocks = engine.assemble(&messages).expect("turn 6 failed");
println!("\n=== TURN 6 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "cooldown_entry", "devastating special move");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("I try my special move again!")];
let blocks = engine.assemble(&messages).expect("turn 7 failed");
println!("\n=== TURN 7 ===");
println!("Active: {:?}", active_ids(&blocks));
assert_block_absent(&blocks, "cooldown_entry");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("special move, now!")];
let blocks = engine.assemble(&messages).expect("turn 8 failed");
println!("\n=== TURN 8 ===");
println!("Active: {:?}", active_ids(&blocks));
assert_block_contains(&blocks, "cooldown_entry", "devastating special move");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user(
"I swear a sacred oath to protect the realm!",
)];
let blocks = engine.assemble(&messages).expect("turn 9 failed");
println!("\n=== TURN 9 ===");
println!("Active: {:?}", active_ids(&blocks));
assert_block_contains(&blocks, "sticky_entry", "Oath active");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("The weather is pleasant today.")];
let blocks = engine.assemble(&messages).expect("turn 10 failed");
println!("\n=== TURN 10 ===");
println!("Active: {:?}", active_ids(&blocks));
assert_block_contains(&blocks, "sticky_entry", "Oath active");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("Just walking around the market.")];
let blocks = engine.assemble(&messages).expect("turn 11 failed");
println!("\n=== TURN 11 ===");
println!("Active: {:?}", active_ids(&blocks));
assert_block_contains(&blocks, "sticky_entry", "Oath active");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("Still walking around.")];
let blocks = engine.assemble(&messages).expect("turn 12 failed");
println!("\n=== TURN 12 ===");
println!("Active: {:?}", active_ids(&blocks));
assert_block_absent(&blocks, "sticky_entry");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("An orc attacks! I fight back!")];
let blocks = engine.assemble(&messages).expect("turn 13 failed");
println!("\n=== TURN 13 ===");
for b in &blocks {
println!("[{}] {}: {}", b.slot, b.entry_id, b.content.trim());
}
assert_block_contains(&blocks, "combat_system", "Wielding: longbow");
assert_block_contains(&blocks, "combat_system", "Base damage: 9");
assert_block_contains(&blocks, "veteran_bonus", "Veteran bonus");
let state = engine.persistent_state();
assert_eq!(state.get("level"), Some(&Value::Number(3.0)));
assert_eq!(state.get("gold"), Some(&Value::Number(50.0)));
assert_eq!(state.get("xp"), Some(&Value::Number(200.0)));
assert_eq!(state.get("quest_started"), Some(&Value::Bool(true)));
assert_eq!(
state.get("quest_name"),
Some(&Value::String("The Goblin Menace".into()))
);
assert_eq!(state.get("visited_forest"), Some(&Value::Bool(true)));
assert_eq!(state.get("weapon"), Some(&Value::String("longbow".into())));
match state.get("event_log") {
Some(Value::Array(log)) => {
assert!(
log.len() >= 5,
"event log should have accumulated multiple entries, got {}",
log.len()
);
for entry in log {
assert_eq!(entry, &Value::String("turn".into()));
}
}
other => panic!("expected event_log to be an array, got {:?}", other),
}
println!("\n=== ALL TURNS PASSED ===");
println!("Final state: {:#?}", state);
}
#[test]
fn test_document_chain_with_state_mutation() {
let mut book = Lorebook::new();
let entry_a = Entry::parse(
r#"---
id: entry_a
constant: true
---
$[set_var("state:greeting", "Hello")]
From A: [[doc_b]]"#,
None,
)
.unwrap();
let entry_b = Entry::parse(
r#"---
id: doc_b
---
{{state:greeting}} from B! [[doc_c]]"#,
None,
)
.unwrap();
let entry_c = Entry::parse(
r#"---
id: doc_c
---
End."#,
None,
)
.unwrap();
book.add_entry(entry_a);
book.add_entry(entry_b);
book.add_entry(entry_c);
let mut engine = ContextWeaver::new(book);
let blocks = engine.assemble(&[]).unwrap();
let block = find_block(&blocks, "entry_a").expect("entry_a should be active");
assert!(
block.content.contains("Hello from B!"),
"doc_b should see state:greeting set by entry_a. Got: {}",
block.content
);
assert!(
block.content.contains("End."),
"doc_c should be inlined through doc_b. Got: {}",
block.content
);
}
#[test]
fn test_trigger_with_state_dependent_condition() {
let mut book = Lorebook::new();
let setter = Entry::parse(
r#"---
id: the_setter
keywords: ["go"]
priority: 200
---
$[set_var("state:flag", true)]
Setter ran.
<trigger id="the_gated">"#,
None,
)
.unwrap();
let gated = Entry::parse(
r#"---
id: the_gated
condition: '{{state:flag}}'
---
Gate opened!"#,
None,
)
.unwrap();
book.add_entry(setter);
book.add_entry(gated);
let mut engine = ContextWeaver::new(book);
let messages = vec![ChatMessage::user("let's go")];
let blocks = engine.assemble(&messages).unwrap();
assert_block_contains(&blocks, "the_setter", "Setter ran");
assert_block_contains(&blocks, "the_gated", "Gate opened");
}
#[test]
fn test_condition_blocks_keyword_activation() {
let mut book = Lorebook::new();
let entry = Entry::parse(
r#"---
id: gated
keywords: ["magic"]
condition: '{{state:mana}} > 0'
---
You cast a spell!"#,
None,
)
.unwrap();
book.add_entry(entry);
let mut engine = ContextWeaver::new(book);
let messages = vec![ChatMessage::user("I use magic!")];
let blocks = engine.assemble(&messages).unwrap();
assert_block_absent(&blocks, "gated");
engine.advance_turn().unwrap();
engine.set_variable("state", "mana", 10i64);
let blocks = engine.assemble(&messages).unwrap();
assert_block_contains(&blocks, "gated", "cast a spell");
}
#[test]
fn test_regex_activation() {
let mut book = Lorebook::new();
let entry = Entry::parse(
r#"---
id: regex_test
regex: ['\d{3,}']
---
Large number detected!"#,
None,
)
.unwrap();
book.add_entry(entry);
let mut engine = ContextWeaver::new(book);
let messages = vec![ChatMessage::user("just 42 gold")];
let blocks = engine.assemble(&messages).unwrap();
assert_block_absent(&blocks, "regex_test");
engine.advance_turn().unwrap();
let messages = vec![ChatMessage::user("I found 1000 gold!")];
let blocks = engine.assemble(&messages).unwrap();
assert_block_contains(&blocks, "regex_test", "Large number detected");
}
#[test]
fn test_disabled_entry_skipped() {
let mut book = Lorebook::new();
let entry = Entry::parse(
r#"---
id: disabled_one
keywords: ["hello"]
enabled: false
---
You should never see this."#,
None,
)
.unwrap();
book.add_entry(entry);
let mut engine = ContextWeaver::new(book);
let messages = vec![ChatMessage::user("hello there!")];
let blocks = engine.assemble(&messages).unwrap();
assert_block_absent(&blocks, "disabled_one");
}
#[test]
fn test_state_persistence_across_turns() {
let mut book = Lorebook::new();
let setter = Entry::parse(
r#"---
id: state_setter
keywords: ["set"]
---
$[set_var("state:persistent_val", 42)]
Set."#,
None,
)
.unwrap();
let reader = Entry::parse(
r#"---
id: state_reader
keywords: ["read"]
---
Value is {{state:persistent_val}}."#,
None,
)
.unwrap();
book.add_entry(setter);
book.add_entry(reader);
let mut engine = ContextWeaver::new(book);
let blocks = engine
.assemble(&[ChatMessage::user("please set the value")])
.unwrap();
assert_block_contains(&blocks, "state_setter", "Set");
engine.advance_turn().unwrap();
let blocks = engine
.assemble(&[ChatMessage::user("now read it back")])
.unwrap();
assert_block_contains(&blocks, "state_reader", "Value is 42");
}
#[test]
fn test_assembly_ordering() {
let mut book = Lorebook::new();
let high = Entry::parse(
r#"---
id: high_prio
constant: true
priority: 200
slot: context
---
HIGH"#,
None,
)
.unwrap();
let low = Entry::parse(
r#"---
id: low_prio
constant: true
priority: 50
slot: context
---
LOW"#,
None,
)
.unwrap();
let early_slot = Entry::parse(
r#"---
id: early_slot
constant: true
priority: 100
slot: preamble
---
FIRST"#,
None,
)
.unwrap();
book.add_entry(high);
book.add_entry(low);
book.add_entry(early_slot);
let mut engine = ContextWeaver::new(book);
let blocks = engine.assemble(&[]).unwrap();
let positions: Vec<_> = blocks.iter().map(|b| b.entry_id.as_str()).collect();
let early_idx = positions.iter().position(|&id| id == "early_slot");
let high_idx = positions.iter().position(|&id| id == "high_prio");
assert!(
early_idx < high_idx,
"preamble should precede context in output. Order: {:?}",
positions
);
let low_idx = positions.iter().position(|&id| id == "low_prio");
assert!(
high_idx < low_idx,
"higher priority should come first within same slot. Order: {:?}",
positions
);
}
#[test]
fn test_slot_fallback_resolution() {
let mut book = Lorebook::new();
let entry = Entry::parse(
r#"---
id: fallback_entry
constant: true
slot: reference
fallback: [context]
---
Fallback content"#,
None,
)
.unwrap();
book.add_entry(entry);
let mut engine = ContextWeaver::new(book);
engine.set_available_slots(vec![Slot::Preamble, Slot::Context, Slot::Aftermath]);
let blocks = engine.assemble(&[]).unwrap();
let block = find_block(&blocks, "fallback_entry").expect("entry should be active via fallback");
assert_eq!(
block.slot,
Slot::Context,
"entry should have fallen back to context slot"
);
}
#[test]
fn test_unavailable_slot_drops_entry() {
let mut book = Lorebook::new();
let entry = Entry::parse(
r#"---
id: no_slot_entry
constant: true
slot: emphasis
---
This should be dropped"#,
None,
)
.unwrap();
book.add_entry(entry);
let mut engine = ContextWeaver::new(book);
engine.set_available_slots(vec![Slot::Preamble]);
let blocks = engine.assemble(&[]).unwrap();
assert_block_absent(&blocks, "no_slot_entry");
}
#[test]
fn test_at_depth_always_resolves() {
let mut book = Lorebook::new();
let entry = Entry::parse(
r#"---
id: depth_entry
constant: true
slot: !at_depth 3
---
Injected at depth"#,
None,
)
.unwrap();
book.add_entry(entry);
let mut engine = ContextWeaver::new(book);
engine.set_available_slots(vec![]);
let blocks = engine.assemble(&[]).unwrap();
assert_block_contains(&blocks, "depth_entry", "Injected at depth");
let block = find_block(&blocks, "depth_entry").unwrap();
assert_eq!(block.slot, Slot::AtDepth(3));
}