lingshu-tools 0.10.0

Tool registry, ToolHandler trait, and 50+ tool implementations
use lingshu_tools::{ACP_TOOLS, AppConfigRef, CORE_TOOLS, LSP_TOOLS, ToolContext, ToolRegistry};
use lingshu_types::{Platform, ToolError};
use serde_json::json;
use tokio_util::sync::CancellationToken;

fn assert_tool_in_acp_surface(tool_name: &str) {
    assert!(
        ACP_TOOLS.contains(&tool_name),
        "ACP_TOOLS should expose {tool_name}"
    );
}

const CLAUDE_CODE_LSP_BASELINE: &[&str] = &[
    "lsp_goto_definition",
    "lsp_find_references",
    "lsp_hover",
    "lsp_document_symbols",
    "lsp_workspace_symbols",
    "lsp_goto_implementation",
    "lsp_call_hierarchy_prepare",
    "lsp_incoming_calls",
    "lsp_outgoing_calls",
];

const EDGECRAB_LSP_ADVANTAGE: &[&str] = &[
    "lsp_code_actions",
    "lsp_apply_code_action",
    "lsp_rename",
    "lsp_format_document",
    "lsp_format_range",
    "lsp_inlay_hints",
    "lsp_semantic_tokens",
    "lsp_signature_help",
    "lsp_type_hierarchy_prepare",
    "lsp_supertypes",
    "lsp_subtypes",
    "lsp_diagnostics_pull",
    "lsp_linked_editing_range",
    "lsp_enrich_diagnostics",
    "lsp_select_and_apply_action",
    "lsp_workspace_type_errors",
];

#[test]
fn browser_advantage_tools_are_exposed_in_core_and_acp_surfaces() {
    for tool_name in ["browser_wait_for", "browser_select", "browser_hover"] {
        assert!(
            CORE_TOOLS.contains(&tool_name),
            "CORE_TOOLS should expose {tool_name}"
        );
        assert_tool_in_acp_surface(tool_name);
    }
}

#[test]
fn moa_tool_is_in_dedicated_constant_and_acp() {
    // MOA moved to on-demand MOA_TOOLS, but still in ACP (editor context)
    assert!(
        !CORE_TOOLS.contains(&"moa"),
        "MOA should not be in CORE_TOOLS"
    );
    assert_tool_in_acp_surface("moa");
}

#[test]
fn claude_code_lsp_parity_tools_are_in_lsp_tools_and_acp() {
    for tool_name in CLAUDE_CODE_LSP_BASELINE {
        assert!(
            LSP_TOOLS.contains(tool_name),
            "LSP_TOOLS should expose {tool_name}"
        );
        assert_tool_in_acp_surface(tool_name);
    }
}

#[test]
fn lingshu_lsp_advantage_tools_are_in_lsp_tools_and_acp() {
    for tool_name in EDGECRAB_LSP_ADVANTAGE {
        assert!(
            LSP_TOOLS.contains(tool_name),
            "LSP_TOOLS should expose {tool_name}"
        );
        assert_tool_in_acp_surface(tool_name);
    }
}

#[test]
fn lingshu_lsp_surface_exceeds_claude_code_baseline() {
    let lsp_tools_count = LSP_TOOLS.len();
    let acp_lsp_count = ACP_TOOLS
        .iter()
        .filter(|name| name.starts_with("lsp_"))
        .count();

    assert_eq!(
        CLAUDE_CODE_LSP_BASELINE.len(),
        9,
        "baseline list should track Claude Code's 9 documented LSP operations"
    );
    assert!(
        lsp_tools_count > CLAUDE_CODE_LSP_BASELINE.len(),
        "LSP_TOOLS should expose more LSP operations than the 9-operation baseline"
    );
    assert!(
        acp_lsp_count > CLAUDE_CODE_LSP_BASELINE.len(),
        "ACP_TOOLS should expose more LSP operations than the 9-operation baseline"
    );
    assert_eq!(
        lsp_tools_count,
        CLAUDE_CODE_LSP_BASELINE.len() + EDGECRAB_LSP_ADVANTAGE.len(),
        "LSP_TOOLS should expose the full parity-plus LSP surface"
    );
    assert_eq!(
        acp_lsp_count,
        CLAUDE_CODE_LSP_BASELINE.len() + EDGECRAB_LSP_ADVANTAGE.len(),
        "ACP_TOOLS should expose the full parity-plus LSP surface"
    );
}

#[tokio::test]
async fn browser_advantage_tools_dispatch_through_registry_with_edge_case_validation() {
    let registry = ToolRegistry::new();
    let ctx = ToolContext {
        task_id: "test-task".into(),
        cwd: std::env::temp_dir(),
        session_id: "test-session".into(),
        user_task: None,
        cancel: CancellationToken::new(),
        config: AppConfigRef::default(),
        state_db: None,
        platform: Platform::Cli,
        process_table: None,
        provider: None,
        tool_registry: None,
        delegate_depth: 0,
        delegate_agent_id: None,
        delegate_parent_id: None,
        sub_agent_runner: None,
        delegation_event_tx: None,
        clarify_tx: None,
        approval_tx: None,
        on_skills_changed: None,
        gateway_sender: None,
        origin_chat: None,
        session_key: None,
        todo_store: None,
        current_tool_call_id: None,
        current_tool_name: None,
        injected_messages: None,
        tool_progress_tx: None,
        watch_notification_tx: None,
        mutation_turn: None,
        lsp_gate: None,
        kanban_task_id: None,
    };

    let wait_for_err = registry
        .dispatch("browser_wait_for", json!({}), &ctx)
        .await
        .expect_err("browser_wait_for should reject empty conditions");
    match wait_for_err {
        ToolError::InvalidArgs { tool, message } => {
            assert_eq!(tool, "browser_wait_for");
            assert!(message.contains("Provide at least one of"));
        }
        other => panic!("expected InvalidArgs for browser_wait_for, got {other:?}"),
    }

    let select_err = registry
        .dispatch("browser_select", json!({"ref": "@e1"}), &ctx)
        .await
        .expect_err("browser_select should reject missing option");
    match select_err {
        ToolError::InvalidArgs { tool, .. } => assert_eq!(tool, "browser_select"),
        other => panic!("expected InvalidArgs for browser_select, got {other:?}"),
    }

    let hover_err = registry
        .dispatch("browser_hover", json!({}), &ctx)
        .await
        .expect_err("browser_hover should reject missing ref");
    match hover_err {
        ToolError::InvalidArgs { tool, .. } => assert_eq!(tool, "browser_hover"),
        other => panic!("expected InvalidArgs for browser_hover, got {other:?}"),
    }
}