use bevy_ecs::prelude::*;
use leviath_core::{EvictionStrategy, Region, RegionKind};
use leviath_runtime::components::MessageInbox;
use leviath_runtime::{AgentState, AgentStatus, ContextWindow, ParentRef, SubAgentChildren};
#[test]
fn test_pinned_region_never_evicted() {
let region = Region::new("pinned".to_string(), RegionKind::Pinned, 5000);
assert!(matches!(region.kind, RegionKind::Pinned));
assert_eq!(region.max_tokens, 5000);
}
#[test]
fn test_sliding_window_configuration() {
let region = Region::new(
"conversation".to_string(),
RegionKind::SlidingWindow {
max_items: 10,
eviction_strategy: EvictionStrategy::PerItem,
},
8000,
);
match region.kind {
RegionKind::SlidingWindow { max_items, .. } => {
assert_eq!(max_items, 10);
}
_ => panic!("Expected SlidingWindow region"),
}
}
#[test]
fn test_temporary_region_properties() {
let region = Region::new("temp".to_string(), RegionKind::Temporary, 10000);
assert!(matches!(region.kind, RegionKind::Temporary));
}
#[test]
fn test_compacting_region_threshold() {
let region = Region::new(
"historical".to_string(),
RegionKind::Compacting {
threshold_tokens: 8000,
},
12000,
);
match region.kind {
RegionKind::Compacting { threshold_tokens } => {
assert_eq!(threshold_tokens, 8000);
}
_ => panic!("Expected Compacting region"),
}
}
#[test]
fn test_eviction_cascade_temporary_then_compacting() {
let mut window = ContextWindow::new(10000);
let mut clearable = Region::new("scratch".to_string(), RegionKind::Clearable, 3000);
clearable
.add_entry("scratch data".to_string(), 1500)
.unwrap();
window.add_region(clearable);
let mut temp = Region::new("temp".to_string(), RegionKind::Temporary, 4000);
temp.add_entry("temp old".to_string(), 1000).unwrap();
temp.add_entry("temp new".to_string(), 1000).unwrap();
window.add_region(temp);
let mut sliding = Region::new(
"conversation".to_string(),
RegionKind::SlidingWindow {
max_items: 5,
eviction_strategy: EvictionStrategy::PerItem,
},
4000,
);
sliding.add_entry("msg 1".to_string(), 500).unwrap();
sliding.add_entry("msg 2".to_string(), 500).unwrap();
window.add_region(sliding);
assert_eq!(window.current_tokens, 4500);
let result = window.try_evict(1000).unwrap();
assert!(result.tokens_freed >= 1500);
assert_eq!(window.get_region("scratch").unwrap().current_tokens, 0);
assert_eq!(window.get_region("conversation").unwrap().entry_count(), 2);
}
#[test]
fn test_schema_validation_json() {
use leviath_core::region::{ContentFormat, RegionSchema};
let schema = RegionSchema::new(ContentFormat::Json);
assert!(schema.validate(r#"{"key": "value"}"#).is_ok());
assert!(schema.validate("not json").is_err());
}
#[test]
fn test_schema_validation_mermaid() {
use leviath_core::region::{ContentFormat, RegionSchema};
let schema = RegionSchema::new(ContentFormat::Mermaid);
assert!(schema.validate("graph TD\n A --> B").is_ok());
assert!(
schema
.validate("sequenceDiagram\n Alice->>Bob: Hello")
.is_ok()
);
assert!(schema.validate("just plain text").is_err());
}
#[test]
fn test_token_budget_enforcement() {
let mut region = Region::new("test".to_string(), RegionKind::Pinned, 1000);
assert!(region.add_entry("small".to_string(), 100).is_ok());
assert_eq!(region.current_tokens, 100);
assert!(region.add_entry("medium".to_string(), 500).is_ok());
assert_eq!(region.current_tokens, 600);
let result = region.add_entry("too large".to_string(), 500);
assert!(result.is_err());
assert_eq!(region.current_tokens, 600); }
#[test]
fn test_region_content_management() {
let mut region = Region::new("test".to_string(), RegionKind::Temporary, 5000);
region.add_entry("entry 1".to_string(), 100).unwrap();
region.add_entry("entry 2".to_string(), 200).unwrap();
region.add_entry("entry 3".to_string(), 300).unwrap();
assert_eq!(region.entry_count(), 3);
assert_eq!(region.current_tokens, 600);
let removed = region.remove_oldest().unwrap();
assert_eq!(removed.content, "entry 1");
assert_eq!(removed.tokens, 100);
assert_eq!(region.entry_count(), 2);
assert_eq!(region.current_tokens, 500);
region.clear();
assert_eq!(region.entry_count(), 0);
assert_eq!(region.current_tokens, 0);
}
#[test]
fn test_compacting_region_needs_compaction() {
let mut region = Region::new(
"findings".to_string(),
RegionKind::Compacting {
threshold_tokens: 500,
},
2000,
);
region.add_entry("data".to_string(), 300).unwrap();
assert!(!region.needs_compaction());
region.add_entry("more data".to_string(), 300).unwrap();
assert!(region.needs_compaction());
}
#[test]
fn test_context_window_add_to_region() {
let mut window = ContextWindow::new(10000);
let region = Region::new("system".to_string(), RegionKind::Pinned, 2000);
window.add_region(region);
let region = Region::new("scratch".to_string(), RegionKind::Clearable, 3000);
window.add_region(region);
assert!(
window
.add_to_region("system", "Hello".to_string(), 10)
.is_ok()
);
assert_eq!(window.current_tokens, 10);
assert!(
window
.add_to_region("nonexistent", "test".to_string(), 5)
.is_err()
);
}
#[test]
fn test_eviction_result_needs_compaction_when_compacting_full() {
let mut window = ContextWindow::new(1500);
let mut compacting = Region::new(
"analysis".to_string(),
RegionKind::Compacting {
threshold_tokens: 1000,
},
1400,
);
compacting
.add_entry("data block 1".to_string(), 600)
.unwrap();
compacting
.add_entry("data block 2".to_string(), 600)
.unwrap();
window.add_region(compacting);
assert_eq!(window.current_tokens, 1200);
let result = window.try_evict(500).unwrap();
assert_eq!(result.tokens_freed, 0);
assert_eq!(result.needs_compaction, vec!["analysis"]);
}
#[test]
fn test_eviction_clears_then_identifies_compaction() {
let mut window = ContextWindow::new(1800);
let mut clearable = Region::new("scratch".to_string(), RegionKind::Clearable, 1000);
clearable
.add_entry("scratch stuff".to_string(), 400)
.unwrap();
window.add_region(clearable);
let mut compacting = Region::new(
"impl".to_string(),
RegionKind::Compacting {
threshold_tokens: 800,
},
1200,
);
compacting.add_entry("impl data".to_string(), 900).unwrap();
window.add_region(compacting);
assert_eq!(window.current_tokens, 1300);
let result = window.try_evict(1000).unwrap();
assert_eq!(result.tokens_freed, 400);
assert_eq!(window.get_region("scratch").unwrap().current_tokens, 0);
assert_eq!(result.needs_compaction, vec!["impl"]);
}
#[test]
fn test_parent_ref_and_children_components() {
let mut world = World::new();
let parent = world
.spawn((
AgentState {
agent_id: "coder-01".to_string(),
current_stage: "analyze".to_string(),
iteration: 0,
status: AgentStatus::Active,
spawned_children_ids: Vec::new(),
pending_wait: None,
accepts_messages: true,
},
MessageInbox::new(),
))
.id();
let child = world
.spawn((
AgentState {
agent_id: "researcher-01".to_string(),
current_stage: "research".to_string(),
iteration: 0,
status: AgentStatus::Active,
spawned_children_ids: Vec::new(),
pending_wait: None,
accepts_messages: true,
},
ParentRef {
parent_entity: parent,
parent_agent_id: "coder-01".to_string(),
depth: 1,
},
MessageInbox::new(),
))
.id();
world.entity_mut(parent).insert(SubAgentChildren {
children: vec![child],
max_child_depth: 3,
});
let parent_children = world.get::<SubAgentChildren>(parent).unwrap();
assert_eq!(parent_children.children.len(), 1);
assert_eq!(parent_children.children[0], child);
let child_parent = world.get::<ParentRef>(child).unwrap();
assert_eq!(child_parent.parent_entity, parent);
assert_eq!(child_parent.parent_agent_id, "coder-01");
assert_eq!(child_parent.depth, 1);
}
#[test]
fn test_spawn_depth_validation() {
let current_depth = 0_usize;
let max_depth = 3_usize;
let child_depth = current_depth + 1;
assert!(
child_depth <= max_depth,
"Should be able to spawn at depth 1"
);
let current_depth = 3_usize;
let child_depth = current_depth + 1;
assert!(
child_depth > max_depth,
"Should NOT be able to spawn beyond max depth"
);
}
#[test]
fn test_child_completion_notifies_parent() {
let mut world = World::new();
let mut parent_window = ContextWindow::new(10000);
parent_window.add_region(Region::new(
"conversation".to_string(),
RegionKind::SlidingWindow {
max_items: 50,
eviction_strategy: EvictionStrategy::PerItem,
},
8000,
));
let parent = world
.spawn((
AgentState {
agent_id: "parent-01".to_string(),
current_stage: "main".to_string(),
iteration: 0,
status: AgentStatus::Active,
spawned_children_ids: vec!["child-01".to_string()],
pending_wait: Some("child-01".to_string()),
accepts_messages: true,
},
parent_window,
MessageInbox::new(),
))
.id();
let _child = world
.spawn((
AgentState {
agent_id: "child-01".to_string(),
current_stage: "main".to_string(),
iteration: 5,
status: AgentStatus::Complete,
spawned_children_ids: Vec::new(),
pending_wait: None,
accepts_messages: true,
},
ParentRef {
parent_entity: parent,
parent_agent_id: "parent-01".to_string(),
depth: 1,
},
MessageInbox::new(),
))
.id();
let parent_state = world.get::<AgentState>(parent).unwrap();
assert!(
parent_state
.spawned_children_ids
.contains(&"child-01".to_string())
);
assert_eq!(parent_state.pending_wait, Some("child-01".to_string()));
if let Some(mut state) = world.get_mut::<AgentState>(parent) {
state.spawned_children_ids.retain(|id| id != "child-01");
state.pending_wait = None;
}
let parent_state = world.get::<AgentState>(parent).unwrap();
assert!(parent_state.spawned_children_ids.is_empty());
assert!(parent_state.pending_wait.is_none());
}
#[test]
fn test_stage_gating_with_requires_children() {
let mut state = AgentState {
agent_id: "test-01".to_string(),
current_stage: "analyze".to_string(),
iteration: 3,
status: AgentStatus::Active,
spawned_children_ids: vec!["researcher-01".to_string()],
pending_wait: Some("researcher-01".to_string()),
accepts_messages: true,
};
if matches!(state.status, AgentStatus::Active) && state.pending_wait.is_some() {
state.status = AgentStatus::Waiting;
}
assert!(matches!(state.status, AgentStatus::Waiting));
state.spawned_children_ids.clear();
state.pending_wait = None;
if matches!(state.status, AgentStatus::Waiting)
&& state.spawned_children_ids.is_empty()
&& state.pending_wait.is_none()
{
state.status = AgentStatus::Active;
}
assert!(matches!(state.status, AgentStatus::Active));
}
#[tokio::test]
async fn test_file_tracking_sync_assembly_integration() {
use leviath_core::{Region, RegionKind};
use std::sync::Arc;
use tokio::sync::Mutex;
let mut entity_cw = ContextWindow::new(200_000);
let files_region = Region::new(
"files".to_string(),
RegionKind::HashMap {
max_entries: Some(50),
},
40_000,
);
entity_cw.add_region(files_region);
let shared_cw: Arc<Mutex<Option<ContextWindow>>> =
Arc::new(Mutex::new(Some(entity_cw.clone())));
{
let mut guard = shared_cw.lock().await;
if let Some(window) = guard.as_mut()
&& let Some(region) = window.get_region_mut("files")
{
region
.upsert_by_key(
"src/main.py",
"def main():\n print('hello')".to_string(),
10,
)
.unwrap();
region
.upsert_by_key(
"src/utils.py",
"def helper():\n return 42".to_string(),
8,
)
.unwrap();
}
}
{
let guard = shared_cw.lock().await;
if let Some(shared) = guard.as_ref() {
entity_cw.regions = shared.regions.clone();
entity_cw.current_tokens = shared.current_tokens;
}
}
let assembled = entity_cw.assemble();
assert_eq!(assembled.system_blocks.len(), 1);
let block_text = &assembled.system_blocks[0].text;
assert!(block_text.contains("[files]:"));
assert!(block_text.contains("### [src/main.py]"));
assert!(block_text.contains("def main()"));
assert!(block_text.contains("### [src/utils.py]"));
assert!(block_text.contains("def helper()"));
}