mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-01 17:19:24 +00:00
test: add 26 tests for multi-thread safety, db CRUD, concurrency, errors (#442)
* fix: use std::sync::RwLock in MessageTool to avoid runtime panic The `requires_approval` method is synchronous but was using `tokio::sync::RwLock` with `.await` which requires blocking the runtime. This caused a panic: "Cannot block the current thread from within a runtime" Changes: - Replace `tokio::sync::RwLock` with `std::sync::RwLock` for `default_channel` and `default_target` fields - Use `unwrap_or_else(|e| e.into_inner())` to gracefully handle poisoned locks (recovers instead of panicking) - Update all usages from `.read().await` to `.read().unwrap_or_else()` The locks are short-held (just cloning strings), making std::sync::RwLock appropriate for sync methods called from async contexts. Fixes: "Cannot block the current thread from within a runtime" panic when the LLM tries to send a message via the message tool. Co-Authored-By: Claude Opus 4.6 <[email protected]> * test: comprehensive testing improvements and fix MessageTool blocking_read panic Fix tokio::sync::RwLock::blocking_read() panic in MessageTool::requires_approval() under multi-threaded tokio runtimes by switching to std::sync::RwLock with poison recovery. Add 26 new tests across 4 tiers: Tier 1 - Multi-thread runtime safety: - Fix MessageTool to use std::sync::RwLock instead of tokio::sync::RwLock - 4 multi-thread tests for MessageTool::requires_approval() scenarios - 1 multi-thread test for HttpTool credential-dependent approval - 1 structural test exercising all core tool sync trait methods under multi-thread runtime Tier 2 - Database CRUD coverage: - Settings lifecycle (CRUD, bulk ops) - Tool failure tracking (record, broken list, repair) - Routine lifecycle (create, get, list, update, delete, runs) - LLM call recording - Sandbox job lifecycle (create, get, update, list, mode) - Job events (save, list, limit) - Estimation snapshot round-trip Tier 3 - Concurrency: - ToolRegistry concurrent register + read under 4-worker runtime Tier 4 - Error coverage: - Display tests for all 8 error variants - From conversion tests for top-level Error enum Supersedes the fix in PR #411 with the same bug fix plus comprehensive test coverage. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: remove trailing whitespace in registry.rs Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Jerome Revillard <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Illia Polosukhin <[email protected]>
This commit is contained in:
co-authored by
Jerome Revillard
Claude Opus 4.6
Illia Polosukhin
parent
04c5c3fe9f
commit
06c84a5c77
+144
@@ -422,3 +422,147 @@ pub enum RoutineError {
|
||||
|
||||
/// Result type alias for the agent.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn config_error_display() {
|
||||
let err = ConfigError::MissingEnvVar("DATABASE_URL".to_string());
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains("DATABASE_URL"),
|
||||
"Should mention the variable name: {msg}"
|
||||
);
|
||||
|
||||
let err = ConfigError::MissingRequired {
|
||||
key: "llm.model".to_string(),
|
||||
hint: "Set LLM_MODEL env var".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("llm.model"), "Should mention the key: {msg}");
|
||||
assert!(
|
||||
msg.contains("Set LLM_MODEL"),
|
||||
"Should include the hint: {msg}"
|
||||
);
|
||||
|
||||
let err = ConfigError::InvalidValue {
|
||||
key: "port".to_string(),
|
||||
message: "must be a number".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("port"), "Should mention the key: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn database_error_display() {
|
||||
let err = DatabaseError::NotFound {
|
||||
entity: "conversation".to_string(),
|
||||
id: "abc-123".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("conversation"), "Should mention entity: {msg}");
|
||||
assert!(msg.contains("abc-123"), "Should mention id: {msg}");
|
||||
|
||||
let err = DatabaseError::Query("syntax error near SELECT".to_string());
|
||||
assert!(err.to_string().contains("syntax error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_error_display() {
|
||||
let err = ChannelError::StartupFailed {
|
||||
name: "telegram".to_string(),
|
||||
reason: "invalid token".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("telegram"), "Should mention channel: {msg}");
|
||||
assert!(
|
||||
msg.contains("invalid token"),
|
||||
"Should mention reason: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_error_display() {
|
||||
let err = LlmError::ContextLengthExceeded {
|
||||
used: 100_000,
|
||||
limit: 50_000,
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("100000"), "Should mention used tokens: {msg}");
|
||||
assert!(msg.contains("50000"), "Should mention limit: {msg}");
|
||||
|
||||
let err = LlmError::RateLimited {
|
||||
provider: "openai".to_string(),
|
||||
retry_after: Some(Duration::from_secs(30)),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("openai"), "Should mention provider: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_error_display() {
|
||||
let err = JobError::MaxJobsExceeded { max: 5 };
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("5"), "Should mention max: {msg}");
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
let err = JobError::NotFound { id };
|
||||
let msg = err.to_string();
|
||||
assert!(
|
||||
msg.contains(&id.to_string()),
|
||||
"Should mention job id: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn safety_error_display() {
|
||||
let err = SafetyError::InjectionDetected {
|
||||
pattern: "SYSTEM:".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("SYSTEM:"), "Should mention pattern: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_error_display() {
|
||||
let err = WorkspaceError::DocumentNotFound {
|
||||
doc_type: "notes".to_string(),
|
||||
user_id: "user1".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("notes"), "Should mention doc_type: {msg}");
|
||||
assert!(msg.contains("user1"), "Should mention user_id: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routine_error_display() {
|
||||
let err = RoutineError::InvalidCron {
|
||||
reason: "bad format".to_string(),
|
||||
};
|
||||
let msg = err.to_string();
|
||||
assert!(msg.contains("bad format"), "Should mention reason: {msg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_error_from_conversions() {
|
||||
let config_err = ConfigError::MissingEnvVar("TEST".to_string());
|
||||
let err: Error = config_err.into();
|
||||
assert!(matches!(err, Error::Config(_)));
|
||||
|
||||
let db_err = DatabaseError::Query("test".to_string());
|
||||
let err: Error = db_err.into();
|
||||
assert!(matches!(err, Error::Database(_)));
|
||||
|
||||
let job_err = JobError::MaxJobsExceeded { max: 1 };
|
||||
let err: Error = job_err.into();
|
||||
assert!(matches!(err, Error::Job(_)));
|
||||
|
||||
let safety_err = SafetyError::ValidationFailed {
|
||||
reason: "test".to_string(),
|
||||
};
|
||||
let err: Error = safety_err.into();
|
||||
assert!(matches!(err, Error::Safety(_)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user