frigg 0.3.1

Local-first MCP server for code understanding.
Documentation
#![allow(clippy::panic)]

use super::*;
use crate::mcp::types::{
    WorkspacePreciseCoverageMode, WorkspacePreciseIngestState, WorkspacePreciseState,
};

#[test]
fn extended_only_tools_are_hidden_by_default_runtime_options() {
    let server = FriggMcpServer::new_with_runtime_options(fixture_config(), false, false);
    let names = to_set(server.runtime_registered_tool_names());

    for tool_name in extended_only_tool_names() {
        assert!(
            !names.contains(&tool_name),
            "extended-only tool should not be registered by default: {tool_name}"
        );
    }
    assert!(
        names.contains("list_repositories"),
        "core tools should remain registered when extended-only tools are disabled"
    );
}

#[test]
fn extended_only_tools_are_registered_when_runtime_option_enabled() {
    let server = FriggMcpServer::new_with_runtime_options(fixture_config(), false, true);
    let names = to_set(server.runtime_registered_tool_names());

    for tool_name in extended_only_tool_names() {
        assert!(
            names.contains(&tool_name),
            "extended-only tool should be registered when enabled: {tool_name}"
        );
    }
}

#[test]
fn server_info_enables_resources_and_prompts() {
    let server = FriggMcpServer::new_with_runtime_options(fixture_config(), false, false);
    let info = <FriggMcpServer as rmcp::ServerHandler>::get_info(&server);

    assert!(info.capabilities.tools.is_some());
    assert!(info.capabilities.resources.is_some());
    assert!(info.capabilities.prompts.is_some());

    let instructions = info
        .instructions
        .expect("server info should publish MCP usage instructions");
    assert!(instructions.contains("call workspace_attach explicitly"));
    assert!(instructions.contains("Use workspace_current for repository health"));
    assert!(instructions.contains("Prefer shell tools for cheap local reads"));
    assert!(instructions.contains("restricted core tool surface"));
    assert!(instructions.contains("Set `FRIGG_MCP_TOOL_SURFACE_PROFILE=extended`"));
}

#[test]
fn server_starts_detached_when_started_without_startup_roots() {
    let workspace_root = temp_workspace_root("declared-roots-attach");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace root fixture");
    fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
        .expect("failed to write workspace root fixture");

    let config = FriggConfig::from_optional_workspace_roots(Vec::new())
        .expect("empty serving config should be valid");
    let server = FriggMcpServer::new_with_runtime_options(config, false, false);
    assert!(server.attached_workspaces().is_empty());
    assert!(server.current_repository_id().is_none());

    let _ = fs::remove_dir_all(workspace_root);
}

#[test]
fn workspace_lexical_summary_stays_ready_when_semantic_config_is_invalid() {
    let workspace_root = temp_workspace_root("workspace-lexical-invalid-semantic-config");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace src directory");
    fs::write(workspace_root.join("src/lib.rs"), "pub struct User;\n")
        .expect("failed to write source fixture");

    let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
        .expect("workspace root must produce valid config");
    config.semantic_runtime.enabled = true;
    let server = FriggMcpServer::new_with_runtime_options(config, false, false);
    let workspace = server
        .runtime_state
        .workspace_registry
        .read()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");

    seed_manifest_snapshot(
        &workspace_root,
        &workspace.runtime_repository_id,
        "snapshot-001",
        &["src/lib.rs"],
    );

    let storage = FriggMcpServer::workspace_storage_summary(&workspace);
    let lexical = server.workspace_lexical_index_summary(&workspace, &storage);
    let semantic = server.workspace_semantic_index_summary(&workspace, &storage);

    assert_eq!(lexical.state, WorkspaceIndexComponentState::Ready);
    assert_eq!(lexical.reason, None);
    assert_eq!(lexical.snapshot_id.as_deref(), Some("snapshot-001"));
    assert_eq!(lexical.artifact_count, Some(1));

    assert_eq!(semantic.state, WorkspaceIndexComponentState::Error);
    assert_eq!(
        semantic.reason.as_deref(),
        Some("semantic_runtime_invalid_config")
    );

    let _ = fs::remove_dir_all(workspace_root);
}

#[test]
fn repository_summary_bypasses_cached_ready_lexical_health_for_dirty_roots() {
    let workspace_root = temp_workspace_root("repository-summary-dirty-root-bypass");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace src directory");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub struct DirtySummary;\n",
    )
    .expect("failed to write source fixture");

    let server = FriggMcpServer::new_with_runtime_options(
        FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
            .expect("workspace root must produce valid config"),
        false,
        false,
    );
    let workspace = server
        .runtime_state
        .workspace_registry
        .read()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");
    seed_manifest_snapshot(
        &workspace_root,
        &workspace.runtime_repository_id,
        "snapshot-001",
        &["src/lib.rs"],
    );

    let initial = server.repository_summary(&workspace);
    let initial_lexical = initial
        .health
        .as_ref()
        .expect("repository summary should expose health")
        .lexical
        .clone();
    assert_eq!(initial_lexical.state, WorkspaceIndexComponentState::Ready);
    assert_eq!(initial_lexical.reason, None);
    assert_eq!(initial_lexical.snapshot_id.as_deref(), Some("snapshot-001"));

    server
        .runtime_state
        .validated_manifest_candidate_cache
        .write()
        .expect("validated manifest candidate cache should not be poisoned")
        .mark_dirty_root(&workspace.root);

    let refreshed = server.repository_summary(&workspace);
    let refreshed_lexical = refreshed
        .health
        .as_ref()
        .expect("repository summary should expose health")
        .lexical
        .clone();
    assert_eq!(refreshed_lexical.state, WorkspaceIndexComponentState::Stale);
    assert_eq!(refreshed_lexical.reason.as_deref(), Some("dirty_root"));
    assert_eq!(
        refreshed_lexical.snapshot_id.as_deref(),
        Some("snapshot-001")
    );
    assert_eq!(refreshed_lexical.artifact_count, Some(1));

    let _ = fs::remove_dir_all(workspace_root);
}

#[test]
fn workspace_current_runtime_tasks_surface_class_aware_watch_phases() {
    let server = FriggMcpServer::new_with_runtime_options(fixture_config(), true, false);

    let manifest_task_id = server
        .runtime_state
        .runtime_task_registry
        .write()
        .expect("runtime task registry should not be poisoned")
        .start_task(
            RuntimeTaskKind::ChangedReindex,
            "repo-001",
            "watch_manifest_fast",
            Some("watch root /tmp/repo-001 class manifest_fast".to_owned()),
        );
    server
        .runtime_state
        .runtime_task_registry
        .write()
        .expect("runtime task registry should not be poisoned")
        .finish_task(
            &manifest_task_id,
            RuntimeTaskStatus::Succeeded,
            Some("watch root /tmp/repo-001 class manifest_fast".to_owned()),
        );
    server
        .runtime_state
        .runtime_task_registry
        .write()
        .expect("runtime task registry should not be poisoned")
        .start_task(
            RuntimeTaskKind::SemanticRefresh,
            "repo-001",
            "watch_semantic_followup",
            Some("watch root /tmp/repo-001 class semantic_followup".to_owned()),
        );

    let runtime = server.runtime_status_summary();

    assert!(
        runtime.recent_tasks.iter().any(|task| {
            task.kind == RuntimeTaskKind::ChangedReindex
                && task.phase == "watch_manifest_fast"
                && task.detail.as_deref() == Some("watch root /tmp/repo-001 class manifest_fast")
        }),
        "recent tasks should surface manifest-fast watch work distinctly"
    );
    assert!(
        runtime.active_tasks.iter().any(|task| {
            task.kind == RuntimeTaskKind::SemanticRefresh
                && task.phase == "watch_semantic_followup"
                && task.detail.as_deref()
                    == Some("watch root /tmp/repo-001 class semantic_followup")
        }),
        "active tasks should surface semantic-followup watch work distinctly"
    );
}

#[test]
fn repository_summary_reports_precise_ingest_failures_separately_from_scip_discovery() {
    let workspace_root = temp_workspace_root("precise-ingest-failure-summary");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace src directory");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub struct PreciseFailure;\n",
    )
    .expect("failed to write source fixture");
    let scip_dir = workspace_root.join(".frigg/scip");
    fs::create_dir_all(&scip_dir).expect("failed to create scip dir");
    fs::write(scip_dir.join("oversized.scip"), "0123456789")
        .expect("failed to write oversized scip artifact");

    let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
        .expect("workspace root must produce valid config");
    config.max_file_bytes = 1;
    let server = FriggMcpServer::new_with_runtime_options(config, false, false);
    let workspace = server
        .runtime_state
        .workspace_registry
        .read()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");

    let summary = server.repository_summary(&workspace);
    let health = summary
        .health
        .as_ref()
        .expect("repository summary should expose health");
    assert_eq!(health.scip.state, WorkspaceIndexComponentState::Ready);
    let precise_ingest = health
        .precise_ingest
        .as_ref()
        .expect("repository health should expose precise ingest status");
    assert_eq!(precise_ingest.state, WorkspacePreciseIngestState::Failed);
    assert_eq!(
        precise_ingest.coverage_mode,
        Some(WorkspacePreciseCoverageMode::None)
    );
    assert_eq!(precise_ingest.artifacts_discovered, 1);
    assert_eq!(precise_ingest.artifacts_failed, 1);
    assert!(
        precise_ingest
            .reason
            .as_deref()
            .is_some_and(|reason| reason.contains("scip ingest failed"))
    );

    let precise = server.workspace_precise_summary_for_workspace(&workspace, None);
    assert_eq!(precise.state, WorkspacePreciseState::Failed);
    assert!(precise.failure_summary.is_some());

    let _ = fs::remove_dir_all(workspace_root);
}

#[test]
fn repository_summary_full_scip_ingest_mode_accepts_artifacts_above_default_budget() {
    let workspace_root = temp_workspace_root("precise-ingest-full-scip-mode");
    fs::create_dir_all(workspace_root.join("src"))
        .expect("failed to create workspace src directory");
    fs::write(
        workspace_root.join("src/lib.rs"),
        "pub struct User;\n\npub fn current_user() -> User { User }\n",
    )
    .expect("failed to write source fixture");
    let scip_dir = workspace_root.join(".frigg/scip");
    fs::create_dir_all(&scip_dir).expect("failed to create scip dir");
    fs::write(
        scip_dir.join("oversized.json"),
        r#"{"documents":[{"relative_path":"src/lib.rs","occurrences":[{"symbol":"scip-rust pkg repo#User","range":[0,11,15],"symbol_roles":1},{"symbol":"scip-rust pkg repo#User","range":[2,31,35],"symbol_roles":8}],"symbols":[{"symbol":"scip-rust pkg repo#User","display_name":"User","kind":"class"}]}]}"#,
    )
    .expect("failed to write scip artifact");

    let mut config = FriggConfig::from_workspace_roots(vec![workspace_root.clone()])
        .expect("workspace root must produce valid config");
    config.max_file_bytes = 1;
    config.full_scip_ingest = true;
    let server = FriggMcpServer::new_with_runtime_options(config, false, false);
    let workspace = server
        .runtime_state
        .workspace_registry
        .read()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
        .known_workspaces()
        .into_iter()
        .next()
        .expect("server should register workspace");

    let summary = server.repository_summary(&workspace);
    let health = summary
        .health
        .as_ref()
        .expect("repository summary should expose health");
    let precise_ingest = health
        .precise_ingest
        .as_ref()
        .expect("repository health should expose precise ingest status");
    assert_eq!(precise_ingest.state, WorkspacePreciseIngestState::Ready);
    assert_eq!(
        precise_ingest.coverage_mode,
        Some(WorkspacePreciseCoverageMode::Full)
    );
    assert_eq!(precise_ingest.artifacts_discovered, 1);
    assert_eq!(precise_ingest.artifacts_ingested, 1);
    assert_eq!(precise_ingest.artifacts_failed, 0);

    let precise = server.workspace_precise_summary_for_workspace(&workspace, None);
    assert_eq!(precise.state, WorkspacePreciseState::Ok);
    assert!(precise.failure_summary.is_none());

    let _ = fs::remove_dir_all(workspace_root);
}