diff --git a/src/openhuman/agent/tests.rs b/src/openhuman/agent/tests.rs index 4916fae37..08c472594 100644 --- a/src/openhuman/agent/tests.rs +++ b/src/openhuman/agent/tests.rs @@ -249,12 +249,17 @@ impl Tool for CountingTool { } } -fn make_memory() -> Arc { +/// 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, 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, tempfile::TempDir) { @@ -267,19 +272,23 @@ fn make_sqlite_memory() -> (Arc, 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, tools: Vec>, dispatcher: Box, -) -> 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>, mem: Arc, 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, tools: Vec>, 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!(