vtcode 0.137.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use super::*;
use hashbrown::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
use vtcode_core::persistent_memory::MemoryCleanupStatus;
use vtcode_core::{EditorContextSnapshot, EditorFileContext};

fn sample_memory_status() -> PersistentMemoryStatus {
    PersistentMemoryStatus {
        enabled: true,
        auto_write: true,
        directory: PathBuf::from("/tmp/memory"),
        summary_file: PathBuf::from("/tmp/memory/memory_summary.md"),
        memory_file: PathBuf::from("/tmp/memory/MEMORY.md"),
        preferences_file: PathBuf::from("/tmp/memory/preferences.md"),
        repository_facts_file: PathBuf::from("/tmp/memory/repository-facts.md"),
        notes_dir: PathBuf::from("/tmp/memory/notes"),
        rollout_summaries_dir: PathBuf::from("/tmp/memory/rollout_summaries"),
        summary_exists: true,
        registry_exists: true,
        pending_rollout_summaries: 0,
        cleanup_status: MemoryCleanupStatus {
            needed: false,
            suspicious_facts: 0,
            suspicious_summary_lines: 0,
        },
    }
}

#[test]
fn structured_resume_lines_preserve_tool_context() {
    let mut assistant = uni::Message::assistant("cargo fmt completed successfully.".to_string());
    assistant.reasoning = Some("Need to run formatter before checks.".to_string());
    assistant.tool_calls = Some(vec![uni::ToolCall::function(
        "call_123".to_string(),
        "exec_command".to_string(),
        "{\"cmd\":\"cargo fmt\"}".to_string(),
    )]);

    let tool_output =
        r#"{"output":" Finished dev profile [unoptimized + debuginfo]\n","exit_code":0,"backend":"pipe"}"#;
    let mut tool_response = uni::Message::tool_response("call_123".to_string(), tool_output.to_string());
    tool_response.origin_tool = Some("exec_command".to_string());

    let history = vec![
        uni::Message::user("run cargo fmt".to_string()),
        assistant,
        tool_response,
    ];

    let lines = build_structured_resume_lines(&history, true);

    assert!(
        lines
            .iter()
            .any(|line| { line.style == MessageStyle::User && line.text.contains("run cargo fmt") })
    );
    assert!(!lines.iter().any(|line| line.text == "You:"));
    assert!(!lines.iter().any(|line| line.text == "Assistant:"));
    assert!(lines.iter().any(|line| {
        line.style == MessageStyle::Tool && line.text.contains("Tool exec_command [tool_call_id: call_123]:")
    }));
    // Tool arguments should show a concise summary instead of raw JSON
    assert!(
        lines
            .iter()
            .any(|line| { line.style == MessageStyle::ToolDetail && line.text.contains("command: cargo fmt") })
    );
    // Tool output should show extracted output text, not raw JSON
    assert!(
        lines
            .iter()
            .any(|line| { line.style == MessageStyle::ToolOutput && line.text.contains("Finished dev profile") })
    );
}

#[test]
fn legacy_style_inference_maps_common_prefixes() {
    assert_eq!(infer_legacy_line_style("  [1] You:"), MessageStyle::User);
    assert_eq!(infer_legacy_line_style("  [5] Assistant:"), MessageStyle::Response);
    assert_eq!(infer_legacy_line_style("System: startup"), MessageStyle::Info);
    assert_eq!(infer_legacy_line_style("Tool [tool_call_id: call_1]:"), MessageStyle::ToolOutput);
}

#[test]
fn structured_resume_lines_fallback_to_reasoning_details() {
    let assistant = uni::Message::assistant("done".to_string())
        .with_reasoning_details(Some(vec![serde_json::json!(r#"{"type":"reasoning.text","text":"detail trace"}"#)]));
    let lines = build_structured_resume_lines(&[assistant], true);
    assert!(
        lines
            .iter()
            .any(|line| { line.style == MessageStyle::Reasoning && line.text.contains("detail trace") })
    );
}

#[test]
fn structured_resume_lines_hide_reasoning_when_unsupported() {
    let mut assistant = uni::Message::assistant("done".to_string());
    assistant.reasoning = Some("trace".to_string());
    let lines = build_structured_resume_lines(&[assistant], false);
    assert!(!lines.iter().any(|line| line.style == MessageStyle::Reasoning));
}

#[test]
fn persistent_memory_guide_lines_show_standard_actions() {
    let lines = persistent_memory_guide_lines(&sample_memory_status());
    assert_eq!(lines.len(), 3);
    assert!(lines[0].contains("Memory is enabled"));
    assert!(lines[1].contains("remember"));
    assert!(lines[2].contains("Auto-write is on"));
}

#[test]
fn persistent_memory_guide_lines_call_out_cleanup_when_needed() {
    let mut status = sample_memory_status();
    status.auto_write = false;
    status.cleanup_status.needed = true;

    let lines = persistent_memory_guide_lines(&status);
    assert_eq!(lines.len(), 3);
    assert!(lines[0].contains("one-time cleanup"));
    assert!(lines[2].contains("Auto-write is off"));
}

#[test]
fn persistent_memory_header_badge_reflects_memory_mode() {
    let badge = persistent_memory_header_badge(&sample_memory_status());
    assert_eq!(badge.text, "Memory: On");
    assert_eq!(badge.tone, InlineHeaderStatusTone::Ready);
}

#[test]
fn persistent_memory_header_badge_warns_on_cleanup() {
    let mut status = sample_memory_status();
    status.cleanup_status.needed = true;

    let badge = persistent_memory_header_badge(&status);
    assert_eq!(badge.text, "Memory: Needs cleanup");
    assert_eq!(badge.tone, InlineHeaderStatusTone::Warning);
}

#[test]
fn apply_persistent_memory_header_guide_sets_badge_and_highlight() {
    let mut header_context = InlineHeaderContext::default();

    apply_persistent_memory_header_guide(&mut header_context, &sample_memory_status());

    assert_eq!(header_context.persistent_memory.as_ref().map(|badge| badge.text.as_str()), Some("Memory: On"));
    assert!(header_context.highlights.iter().any(|highlight| highlight.title == "Memory"));
}

#[test]
fn background_local_agent_visibility_hides_stopped_entries() {
    let entry = vtcode_core::subagents::BackgroundSubprocessEntry {
        id: "background-default".to_string(),
        session_id: "session-456".to_string(),
        exec_session_id: String::new(),
        agent_name: "default".to_string(),
        display_label: "default".to_string(),
        description: "Default agent".to_string(),
        source: "builtin".to_string(),
        color: None,
        status: vtcode_core::subagents::BackgroundSubprocessStatus::Stopped,
        desired_enabled: false,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
        started_at: None,
        ended_at: None,
        pid: None,
        summary: None,
        error: None,
        archive_path: None,
        transcript_path: None,
    };

    assert!(visible_background_local_agents(vec![entry]).is_empty());
}

#[test]
fn delegated_local_agent_preview_uses_queue_placeholder() {
    let entry = SubagentStatusEntry {
        id: "thread-1".to_string(),
        session_id: "session-123".to_string(),
        parent_thread_id: "main".to_string(),
        agent_name: "rust-engineer".to_string(),
        display_label: "rust-engineer".to_string(),
        description: "Review Rust changes".to_string(),
        source: "project".to_string(),
        color: None,
        status: vtcode_core::subagents::SubagentStatus::Queued,
        background: false,
        depth: 1,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
        completed_at: None,
        summary: None,
        error: None,
        transcript_path: None,
        nickname: None,
    };

    assert_eq!(
        delegated_local_agent_preview_placeholder(&entry),
        "Agent is queued and has not emitted transcript output yet."
    );
}

#[test]
fn delegated_local_agent_visibility_keeps_failed_entries() {
    let entry = SubagentStatusEntry {
        id: "thread-1".to_string(),
        session_id: "session-123".to_string(),
        parent_thread_id: "main".to_string(),
        agent_name: "rust-engineer".to_string(),
        display_label: "rust-engineer".to_string(),
        description: "Review Rust changes".to_string(),
        source: "project".to_string(),
        color: None,
        status: vtcode_core::subagents::SubagentStatus::Failed,
        background: false,
        depth: 1,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
        completed_at: Some(chrono::Utc::now()),
        summary: None,
        error: Some("subagent failed".to_string()),
        transcript_path: None,
        nickname: None,
    };

    let visible = visible_delegated_local_agents(vec![entry]);
    assert_eq!(visible.len(), 1);
}

#[test]
fn delegated_local_agent_preview_uses_failure_message() {
    let entry = SubagentStatusEntry {
        id: "thread-1".to_string(),
        session_id: "session-123".to_string(),
        parent_thread_id: "main".to_string(),
        agent_name: "rust-engineer".to_string(),
        display_label: "rust-engineer".to_string(),
        description: "Review Rust changes".to_string(),
        source: "project".to_string(),
        color: None,
        status: vtcode_core::subagents::SubagentStatus::Failed,
        background: false,
        depth: 1,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
        completed_at: Some(chrono::Utc::now()),
        summary: None,
        error: Some("subagent failed".to_string()),
        transcript_path: None,
        nickname: None,
    };

    assert_eq!(delegated_local_agent_preview_placeholder(&entry), "subagent failed");
}

#[test]
fn background_local_agent_preview_uses_status_placeholder() {
    let entry = vtcode_core::subagents::BackgroundSubprocessEntry {
        id: "background-default".to_string(),
        session_id: "session-456".to_string(),
        exec_session_id: String::new(),
        agent_name: "default".to_string(),
        display_label: "default".to_string(),
        description: "Default agent".to_string(),
        source: "builtin".to_string(),
        color: None,
        status: vtcode_core::subagents::BackgroundSubprocessStatus::Starting,
        desired_enabled: true,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
        started_at: None,
        ended_at: None,
        pid: None,
        summary: None,
        error: None,
        archive_path: None,
        transcript_path: None,
    };

    assert_eq!(background_local_agent_preview_placeholder(&entry), "Waiting for the subprocess to emit output...");
}

#[test]
fn ide_context_status_label_respects_session_override() {
    let workspace = assert_fs::TempDir::new().expect("workspace");
    let mut context_manager =
        context_manager::ContextManager::new("sys".into(), (), Arc::new(RwLock::new(HashMap::new())), None);
    context_manager.set_workspace_root(workspace.path());

    let snapshot = EditorContextSnapshot {
        workspace_root: Some(PathBuf::from(workspace.path())),
        active_file: Some(EditorFileContext {
            path: workspace.path().join("src/main.rs").display().to_string(),
            language_id: Some("rust".to_string()),
            line_range: None,
            dirty: false,
            truncated: false,
            selection: None,
        }),
        ..EditorContextSnapshot::default()
    };
    context_manager
        .set_editor_context_snapshot(Some(snapshot.clone()), Some(&vtcode_config::IdeContextConfig::default()));

    assert_eq!(
        ide_context_status_label(&context_manager, workspace.path(), None, Some(&snapshot), None).as_deref(),
        Some("IDE Context (IDE): src/main.rs")
    );

    assert!(!context_manager.toggle_session_ide_context());
    assert_eq!(ide_context_status_label(&context_manager, workspace.path(), None, Some(&snapshot), None), None);
}