use std::path::PathBuf;
use std::time::{Duration, Instant};
use mermaid_cli::domain::{ToolCallId, TurnId};
use mermaid_cli::providers::ctx::test_exec_context;
use mermaid_cli::providers::tool::ToolExecutor;
use mermaid_cli::providers::tool::exec::ExecuteCommandTool;
#[tokio::test]
async fn execute_command_cancels_within_100ms() {
let (ctx, _rx) = test_exec_context(TurnId(1), ToolCallId(1), PathBuf::from("/tmp"));
let token = ctx.token.clone();
let handle = tokio::spawn(async move {
ExecuteCommandTool
.execute(serde_json::json!({"command": "sleep 60"}), ctx)
.await
});
tokio::time::sleep(Duration::from_millis(30)).await;
let cancel_at = Instant::now();
token.cancel();
let outcome = tokio::time::timeout(Duration::from_millis(500), handle)
.await
.expect("test timed out — cancellation didn't propagate")
.expect("join");
let elapsed = cancel_at.elapsed();
assert!(outcome.was_cancelled());
assert!(
elapsed < Duration::from_millis(300),
"cancellation took {:?} — v0.6 regression?",
elapsed
);
}
#[tokio::test]
async fn execute_command_timeout_honored() {
let (ctx, _rx) = test_exec_context(TurnId(2), ToolCallId(1), PathBuf::from("/tmp"));
let start = Instant::now();
let outcome = ExecuteCommandTool
.execute(
serde_json::json!({"command": "sleep 10", "timeout": 1}),
ctx,
)
.await;
let elapsed = start.elapsed();
assert_eq!(outcome.status, mermaid_cli::domain::ToolStatus::Error);
let output = outcome.as_tool_message_content();
assert!(output.contains("timed out"), "got: {}", output);
assert!(output.contains("was killed"), "got: {}", output);
assert!(
elapsed >= Duration::from_millis(900) && elapsed < Duration::from_millis(1500),
"timeout duration off: {:?}",
elapsed
);
}
#[tokio::test]
async fn cancelling_empty_scope_is_safe() {
use mermaid_cli::effect::TurnScope;
let scope = TurnScope::new(TurnId(1));
drop(scope);
}
#[tokio::test]
async fn effect_runner_cancels_scope_on_command() {
use mermaid_cli::domain::{Cmd, Msg};
use mermaid_cli::effect::EffectRunner;
let (mut runner, _rx) = EffectRunner::pair(PathBuf::from("/tmp"));
let request = mermaid_cli::domain::ChatRequest {
model_id: "test/m".to_string(),
messages: vec![],
system_prompt: String::new(),
instructions: None,
reasoning: mermaid_cli::models::ReasoningLevel::Medium,
temperature: 0.7,
max_tokens: 4096,
tools: vec![],
};
runner.dispatch(Cmd::CallModel {
turn: TurnId(1),
request,
});
assert_eq!(runner.scope_count(), 1);
runner.dispatch(Cmd::CancelScope(TurnId(1)));
assert_eq!(
runner.scope_count(),
0,
"CancelScope must drop the scope entry"
);
let _ = &_rx as &dyn std::any::Any;
let _ = Msg::Tick; }
#[tokio::test]
async fn effect_runner_shutdown_bounded_time() {
use mermaid_cli::domain::Cmd;
use mermaid_cli::effect::EffectRunner;
let (mut runner, _rx) = EffectRunner::pair(PathBuf::from("/tmp"));
for _ in 0..10 {
runner.dispatch(Cmd::DismissStatusAfter { ms: 10 });
}
let start = Instant::now();
runner.shutdown().await;
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_millis(500),
"shutdown took {:?} — bounded drain broken?",
elapsed
);
}