frigg 0.9.2

Frigg gives AI agents local, source-backed code search and navigation without sending whole repositories through every prompt.
Documentation
//! Integration tests that gate MCP runtime behavior across workspace, cache, navigation, and freshness.

use std::collections::BTreeSet;
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock, RwLock};
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use crate::domain::FriggError;
use crate::indexer::{FileDigest, FileMetadataDigest, IndexMode, ManifestBuilder};
use crate::mcp::server_cache::{
    HeuristicReferenceCacheKey, RuntimeCacheFamily, RuntimeCacheFreshnessContract,
    RuntimeCacheResidency, RuntimeCacheReuseClass,
};
use crate::mcp::tool_surface::{ToolSurfaceProfile, manifest_for_tool_surface_profile};
use crate::mcp::types::{
    InspectSyntaxTreeParams, ListFilesParams, RuntimeTaskKind, RuntimeTaskStatus,
    SearchStructuralParams, WorkspaceAttachParams, WorkspaceDetachParams,
    WorkspaceIndexComponentState, WorkspacePreciseGeneratorState, WorkspaceResolveMode,
};
use crate::mcp::{RuntimeTaskGuard, RuntimeTaskRegistry};
use crate::searcher::ValidatedManifestCandidateCache;
use crate::settings::{
    FriggConfig, RuntimeProfile, RuntimeTransportKind, SemanticRuntimeConfig,
    SemanticRuntimeCredentials, SemanticRuntimeProvider, WatchConfig, WatchMode,
};
use crate::storage::{
    DEFAULT_VECTOR_DIMENSIONS, ManifestEntry, SemanticChunkEmbeddingRecord, Storage,
};
use crate::watch::maybe_start_watch_runtime;
use protobuf::{EnumOrUnknown, Message};
use rmcp::handler::server::wrapper::Parameters;
use rmcp::model::ErrorCode;
use scip::types::{
    Document as ScipDocumentProto, Index as ScipIndexProto, Occurrence as ScipOccurrenceProto,
    SymbolInformation as ScipSymbolInformationProto,
};
use serde_json::Value;

use super::{
    FriggMcpServer, PreciseCoverageMode, PreciseIngestStats, RepositoryResponseCacheFreshnessMode,
};

static TEST_PATH_LOCK: OnceLock<Mutex<()>> = OnceLock::new();

fn fixture_config() -> FriggConfig {
    let workspace_root = std::env::current_dir()
        .expect("current working directory should exist for runtime gate tests");
    FriggConfig::from_workspace_roots(vec![workspace_root])
        .expect("runtime gate tests should build a valid FriggConfig")
}

fn to_set(values: Vec<String>) -> BTreeSet<String> {
    values.into_iter().collect()
}

fn extended_only_tool_names() -> Vec<String> {
    let core = manifest_for_tool_surface_profile(ToolSurfaceProfile::Core)
        .tool_names
        .into_iter()
        .collect::<BTreeSet<_>>();
    manifest_for_tool_surface_profile(ToolSurfaceProfile::Extended)
        .tool_names
        .into_iter()
        .filter(|tool_name| !core.contains(tool_name))
        .collect()
}

fn temp_workspace_root(test_name: &str) -> PathBuf {
    let nanos_since_epoch = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    let root = std::env::temp_dir().join(format!(
        "frigg-runtime-gate-tests-{test_name}-{}-{nanos_since_epoch}",
        std::process::id()
    ));
    fs::create_dir_all(root.join(".git")).expect("temp workspace git marker should be creatable");
    root
}

fn non_git_temp_workspace_root(test_name: &str) -> PathBuf {
    let nanos_since_epoch = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    std::env::temp_dir().join(format!(
        "frigg-runtime-gate-tests-{test_name}-{}-{nanos_since_epoch}",
        std::process::id()
    ))
}

fn authorized_temp_workspace_root(test_name: &str) -> PathBuf {
    let nanos_since_epoch = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|duration| duration.as_nanos())
        .unwrap_or(0);
    let base = std::env::current_dir()
        .unwrap_or_else(|_| std::env::temp_dir())
        .join(".codex-cache")
        .join("runtime-gate-tests");
    base.join(format!(
        "frigg-runtime-gate-tests-{test_name}-{}-{nanos_since_epoch}",
        std::process::id()
    ))
}

fn write_fake_precise_generator_script(
    bin_dir: &Path,
    name: &str,
    version_output: &str,
    payload: &str,
) -> PathBuf {
    let path = bin_dir.join(name);
    write_fake_precise_generator_script_with_body(
        bin_dir,
        name,
        &format!(
            r#"#!/bin/sh
if [ "${{1:-}}" = "--version" ] || [ "${{1:-}}" = "version" ]; then
  printf '%s\n' "{version_output}"
  exit 0
fi
printf '%s' "{payload}"
"#
        ),
    );
    path
}

fn write_fake_precise_generator_script_with_body(
    bin_dir: &Path,
    name: &str,
    body: &str,
) -> PathBuf {
    let path = bin_dir.join(name);
    fs::write(&path, body).expect("failed to write fake precise generator script");
    let mut permissions = fs::metadata(&path)
        .expect("fake precise generator script should exist")
        .permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&path, permissions)
        .expect("fake precise generator script should be executable");
    path
}

fn with_fake_precise_generator_path<T>(bin_dir: &Path, f: impl FnOnce() -> T) -> T {
    let _guard = TEST_PATH_LOCK
        .get_or_init(|| Mutex::new(()))
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    FriggMcpServer::set_test_precise_generator_bin_override(Some(bin_dir.to_path_buf()));

    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
    FriggMcpServer::set_test_precise_generator_bin_override(None);

    match result {
        Ok(value) => value,
        Err(payload) => std::panic::resume_unwind(payload),
    }
}

fn rewrite_fixture_file_with_mtime_tick(path: &Path, contents: &str) {
    let before = fs::metadata(path)
        .ok()
        .and_then(|metadata| metadata.modified().ok())
        .and_then(FriggMcpServer::system_time_to_unix_nanos);
    for _ in 0..8 {
        std::thread::sleep(Duration::from_millis(2));
        fs::write(path, contents).expect("failed to rewrite fixture file");
        let after = fs::metadata(path)
            .ok()
            .and_then(|metadata| metadata.modified().ok())
            .and_then(FriggMcpServer::system_time_to_unix_nanos);
        if after != before {
            return;
        }
    }

    unreachable!("fixture file mtime did not advance after rewrite");
}

fn semantic_runtime_enabled_openai() -> SemanticRuntimeConfig {
    SemanticRuntimeConfig {
        enabled: true,
        provider: Some(SemanticRuntimeProvider::OpenAi),
        model: Some("text-embedding-3-small".to_owned()),
        strict_mode: false,
        openai_compat_endpoint: None,
    }
}

async fn wait_for_heuristic_reference_cache_eviction(
    server: &FriggMcpServer,
    timeout: Duration,
) -> bool {
    let deadline = tokio::time::Instant::now() + timeout;
    loop {
        let heuristic_evicted = server
            .cache_state
            .heuristic_reference_cache
            .read()
            .expect("heuristic reference cache should not be poisoned")
            .is_empty();

        if heuristic_evicted {
            return true;
        }

        if tokio::time::Instant::now() >= deadline {
            return false;
        }

        tokio::time::sleep(Duration::from_millis(25)).await;
    }
}

fn seed_manifest_snapshot(
    workspace_root: &Path,
    repository_id: &str,
    snapshot_id: &str,
    paths: &[&str],
) {
    let db_path = crate::storage::ensure_provenance_db_parent_dir(workspace_root)
        .expect("manifest storage path should work");
    let storage = Storage::new(db_path);
    storage
        .initialize()
        .expect("manifest storage should initialize");

    let wanted_paths = paths
        .iter()
        .copied()
        .map(ToOwned::to_owned)
        .collect::<BTreeSet<_>>();
    let mut seen_paths = BTreeSet::new();
    let mut manifest_entries = ManifestBuilder::default()
        .build(workspace_root)
        .expect("manifest snapshot paths should build for test")
        .into_iter()
        .filter_map(|entry| {
            let path = entry
                .path
                .strip_prefix(workspace_root)
                .unwrap_or(&entry.path)
                .to_string_lossy()
                .to_string();
            if wanted_paths.contains(&path) {
                seen_paths.insert(path.clone());
                Some(ManifestEntry {
                    path,
                    sha256: entry.hash_blake3_hex,
                    size_bytes: entry.size_bytes,
                    mtime_ns: entry.mtime_ns,
                })
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    for path in wanted_paths.difference(&seen_paths) {
        let absolute_path = workspace_root.join(path);
        let bytes = fs::read(&absolute_path).expect("manifest snapshot path should read for test");
        let metadata =
            fs::metadata(&absolute_path).expect("manifest snapshot path should exist for test");
        manifest_entries.push(ManifestEntry {
            path: path.to_owned(),
            sha256: blake3::hash(&bytes).to_hex().to_string(),
            size_bytes: metadata.len(),
            mtime_ns: metadata
                .modified()
                .ok()
                .and_then(FriggMcpServer::system_time_to_unix_nanos),
        });
    }
    manifest_entries.sort_by(|left, right| left.path.cmp(&right.path));
    manifest_entries.dedup_by(|left, right| left.path == right.path);

    storage
        .upsert_manifest(repository_id, snapshot_id, &manifest_entries)
        .expect("manifest snapshot should persist");
}

fn semantic_record(
    repository_id: &str,
    snapshot_id: &str,
    path: &str,
) -> SemanticChunkEmbeddingRecord {
    let mut embedding = vec![0.25, 0.75];
    embedding.resize(DEFAULT_VECTOR_DIMENSIONS, 0.0);
    SemanticChunkEmbeddingRecord {
        chunk_id: format!("chunk-{}", path.replace('/', "_")),
        repository_id: repository_id.to_owned(),
        snapshot_id: snapshot_id.to_owned(),
        path: path.to_owned(),
        language: "rust".to_owned(),
        chunk_index: 0,
        start_line: 1,
        end_line: 1,
        provider: "openai".to_owned(),
        model: "text-embedding-3-small".to_owned(),
        trace_id: Some("trace-001".to_owned()),
        content_hash_blake3: format!("hash-{}", path.replace('/', "_")),
        content_text: path.to_owned(),
        embedding,
    }
}

fn write_scip_protobuf_fixture(workspace_root: &Path, file_name: &str) {
    let fixture_dir = workspace_root.join(".frigg/scip");
    fs::create_dir_all(&fixture_dir).expect("failed to create scip fixture directory");

    let mut index = ScipIndexProto::new();
    let mut document = ScipDocumentProto::new();
    document.relative_path = "src/lib.rs".to_owned();

    let mut definition = ScipOccurrenceProto::new();
    definition.symbol = "scip-rust pkg repo#User".to_owned();
    definition.range = vec![0, 11, 15];
    definition.symbol_roles = 1;
    document.occurrences.push(definition);

    let mut reference = ScipOccurrenceProto::new();
    reference.symbol = "scip-rust pkg repo#User".to_owned();
    reference.range = vec![2, 31, 35];
    reference.symbol_roles = 8;
    document.occurrences.push(reference);

    let mut symbol = ScipSymbolInformationProto::new();
    symbol.symbol = "scip-rust pkg repo#User".to_owned();
    symbol.display_name = "User".to_owned();
    symbol.kind = EnumOrUnknown::from_i32(7);
    document.symbols.push(symbol);

    index.documents.push(document);
    let payload = index
        .write_to_bytes()
        .expect("protobuf fixture payload should serialize");
    fs::write(fixture_dir.join(file_name), payload)
        .expect("failed to write scip protobuf fixture payload");
}

mod cache_runtime;
mod error_mapping;
mod freshness;
mod navigation;
mod precise_generation;
mod status;
mod workspace;