aidaemon 0.9.34

A personal AI agent that runs as a background daemon, accessible via Telegram, Slack, or Discord, with tool use, MCP integration, and persistent memory
use super::*;
use crate::testing::{setup_test_agent, MockProvider};
use crate::types::{ChannelContext, UserRole};
use serde_json::json;

#[test]
fn test_is_resume_request_detects_continue_variants() {
    assert!(is_resume_request("continue"));
    assert!(is_resume_request("Continue with next phase"));
    assert!(is_resume_request("resume the previous task"));
    assert!(is_resume_request("next phase"));
    assert!(!is_resume_request("How do I continue learning Rust?"));
}

#[tokio::test]
async fn test_continue_injects_resume_checkpoint_and_closes_orphan_task() {
    let provider =
        MockProvider::with_responses(vec![MockProvider::text_response("Resumed and done.")]);
    let harness = setup_test_agent(provider).await.unwrap();
    let session_id = "resume_session";
    let orphan_task_id = "task-orphan-1";

    let emitter =
        crate::events::EventEmitter::new(harness.agent.event_store.clone(), session_id.to_string())
            .with_task_id(orphan_task_id.to_string());

    emitter
        .emit(
            EventType::TaskStart,
            TaskStartData {
                task_id: orphan_task_id.to_string(),
                description: "Build website and deploy".to_string(),
                parent_task_id: None,
                user_message: Some("Build website and deploy".to_string()),
            },
        )
        .await
        .unwrap();
    emitter
        .emit(
            EventType::ThinkingStart,
            ThinkingStartData {
                iteration: 2,
                task_id: orphan_task_id.to_string(),
                total_tool_calls: 1,
            },
        )
        .await
        .unwrap();
    emitter
        .emit(
            EventType::AssistantResponse,
            AssistantResponseData {
                message_id: None,
                content: Some("I'll continue by checking the config.".to_string()),
                tool_calls: Some(vec![ToolCallInfo {
                    id: "call_pending".to_string(),
                    name: "system_info".to_string(),
                    arguments: json!({}),
                    extra_content: None,
                }]),
                model: "mock-model".to_string(),
                input_tokens: None,
                output_tokens: None,
                annotations: Vec::new(),
            },
        )
        .await
        .unwrap();
    emitter
        .emit(
            EventType::ToolResult,
            ToolResultData {
                message_id: None,
                tool_call_id: "call_done".to_string(),
                name: "system_info".to_string(),
                result: "ok".to_string(),
                success: true,
                duration_ms: 12,
                error: None,
                task_id: Some(orphan_task_id.to_string()),
                annotations: Vec::new(),
            },
        )
        .await
        .unwrap();
    emitter
        .emit(
            EventType::DecisionPoint,
            DecisionPointData {
                decision_type: DecisionType::ExecutionStateSnapshot,
                task_id: orphan_task_id.to_string(),
                iteration: 2,
                severity: crate::events::DiagnosticSeverity::Info,
                code: Some("step_completed".to_string()),
                metadata: json!({
                    "condition": "step_completed",
                    "execution_state": {
                        "execution_id": "exec-orphan-1",
                        "current_step": {
                            "step_id": "step-2-call_pending",
                            "description": "Run `system_info` against /tmp/demo",
                            "plan_version": 1,
                            "primary_tool": "system_info",
                            "expected_effect": "observation",
                            "target_scope": {
                                "allowed_targets": [{"kind":"path","value":"/tmp/demo"}],
                                "hard_fail_outside_scope": false
                            },
                            "expected_targets": [{"kind":"path","value":"/tmp/demo"}],
                            "retry_policy": {
                                "max_attempts": 1,
                                "allow_tool_invocation_retry": false
                            },
                            "approval_requirement": "not_needed",
                            "idempotency_key": null
                        },
                        "attempt_count": 1,
                        "last_tool_name": "system_info",
                        "last_outcome": "progress",
                        "background_handoff_active": false,
                        "persisted_at": null,
                        "budget_tier": "small",
                        "budget": {
                            "max_steps": 4,
                            "max_tokens": 12000,
                            "max_llm_calls": 8,
                            "max_tool_calls": 5,
                            "max_validation_rounds": 2,
                            "max_wall_clock_ms": 180000
                        },
                        "persistence": "durable",
                        "llm_calls_used": 1,
                        "tool_calls_used": 1,
                        "validation_rounds_used": 0,
                        "steps_used": 1
                    }
                }),
                summary: "Captured execution state snapshot".to_string(),
            },
        )
        .await
        .unwrap();

    let checkpoint = harness
        .agent
        .build_resume_checkpoint(session_id)
        .await
        .unwrap()
        .expect("expected resume checkpoint");
    let execution_snapshot = checkpoint
        .execution_snapshot
        .as_ref()
        .expect("expected execution snapshot on resume checkpoint");
    assert_eq!(execution_snapshot.execution_id, "exec-orphan-1");
    assert_eq!(
        execution_snapshot.current_step_id.as_deref(),
        Some("step-2-call_pending")
    );
    assert_eq!(
        execution_snapshot.current_tool.as_deref(),
        Some("system_info")
    );
    assert_eq!(
        execution_snapshot.current_target.as_deref(),
        Some("/tmp/demo")
    );

    let reply = harness
        .agent
        .handle_message(
            session_id,
            "continue",
            None,
            UserRole::Owner,
            ChannelContext::private("test"),
            None,
        )
        .await
        .unwrap();
    assert_eq!(reply, "Resumed and done.");

    let calls = harness.provider.call_log.lock().await;
    assert!(!calls.is_empty());
    let first_call = &calls[0];
    let system_prompt = first_call
        .messages
        .iter()
        .find_map(|msg| {
            if msg.get("role").and_then(|v| v.as_str()) == Some("system") {
                return msg
                    .get("content")
                    .and_then(|v| v.as_str())
                    .map(|s| s.to_string());
            }
            None
        })
        .expect("expected system prompt");
    assert!(system_prompt.contains("## Resume Checkpoint"));
    assert!(system_prompt.contains(orphan_task_id));

    let orphan_events = harness
        .agent
        .event_store
        .query_task_events_for_session(session_id, orphan_task_id)
        .await
        .unwrap();
    let orphan_end = orphan_events
        .iter()
        .find(|e| e.event_type == EventType::TaskEnd)
        .expect("expected orphan task_end after resume");
    let orphan_end_data = orphan_end.parse_data::<TaskEndData>().unwrap();
    assert_eq!(orphan_end_data.status, TaskStatus::Failed);
    assert!(
        orphan_end_data
            .error
            .unwrap_or_default()
            .contains("Resumed in task"),
        "expected interruption reason to reference resumed task"
    );

    let starts = harness
        .agent
        .event_store
        .query_events_by_types(session_id, &[EventType::TaskStart], 10)
        .await
        .unwrap();
    let resumed_start = starts.into_iter().find_map(|event| {
        let data = event.parse_data::<TaskStartData>().ok()?;
        if data.parent_task_id.as_deref() == Some(orphan_task_id) {
            Some(data)
        } else {
            None
        }
    });
    assert!(
        resumed_start.is_some(),
        "expected resumed task_start to reference orphan as parent"
    );
}