mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 23:14:37 +00:00
fix(tests): isolate agent tests with per-test temp directories
All 26 agent tests shared std::env::temp_dir() as workspace, causing them to contend on the same SQLite database file when running in parallel. This caused flaky "database is locked" failures in CI (e.g. clear_history_resets_conversation). Fix: each test helper now creates its own tempfile::TempDir, returning it alongside the Agent so it stays alive for the test duration. Verified: 36/36 pass across 5 consecutive runs with zero flakiness.
This commit is contained in:
@@ -249,12 +249,17 @@ impl Tool for CountingTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn make_memory() -> Arc<dyn Memory> {
|
||||
/// Create an isolated memory instance with its own temp directory.
|
||||
/// The returned `TempDir` must be held alive for the duration of the test
|
||||
/// to prevent the directory (and its SQLite database) from being deleted.
|
||||
fn make_memory() -> (Arc<dyn Memory>, tempfile::TempDir) {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let cfg = MemoryConfig {
|
||||
backend: "none".into(),
|
||||
..MemoryConfig::default()
|
||||
};
|
||||
Arc::from(memory::create_memory(&cfg, &std::env::temp_dir(), None).unwrap())
|
||||
let mem = Arc::from(memory::create_memory(&cfg, tmp.path(), None).unwrap());
|
||||
(mem, tmp)
|
||||
}
|
||||
|
||||
fn make_sqlite_memory() -> (Arc<dyn Memory>, tempfile::TempDir) {
|
||||
@@ -267,19 +272,23 @@ fn make_sqlite_memory() -> (Arc<dyn Memory>, tempfile::TempDir) {
|
||||
(mem, tmp)
|
||||
}
|
||||
|
||||
/// Build an agent with an isolated temp workspace.
|
||||
/// Returns `(Agent, TempDir)` — hold `_tmp` in the test to keep the dir alive.
|
||||
fn build_agent_with(
|
||||
provider: Box<dyn Provider>,
|
||||
tools: Vec<Box<dyn Tool>>,
|
||||
dispatcher: Box<dyn ToolDispatcher>,
|
||||
) -> Agent {
|
||||
Agent::builder()
|
||||
) -> (Agent, tempfile::TempDir) {
|
||||
let (mem, tmp) = make_memory();
|
||||
let agent = Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(make_memory())
|
||||
.memory(mem)
|
||||
.tool_dispatcher(dispatcher)
|
||||
.workspace_dir(std::env::temp_dir())
|
||||
.workspace_dir(tmp.path().to_path_buf())
|
||||
.build()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
(agent, tmp)
|
||||
}
|
||||
|
||||
fn build_agent_with_memory(
|
||||
@@ -287,32 +296,36 @@ fn build_agent_with_memory(
|
||||
tools: Vec<Box<dyn Tool>>,
|
||||
mem: Arc<dyn Memory>,
|
||||
auto_save: bool,
|
||||
) -> Agent {
|
||||
Agent::builder()
|
||||
) -> (Agent, tempfile::TempDir) {
|
||||
let tmp = tempfile::TempDir::new().unwrap();
|
||||
let agent = Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(mem)
|
||||
.tool_dispatcher(Box::new(NativeToolDispatcher))
|
||||
.workspace_dir(std::env::temp_dir())
|
||||
.workspace_dir(tmp.path().to_path_buf())
|
||||
.auto_save(auto_save)
|
||||
.build()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
(agent, tmp)
|
||||
}
|
||||
|
||||
fn build_agent_with_config(
|
||||
provider: Box<dyn Provider>,
|
||||
tools: Vec<Box<dyn Tool>>,
|
||||
config: AgentConfig,
|
||||
) -> Agent {
|
||||
Agent::builder()
|
||||
) -> (Agent, tempfile::TempDir) {
|
||||
let (mem, tmp) = make_memory();
|
||||
let agent = Agent::builder()
|
||||
.provider(provider)
|
||||
.tools(tools)
|
||||
.memory(make_memory())
|
||||
.memory(mem)
|
||||
.tool_dispatcher(Box::new(NativeToolDispatcher))
|
||||
.workspace_dir(std::env::temp_dir())
|
||||
.workspace_dir(tmp.path().to_path_buf())
|
||||
.config(config)
|
||||
.build()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
(agent, tmp)
|
||||
}
|
||||
|
||||
/// Helper: create a ChatResponse with tool calls (native format).
|
||||
@@ -348,7 +361,7 @@ fn xml_tool_response(name: &str, args: &str) -> ChatResponse {
|
||||
#[tokio::test]
|
||||
async fn turn_returns_text_when_no_tools_called() {
|
||||
let provider = Box::new(ScriptedProvider::new(vec![text_response("Hello world")]));
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -376,7 +389,7 @@ async fn turn_executes_single_tool_then_returns() {
|
||||
text_response("I ran the tool"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -416,7 +429,7 @@ async fn turn_handles_multi_step_tool_chain() {
|
||||
text_response("Done after 3 calls"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(counting_tool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -454,7 +467,7 @@ async fn turn_bails_out_at_max_iterations() {
|
||||
..AgentConfig::default()
|
||||
};
|
||||
|
||||
let mut agent = build_agent_with_config(provider, vec![Box::new(EchoTool)], config);
|
||||
let (mut agent, _tmp) = build_agent_with_config(provider, vec![Box::new(EchoTool)], config);
|
||||
|
||||
let result = agent.turn("infinite loop").await;
|
||||
assert!(result.is_err());
|
||||
@@ -480,7 +493,7 @@ async fn turn_handles_unknown_tool_gracefully() {
|
||||
text_response("I couldn't find that tool"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -520,7 +533,7 @@ async fn turn_recovers_from_tool_failure() {
|
||||
text_response("Tool failed but I recovered"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(FailingTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -544,7 +557,7 @@ async fn turn_recovers_from_tool_error() {
|
||||
text_response("I recovered from the error"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(PanickingTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -563,7 +576,7 @@ async fn turn_recovers_from_tool_error() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn turn_propagates_provider_error() {
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
Box::new(FailingProvider),
|
||||
vec![],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -591,7 +604,7 @@ async fn history_trims_after_max_messages() {
|
||||
..AgentConfig::default()
|
||||
};
|
||||
|
||||
let mut agent = build_agent_with_config(provider, vec![], config);
|
||||
let (mut agent, _tmp) = build_agent_with_config(provider, vec![], config);
|
||||
|
||||
for i in 0..max_history + 5 {
|
||||
let _ = agent.turn(&format!("msg {i}")).await.unwrap();
|
||||
@@ -622,7 +635,7 @@ async fn auto_save_stores_messages_in_memory() {
|
||||
"I remember everything",
|
||||
)]));
|
||||
|
||||
let mut agent = build_agent_with_memory(
|
||||
let (mut agent, _tmp2) = build_agent_with_memory(
|
||||
provider,
|
||||
vec![],
|
||||
mem.clone(),
|
||||
@@ -644,7 +657,7 @@ async fn auto_save_disabled_does_not_store() {
|
||||
let (mem, _tmp) = make_sqlite_memory();
|
||||
let provider = Box::new(ScriptedProvider::new(vec![text_response("hello")]));
|
||||
|
||||
let mut agent = build_agent_with_memory(
|
||||
let (mut agent, _tmp2) = build_agent_with_memory(
|
||||
provider,
|
||||
vec![],
|
||||
mem.clone(),
|
||||
@@ -668,7 +681,7 @@ async fn xml_dispatcher_parses_and_loops() {
|
||||
text_response("XML tool completed"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(XmlToolDispatcher),
|
||||
@@ -684,7 +697,7 @@ async fn xml_dispatcher_parses_and_loops() {
|
||||
#[tokio::test]
|
||||
async fn native_dispatcher_sends_tool_specs() {
|
||||
let provider = Box::new(ScriptedProvider::new(vec![text_response("ok")]));
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -714,7 +727,7 @@ async fn turn_handles_empty_text_response() {
|
||||
tool_calls: vec![],
|
||||
}]));
|
||||
|
||||
let mut agent = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
let (mut agent, _tmp) = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
|
||||
let response = agent.turn("hi").await.unwrap();
|
||||
assert!(response.is_empty());
|
||||
@@ -727,7 +740,7 @@ async fn turn_handles_none_text_response() {
|
||||
tool_calls: vec![],
|
||||
}]));
|
||||
|
||||
let mut agent = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
let (mut agent, _tmp) = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
|
||||
// Should not panic — falls back to empty string
|
||||
let response = agent.turn("hi").await.unwrap();
|
||||
@@ -752,7 +765,7 @@ async fn turn_preserves_text_alongside_tool_calls() {
|
||||
text_response("Here are the results"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -801,7 +814,7 @@ async fn turn_handles_multiple_tools_in_one_response() {
|
||||
text_response("All 3 done"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(counting_tool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -826,7 +839,7 @@ async fn turn_handles_multiple_tools_in_one_response() {
|
||||
#[tokio::test]
|
||||
async fn system_prompt_injected_on_first_turn() {
|
||||
let provider = Box::new(ScriptedProvider::new(vec![text_response("ok")]));
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -850,7 +863,7 @@ async fn system_prompt_not_duplicated_on_second_turn() {
|
||||
text_response("first"),
|
||||
text_response("second"),
|
||||
]));
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -882,7 +895,7 @@ async fn history_contains_all_expected_entries_after_tool_loop() {
|
||||
text_response("final answer"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(
|
||||
let (mut agent, _tmp) = build_agent_with(
|
||||
provider,
|
||||
vec![Box::new(EchoTool)],
|
||||
Box::new(NativeToolDispatcher),
|
||||
@@ -921,11 +934,12 @@ async fn history_contains_all_expected_entries_after_tool_loop() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn builder_fails_without_provider() {
|
||||
let (mem, _tmp) = make_memory();
|
||||
let result = Agent::builder()
|
||||
.tools(vec![])
|
||||
.memory(make_memory())
|
||||
.memory(mem)
|
||||
.tool_dispatcher(Box::new(NativeToolDispatcher))
|
||||
.workspace_dir(std::path::PathBuf::from("/tmp"))
|
||||
.workspace_dir(_tmp.path().to_path_buf())
|
||||
.build();
|
||||
|
||||
assert!(result.is_err(), "Building without provider should fail");
|
||||
@@ -943,7 +957,7 @@ async fn multi_turn_maintains_growing_history() {
|
||||
text_response("response 3"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
let (mut agent, _tmp) = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
|
||||
let r1 = agent.turn("msg 1").await.unwrap();
|
||||
let len_after_1 = agent.history().len();
|
||||
@@ -1261,7 +1275,7 @@ async fn clear_history_resets_conversation() {
|
||||
text_response("second"),
|
||||
]));
|
||||
|
||||
let mut agent = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
let (mut agent, _tmp) = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
|
||||
let _ = agent.turn("hi").await.unwrap();
|
||||
assert!(!agent.history().is_empty());
|
||||
@@ -1284,7 +1298,7 @@ async fn clear_history_resets_conversation() {
|
||||
#[tokio::test]
|
||||
async fn run_single_delegates_to_turn() {
|
||||
let provider = Box::new(ScriptedProvider::new(vec![text_response("via run_single")]));
|
||||
let mut agent = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
let (mut agent, _tmp) = build_agent_with(provider, vec![], Box::new(NativeToolDispatcher));
|
||||
|
||||
let response = agent.run_single("test").await.unwrap();
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user