mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 16:19:21 +00:00
fix(tests): replace hardcoded /tmp paths with tempdir + add 300 unit tests (#659)
* test: add unit tests across 20 modules for coverage push Add 300+ unit tests covering config, context, evaluation, extensions, LLM, secrets, tools/builder, and tools/mcp modules. All tests are pure unit tests (no mocks) exercising serde roundtrips, edge cases, error paths, and business logic. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(tests): replace hardcoded /tmp paths with tempfile::tempdir The e2e_metrics_test::test_metrics_collected_from_tool_trace test was failing because setup_test_dir() created /tmp/ironclaw_metrics_test but the fixture referenced /tmp/ironclaw_e2e_test/hello.txt (path mismatch). Added LlmTrace::replace_paths() to substitute fixture paths at runtime, then converted all 12 test files from hardcoded /tmp/ironclaw_* paths to tempfile::tempdir(). Tests are now isolated, parallel-safe, and leave no debris on disk. Regression test: test_metrics_collected_from_tool_trace now passes consistently regardless of prior /tmp state. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8fbb782090
commit
cf96a3253c
@@ -490,4 +490,391 @@ mod tests {
|
||||
assert_eq!(ctx.state, crate::context::JobState::InProgress);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_context_not_found() {
|
||||
let manager = ContextManager::new(5);
|
||||
let bogus_id = Uuid::new_v4();
|
||||
let result = manager.get_context(bogus_id).await;
|
||||
assert!(matches!(result, Err(JobError::NotFound { id }) if id == bogus_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_context_not_found() {
|
||||
let manager = ContextManager::new(5);
|
||||
let bogus_id = Uuid::new_v4();
|
||||
let result = manager.update_context(bogus_id, |_ctx| {}).await;
|
||||
assert!(matches!(result, Err(JobError::NotFound { id }) if id == bogus_id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remove_job_returns_context_and_memory() {
|
||||
let manager = ContextManager::new(5);
|
||||
let job_id = manager.create_job("Removable", "bye bye").await.unwrap();
|
||||
|
||||
let (ctx, mem) = manager.remove_job(job_id).await.unwrap();
|
||||
assert_eq!(ctx.title, "Removable");
|
||||
assert_eq!(mem.job_id, job_id);
|
||||
|
||||
// After removal, get should fail
|
||||
assert!(matches!(
|
||||
manager.get_context(job_id).await,
|
||||
Err(JobError::NotFound { .. })
|
||||
));
|
||||
assert!(matches!(
|
||||
manager.get_memory(job_id).await,
|
||||
Err(JobError::NotFound { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remove_job_not_found() {
|
||||
let manager = ContextManager::new(5);
|
||||
let result = manager.remove_job(Uuid::new_v4()).await;
|
||||
assert!(matches!(result, Err(JobError::NotFound { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_memory_and_update_memory() {
|
||||
let manager = ContextManager::new(5);
|
||||
let job_id = manager.create_job("Mem test", "desc").await.unwrap();
|
||||
|
||||
// Fresh memory should be empty
|
||||
let mem = manager.get_memory(job_id).await.unwrap();
|
||||
assert_eq!(mem.job_id, job_id);
|
||||
assert!(mem.actions.is_empty());
|
||||
assert!(mem.conversation.is_empty());
|
||||
|
||||
// Update memory by adding a message
|
||||
manager
|
||||
.update_memory(job_id, |m| {
|
||||
m.add_message(crate::llm::ChatMessage::user("hello from test"));
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mem = manager.get_memory(job_id).await.unwrap();
|
||||
assert_eq!(mem.conversation.len(), 1);
|
||||
assert_eq!(mem.conversation.messages()[0].content, "hello from test");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_memory_not_found() {
|
||||
let manager = ContextManager::new(5);
|
||||
let result = manager.update_memory(Uuid::new_v4(), |_| {}).await;
|
||||
assert!(matches!(result, Err(JobError::NotFound { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_memory_not_found() {
|
||||
let manager = ContextManager::new(5);
|
||||
let result = manager.get_memory(Uuid::new_v4()).await;
|
||||
assert!(matches!(result, Err(JobError::NotFound { .. })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find_stuck_jobs_returns_only_stuck() {
|
||||
let manager = ContextManager::new(10);
|
||||
|
||||
let id1 = manager.create_job("Job 1", "desc").await.unwrap();
|
||||
let id2 = manager.create_job("Job 2", "desc").await.unwrap();
|
||||
let id3 = manager.create_job("Job 3", "desc").await.unwrap();
|
||||
|
||||
// Transition id1 and id2 to InProgress, then mark id2 as stuck
|
||||
for id in [id1, id2, id3] {
|
||||
manager
|
||||
.update_context(id, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
manager
|
||||
.update_context(id2, |ctx| ctx.mark_stuck("timed out"))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let stuck = manager.find_stuck_jobs().await;
|
||||
assert_eq!(stuck.len(), 1);
|
||||
assert_eq!(stuck[0], id2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn active_count_tracks_non_terminal_jobs() {
|
||||
let manager = ContextManager::new(10);
|
||||
|
||||
let id1 = manager.create_job("J1", "d").await.unwrap();
|
||||
let id2 = manager.create_job("J2", "d").await.unwrap();
|
||||
|
||||
// Both pending (active)
|
||||
assert_eq!(manager.active_count().await, 2);
|
||||
|
||||
// Transition id1 through to Failed (terminal)
|
||||
manager
|
||||
.update_context(id1, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
manager
|
||||
.update_context(id1, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::Failed, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// id1 is terminal, id2 still pending
|
||||
assert_eq!(manager.active_count().await, 1);
|
||||
|
||||
// Transition id2 to cancelled
|
||||
manager
|
||||
.update_context(id2, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::Cancelled, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manager.active_count().await, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn active_jobs_for_filters_by_user() {
|
||||
let manager = ContextManager::new(10);
|
||||
|
||||
manager
|
||||
.create_job_for_user("alice", "A1", "d")
|
||||
.await
|
||||
.unwrap();
|
||||
manager
|
||||
.create_job_for_user("alice", "A2", "d")
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_id = manager.create_job_for_user("bob", "B1", "d").await.unwrap();
|
||||
|
||||
assert_eq!(manager.active_jobs_for("alice").await.len(), 2);
|
||||
assert_eq!(manager.active_jobs_for("bob").await.len(), 1);
|
||||
assert_eq!(manager.active_jobs_for("nobody").await.len(), 0);
|
||||
|
||||
// Make bob's job terminal
|
||||
manager
|
||||
.update_context(bob_id, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
manager
|
||||
.update_context(bob_id, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::Failed, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(manager.active_jobs_for("bob").await.len(), 0);
|
||||
// But all_jobs_for still shows it
|
||||
assert_eq!(manager.all_jobs_for("bob").await.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn summary_counts_states_correctly() {
|
||||
let manager = ContextManager::new(10);
|
||||
|
||||
let id1 = manager.create_job("J1", "d").await.unwrap();
|
||||
let id2 = manager.create_job("J2", "d").await.unwrap();
|
||||
let id3 = manager.create_job("J3", "d").await.unwrap();
|
||||
|
||||
// id1: Pending -> InProgress -> Completed
|
||||
manager
|
||||
.update_context(id1, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
manager
|
||||
.update_context(id1, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::Completed, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// id2: Pending -> InProgress -> Failed
|
||||
manager
|
||||
.update_context(id2, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
manager
|
||||
.update_context(id2, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::Failed, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// id3: stays Pending
|
||||
|
||||
let s = manager.summary().await;
|
||||
assert_eq!(s.total, 3);
|
||||
assert_eq!(s.pending, 1);
|
||||
assert_eq!(s.completed, 1);
|
||||
assert_eq!(s.failed, 1);
|
||||
assert_eq!(s.in_progress, 0);
|
||||
assert_eq!(s.stuck, 0);
|
||||
assert_eq!(s.cancelled, 0);
|
||||
assert_eq!(s.submitted, 0);
|
||||
assert_eq!(s.accepted, 0);
|
||||
|
||||
// Suppress unused field warning
|
||||
let _ = id3;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn summary_for_scopes_to_user() {
|
||||
let manager = ContextManager::new(10);
|
||||
|
||||
manager
|
||||
.create_job_for_user("alice", "A1", "d")
|
||||
.await
|
||||
.unwrap();
|
||||
let bob_id = manager.create_job_for_user("bob", "B1", "d").await.unwrap();
|
||||
|
||||
// Transition bob's job to InProgress
|
||||
manager
|
||||
.update_context(bob_id, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
let alice_summary = manager.summary_for("alice").await;
|
||||
assert_eq!(alice_summary.total, 1);
|
||||
assert_eq!(alice_summary.pending, 1);
|
||||
assert_eq!(alice_summary.in_progress, 0);
|
||||
|
||||
let bob_summary = manager.summary_for("bob").await;
|
||||
assert_eq!(bob_summary.total, 1);
|
||||
assert_eq!(bob_summary.pending, 0);
|
||||
assert_eq!(bob_summary.in_progress, 1);
|
||||
|
||||
let nobody_summary = manager.summary_for("nobody").await;
|
||||
assert_eq!(nobody_summary.total, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_context_manager_has_max_10() {
|
||||
let manager = ContextManager::default();
|
||||
// Create 10 jobs and make them active
|
||||
for i in 0..10 {
|
||||
let id = manager
|
||||
.create_job(format!("Job {i}"), "desc")
|
||||
.await
|
||||
.unwrap();
|
||||
manager
|
||||
.update_context(id, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
}
|
||||
// 11th should fail
|
||||
let result = manager.create_job("overflow", "d").await;
|
||||
assert!(matches!(result, Err(JobError::MaxJobsExceeded { max: 10 })));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_jobs_returns_all_regardless_of_state() {
|
||||
let manager = ContextManager::new(10);
|
||||
|
||||
let id1 = manager.create_job("J1", "d").await.unwrap();
|
||||
manager.create_job("J2", "d").await.unwrap();
|
||||
|
||||
// Make id1 terminal
|
||||
manager
|
||||
.update_context(id1, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::InProgress, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
manager
|
||||
.update_context(id1, |ctx| {
|
||||
ctx.transition_to(crate::context::JobState::Failed, None)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// all_jobs includes terminal, active_jobs does not
|
||||
assert_eq!(manager.all_jobs().await.len(), 2);
|
||||
assert_eq!(manager.active_jobs().await.len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_job_uses_default_user() {
|
||||
let manager = ContextManager::new(5);
|
||||
let job_id = manager.create_job("Test", "desc").await.unwrap();
|
||||
let ctx = manager.get_context(job_id).await.unwrap();
|
||||
assert_eq!(ctx.user_id, "default");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_remove_and_read() {
|
||||
let manager = std::sync::Arc::new(ContextManager::new(100));
|
||||
|
||||
// Create 20 jobs
|
||||
let mut job_ids = Vec::new();
|
||||
for i in 0..20 {
|
||||
let id = manager
|
||||
.create_job(format!("Job {i}"), "desc")
|
||||
.await
|
||||
.unwrap();
|
||||
job_ids.push(id);
|
||||
}
|
||||
|
||||
// Concurrently remove the first 10 while reading the last 10
|
||||
let remove_handles: Vec<_> = job_ids[..10]
|
||||
.iter()
|
||||
.map(|&id| {
|
||||
let mgr = std::sync::Arc::clone(&manager);
|
||||
tokio::spawn(async move { mgr.remove_job(id).await })
|
||||
})
|
||||
.collect();
|
||||
|
||||
let read_handles: Vec<_> = job_ids[10..]
|
||||
.iter()
|
||||
.map(|&id| {
|
||||
let mgr = std::sync::Arc::clone(&manager);
|
||||
tokio::spawn(async move { mgr.get_context(id).await })
|
||||
})
|
||||
.collect();
|
||||
|
||||
for handle in remove_handles {
|
||||
handle
|
||||
.await
|
||||
.expect("remove task should not panic")
|
||||
.expect("remove should succeed");
|
||||
}
|
||||
|
||||
for handle in read_handles {
|
||||
let ctx = handle
|
||||
.await
|
||||
.expect("read task should not panic")
|
||||
.expect("read should succeed");
|
||||
assert!(job_ids[10..].contains(&ctx.job_id));
|
||||
}
|
||||
|
||||
assert_eq!(manager.all_jobs().await.len(), 10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,4 +290,276 @@ mod tests {
|
||||
assert_eq!(memory.total_duration(), Duration::from_secs(3));
|
||||
assert_eq!(memory.successful_actions(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_record_fail() {
|
||||
let action = ActionRecord::new(1, "broken_tool", serde_json::json!({"x": 1}));
|
||||
let action = action.fail("something went wrong", Duration::from_millis(50));
|
||||
|
||||
assert!(!action.success);
|
||||
assert_eq!(action.error.as_deref(), Some("something went wrong"));
|
||||
assert_eq!(action.duration, Duration::from_millis(50));
|
||||
assert!(action.output_raw.is_none());
|
||||
assert!(action.output_sanitized.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_record_with_warnings() {
|
||||
let action = ActionRecord::new(0, "risky_tool", serde_json::json!({}));
|
||||
let action = action.with_warnings(vec!["suspicious pattern".into(), "possible xss".into()]);
|
||||
|
||||
assert_eq!(action.sanitization_warnings.len(), 2);
|
||||
assert_eq!(action.sanitization_warnings[0], "suspicious pattern");
|
||||
assert_eq!(action.sanitization_warnings[1], "possible xss");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_record_with_cost() {
|
||||
let action = ActionRecord::new(0, "expensive_tool", serde_json::json!({}));
|
||||
let cost = Decimal::new(42, 2); // 0.42
|
||||
let action = action.with_cost(cost);
|
||||
|
||||
assert_eq!(action.cost, Some(Decimal::new(42, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_record_new_defaults() {
|
||||
let action = ActionRecord::new(5, "my_tool", serde_json::json!({"key": "val"}));
|
||||
|
||||
assert_eq!(action.sequence, 5);
|
||||
assert_eq!(action.tool_name, "my_tool");
|
||||
assert_eq!(action.input, serde_json::json!({"key": "val"}));
|
||||
assert!(!action.success);
|
||||
assert!(action.output_raw.is_none());
|
||||
assert!(action.output_sanitized.is_none());
|
||||
assert!(action.sanitization_warnings.is_empty());
|
||||
assert!(action.cost.is_none());
|
||||
assert_eq!(action.duration, Duration::ZERO);
|
||||
assert!(action.error.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_action_record_succeed_sets_fields() {
|
||||
let action = ActionRecord::new(0, "tool", serde_json::json!({}));
|
||||
let action = action.succeed(
|
||||
Some("raw output here".into()),
|
||||
serde_json::json!({"clean": true}),
|
||||
Duration::from_secs(7),
|
||||
);
|
||||
|
||||
assert!(action.success);
|
||||
assert_eq!(action.output_raw.as_deref(), Some("raw output here"));
|
||||
assert_eq!(
|
||||
action.output_sanitized,
|
||||
Some(serde_json::json!({"clean": true}))
|
||||
);
|
||||
assert_eq!(action.duration, Duration::from_secs(7));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversation_memory_clear() {
|
||||
let mut mem = ConversationMemory::new(10);
|
||||
mem.add(ChatMessage::user("hello"));
|
||||
mem.add(ChatMessage::assistant("hi"));
|
||||
assert_eq!(mem.len(), 2);
|
||||
assert!(!mem.is_empty());
|
||||
|
||||
mem.clear();
|
||||
assert_eq!(mem.len(), 0);
|
||||
assert!(mem.is_empty());
|
||||
assert!(mem.messages().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversation_memory_last_n() {
|
||||
let mut mem = ConversationMemory::new(10);
|
||||
mem.add(ChatMessage::user("one"));
|
||||
mem.add(ChatMessage::assistant("two"));
|
||||
mem.add(ChatMessage::user("three"));
|
||||
mem.add(ChatMessage::assistant("four"));
|
||||
|
||||
let last_2 = mem.last_n(2);
|
||||
assert_eq!(last_2.len(), 2);
|
||||
assert_eq!(last_2[0].content, "three");
|
||||
assert_eq!(last_2[1].content, "four");
|
||||
|
||||
// Requesting more than available returns all
|
||||
let last_100 = mem.last_n(100);
|
||||
assert_eq!(last_100.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversation_memory_last_n_empty() {
|
||||
let mem = ConversationMemory::new(10);
|
||||
let result = mem.last_n(5);
|
||||
assert!(result.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversation_memory_preserves_system_message_on_trim() {
|
||||
let mut mem = ConversationMemory::new(3);
|
||||
mem.add(ChatMessage::system("You are helpful"));
|
||||
mem.add(ChatMessage::user("msg1"));
|
||||
mem.add(ChatMessage::user("msg2"));
|
||||
|
||||
// At capacity (3). Adding one more should trim, but keep system.
|
||||
mem.add(ChatMessage::user("msg3"));
|
||||
|
||||
assert_eq!(mem.len(), 3);
|
||||
// System message must survive
|
||||
assert_eq!(mem.messages()[0].role, crate::llm::Role::System);
|
||||
assert_eq!(mem.messages()[0].content, "You are helpful");
|
||||
// Oldest non-system message (msg1) should be gone
|
||||
assert_eq!(mem.messages()[1].content, "msg2");
|
||||
assert_eq!(mem.messages()[2].content, "msg3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversation_memory_trims_non_system_first() {
|
||||
let mut mem = ConversationMemory::new(2);
|
||||
mem.add(ChatMessage::system("sys"));
|
||||
mem.add(ChatMessage::user("a"));
|
||||
// Now at capacity. Add another.
|
||||
mem.add(ChatMessage::user("b"));
|
||||
|
||||
assert_eq!(mem.len(), 2);
|
||||
assert_eq!(mem.messages()[0].role, crate::llm::Role::System);
|
||||
assert_eq!(mem.messages()[1].content, "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversation_memory_max_one_with_system_does_not_loop() {
|
||||
// Edge case: max_messages = 1 and only a system message.
|
||||
// Adding another message would try to trim but should not
|
||||
// remove the system message and get stuck.
|
||||
let mut mem = ConversationMemory::new(1);
|
||||
mem.add(ChatMessage::system("sys"));
|
||||
// The system message is already at capacity. Adding another
|
||||
// cannot trim the system message, so we end up with 2 (graceful).
|
||||
// The important thing is we don't infinite-loop.
|
||||
mem.add(ChatMessage::user("hello"));
|
||||
// Should have broken out rather than looping forever.
|
||||
// The system message is protected, so len may exceed max.
|
||||
assert!(mem.len() <= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_failed_actions() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
|
||||
let ok = memory.create_action("good", serde_json::json!({})).succeed(
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
Duration::from_millis(1),
|
||||
);
|
||||
memory.record_action(ok);
|
||||
|
||||
let err = memory
|
||||
.create_action("bad", serde_json::json!({}))
|
||||
.fail("oops", Duration::from_millis(2));
|
||||
memory.record_action(err);
|
||||
|
||||
assert_eq!(memory.successful_actions(), 1);
|
||||
assert_eq!(memory.failed_actions(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_last_action() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
assert!(memory.last_action().is_none());
|
||||
|
||||
let a1 = memory
|
||||
.create_action("first", serde_json::json!({}))
|
||||
.succeed(None, serde_json::json!({}), Duration::ZERO);
|
||||
memory.record_action(a1);
|
||||
|
||||
let a2 = memory
|
||||
.create_action("second", serde_json::json!({}))
|
||||
.fail("nope", Duration::ZERO);
|
||||
memory.record_action(a2);
|
||||
|
||||
let last = memory.last_action().unwrap();
|
||||
assert_eq!(last.tool_name, "second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_actions_by_tool() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
|
||||
for _ in 0..3 {
|
||||
let a = memory
|
||||
.create_action("shell", serde_json::json!({}))
|
||||
.succeed(None, serde_json::json!({}), Duration::ZERO);
|
||||
memory.record_action(a);
|
||||
}
|
||||
let a = memory.create_action("http", serde_json::json!({})).succeed(
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
Duration::ZERO,
|
||||
);
|
||||
memory.record_action(a);
|
||||
|
||||
assert_eq!(memory.actions_by_tool("shell").len(), 3);
|
||||
assert_eq!(memory.actions_by_tool("http").len(), 1);
|
||||
assert_eq!(memory.actions_by_tool("nonexistent").len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_create_action_increments_sequence() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
|
||||
let a0 = memory.create_action("t", serde_json::json!({}));
|
||||
assert_eq!(a0.sequence, 0);
|
||||
|
||||
let a1 = memory.create_action("t", serde_json::json!({}));
|
||||
assert_eq!(a1.sequence, 1);
|
||||
|
||||
let a2 = memory.create_action("t", serde_json::json!({}));
|
||||
assert_eq!(a2.sequence, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_add_message_delegates_to_conversation() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
assert!(memory.conversation.is_empty());
|
||||
|
||||
memory.add_message(ChatMessage::user("hello"));
|
||||
memory.add_message(ChatMessage::assistant("hi"));
|
||||
|
||||
assert_eq!(memory.conversation.len(), 2);
|
||||
assert_eq!(memory.conversation.messages()[0].content, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_total_cost_with_no_cost_actions() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
|
||||
// Actions without cost should contribute zero
|
||||
let a = memory
|
||||
.create_action("free_tool", serde_json::json!({}))
|
||||
.succeed(None, serde_json::json!({}), Duration::ZERO);
|
||||
memory.record_action(a);
|
||||
|
||||
assert_eq!(memory.total_cost(), Decimal::ZERO);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_total_duration_mixed() {
|
||||
let mut memory = Memory::new(Uuid::new_v4());
|
||||
|
||||
let a1 = memory.create_action("t1", serde_json::json!({})).succeed(
|
||||
None,
|
||||
serde_json::json!({}),
|
||||
Duration::from_millis(100),
|
||||
);
|
||||
memory.record_action(a1);
|
||||
|
||||
let a2 = memory
|
||||
.create_action("t2", serde_json::json!({}))
|
||||
.fail("err", Duration::from_millis(200));
|
||||
memory.record_action(a2);
|
||||
|
||||
// Both successful and failed actions contribute to total duration
|
||||
assert_eq!(memory.total_duration(), Duration::from_millis(300));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user