vtcode 0.106.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use super::*;

#[test]
fn preflight_fallback_normalizes_unified_search_args() {
    let error =
        anyhow!("Invalid arguments for tool 'unified_search': \"action\" is a required property");
    let args = json!({
        "Pattern": "LLMStreamEvent::",
        "Path": "."
    });
    let fallback = preflight_validation_fallback(tool_names::UNIFIED_SEARCH, &args, &error)
        .expect("fallback expected for recoverable unified_search preflight");
    assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH);
    assert_eq!(fallback.1["action"], "grep");
    assert_eq!(fallback.1["pattern"], "LLMStreamEvent::");
}

#[test]
fn preflight_fallback_maps_keyword_to_pattern_for_grep() {
    let error = anyhow!("Invalid arguments for tool 'unified_search': missing field `pattern`");
    let args = json!({
        "action": "grep",
        "keyword": "system prompt",
        "path": "src"
    });
    let fallback = preflight_validation_fallback(tool_names::UNIFIED_SEARCH, &args, &error)
        .expect("fallback expected for grep missing pattern");
    assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH);
    assert_eq!(fallback.1["action"], "grep");
    assert_eq!(fallback.1["pattern"], "system prompt");
}

#[test]
fn preflight_fallback_remaps_unified_search_read_action() {
    let error = anyhow!("Tool execution failed: Invalid action: read");
    let args = json!({
        "action": "read",
        "query": "retry",
        "path": "src"
    });
    let fallback = preflight_validation_fallback(tool_names::UNIFIED_SEARCH, &args, &error)
        .expect("fallback expected for invalid read action");
    assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH);
    assert_eq!(fallback.1["action"], "grep");
    assert_eq!(fallback.1["pattern"], "retry");
}

#[test]
fn recovery_fallback_skips_list_degradation_for_search_refinement() {
    let grep = recovery_fallback_for_tool(
        tool_names::UNIFIED_SEARCH,
        &json!({"action":"grep","path":"src","pattern":"Result<"}),
    );
    let structural = recovery_fallback_for_tool(
        tool_names::UNIFIED_SEARCH,
        &json!({"action":"structural","path":"src","pattern":"fn $NAME() {}","lang":"rust"}),
    );

    assert!(grep.is_none());
    assert!(structural.is_none());
}

#[test]
fn recovery_fallback_preserves_list_for_file_discovery_calls() {
    let fallback = recovery_fallback_for_tool(
        tool_names::UNIFIED_SEARCH,
        &json!({"action":"list","path":"src","mode":"tree"}),
    )
    .expect("list fallback expected");

    assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH);
    assert_eq!(fallback.1["action"], "list");
    assert_eq!(fallback.1["path"], "src");
    assert_eq!(fallback.1["mode"], "tree");
}

#[test]
fn preflight_fallback_remaps_unified_file_command_payload_to_unified_exec() {
    let error = anyhow!("Missing action in unified_file");
    let args = json!({
        "command": "git status --short",
        "cwd": "."
    });
    let fallback = preflight_validation_fallback(tool_names::UNIFIED_FILE, &args, &error)
        .expect("fallback expected for unified_file command payload");
    assert_eq!(fallback.0, tool_names::UNIFIED_EXEC);
    assert_eq!(fallback.1["action"], "run");
    assert_eq!(fallback.1["command"], "git status --short");
    assert_eq!(fallback.1["cwd"], ".");
}

#[test]
fn preflight_fallback_remaps_unified_file_list_to_unified_search_list() {
    let error = anyhow!("Invalid arguments for tool 'unified_file': unknown variant `list`");
    let args = json!({
        "action": "list",
        "path": "src"
    });
    let fallback = preflight_validation_fallback(tool_names::UNIFIED_FILE, &args, &error)
        .expect("fallback expected for unified_file list payload");
    assert_eq!(fallback.0, tool_names::UNIFIED_SEARCH);
    assert_eq!(fallback.1["action"], "list");
    assert_eq!(fallback.1["path"], "src");
}

#[test]
fn preflight_fallback_normalizes_request_user_input_single_question_shape() {
    let error = anyhow!(
        "Invalid arguments for tool 'request_user_input': \"questions\" is a required property"
    );
    let args = json!({
        "question": "Which direction should we take?",
        "header": "Scope",
        "options": [
            {"label": "Minimal", "description": "Smallest viable change"},
            {"label": "Full", "description": "Broader implementation"}
        ]
    });
    let fallback = preflight_validation_fallback(tool_names::REQUEST_USER_INPUT, &args, &error)
        .expect("fallback expected for request_user_input shorthand");
    assert_eq!(fallback.0, tool_names::REQUEST_USER_INPUT);
    assert_eq!(fallback.1["questions"][0]["id"], "question_1");
    assert_eq!(fallback.1["questions"][0]["header"], "Scope");
    assert_eq!(
        fallback.1["questions"][0]["question"],
        "Which direction should we take?"
    );
    assert_eq!(
        fallback.1["questions"][0]["options"]
            .as_array()
            .map(|v| v.len()),
        Some(2)
    );
}

#[test]
fn preflight_fallback_normalizes_request_user_input_tabs_shape() {
    let error = anyhow!(
        "Invalid arguments for tool 'request_user_input': additional properties are not allowed"
    );
    let args = json!({
        "question": "Which area should we prioritize first?",
        "tabs": [
            {
                "id": "priority",
                "title": "Priority",
                "items": [
                    {"title": "Reliability", "subtitle": "Reduce failure modes"},
                    {"title": "UX", "subtitle": "Improve user flow"}
                ]
            }
        ]
    });
    let fallback = preflight_validation_fallback(tool_names::REQUEST_USER_INPUT, &args, &error)
        .expect("fallback expected for request_user_input tabbed payload");
    assert_eq!(fallback.0, tool_names::REQUEST_USER_INPUT);
    assert_eq!(fallback.1["questions"][0]["id"], "priority");
    assert_eq!(fallback.1["questions"][0]["header"], "Priority");
    assert_eq!(
        fallback.1["questions"][0]["question"],
        "Which area should we prioritize first?"
    );
    assert_eq!(
        fallback.1["questions"][0]["options"]
            .as_array()
            .map(|v| v.len()),
        Some(2)
    );
}

#[test]
fn validation_error_payload_includes_fallback_metadata() {
    let payload = build_validation_error_content_with_fallback(
        "Tool preflight validation failed: x".to_string(),
        "preflight",
        Some(tool_names::UNIFIED_SEARCH.to_string()),
        Some(json!({"action":"grep","pattern":"foo","path":"."})),
    );
    let parsed: serde_json::Value =
        serde_json::from_str(&payload).expect("validation payload should be json");
    assert_eq!(parsed["error_class"], "invalid_arguments");
    assert_eq!(parsed["is_recoverable"], true);
    assert_eq!(parsed["fallback_tool"], tool_names::UNIFIED_SEARCH);
    assert_eq!(parsed["fallback_tool_args"]["action"], "grep");
    assert_eq!(
        parsed.get("next_action"),
        Some(&json!("Retry with fallback_tool_args."))
    );
    assert!(parsed.get("loop_detected").is_none());
}

#[test]
fn validation_error_payload_marks_loop_detection_without_prose_hint() {
    let payload = build_validation_error_content_with_fallback(
        "Tool 'read_file' is blocked due to excessive repetition (Loop Detected).".to_string(),
        "loop_detection",
        Some(tool_names::UNIFIED_SEARCH.to_string()),
        Some(json!({"action":"list","path":"."})),
    );
    let parsed: serde_json::Value =
        serde_json::from_str(&payload).expect("validation payload should be json");
    assert_eq!(parsed.get("loop_detected"), Some(&json!(true)));
    assert_eq!(parsed["fallback_tool"], tool_names::UNIFIED_SEARCH);
    assert_eq!(parsed["fallback_tool_args"]["action"], "list");
    assert_eq!(
        parsed.get("next_action"),
        Some(&json!("Retry with fallback_tool_args."))
    );
}

#[test]
fn reused_read_only_result_uses_canonical_guidance() {
    let mut payload = json!({
        "output": "preview",
        "content": "preview",
        "stdout": "preview",
        "stderr": "preview",
        "stderr_preview": "preview"
    });

    apply_reused_read_only_loop_metadata(
        payload
            .as_object_mut()
            .expect("payload should be an object for reuse metadata"),
    );

    assert_eq!(payload.get("reused_recent_result"), Some(&json!(true)));
    assert_eq!(payload.get("result_ref_only"), Some(&json!(true)));
    assert_eq!(payload.get("loop_detected"), Some(&json!(true)));
    assert_eq!(
        payload.get("loop_detected_note"),
        Some(&json!(
            "Loop detected on repeated read-only call; reusing recent output. Use unified_search (action='grep') or summarize before another read."
        ))
    );
    assert_eq!(
        payload.get("next_action"),
        Some(&json!(
            "Use unified_search (action='grep') or retry unified_file with a narrower offset/limit before reading again."
        ))
    );
    assert!(payload.get("output").is_none());
    assert!(payload.get("content").is_none());
    assert!(payload.get("stdout").is_none());
    assert!(payload.get("stderr").is_none());
    assert!(payload.get("stderr_preview").is_none());
}