relay-knowledge 1.1.17

Graph-database-based knowledge graph project.
Documentation
//! Cross-owner MCP runtime guardrail scenarios.

use std::{
    fs,
    path::{Path, PathBuf},
    process::Command,
    time::{SystemTime, UNIX_EPOCH},
};

use serde_json::json;

use super::tests::{
    call_mcp_with_session, initialize_session, server_and_service, tool_call, tool_names,
};
use super::*;
use crate::{
    api::{CodeRepositoryRegisterRequest, InterfaceKind, RequestContext},
    application::{RelayKnowledgeService, RuntimeConfiguration},
    domain::{CodeIndexMode, CodeIndexRequest, CodeRepositorySelector, FreshnessPolicy},
    env::{EnvironmentConfig, PlatformKind},
};

#[tokio::test]
async fn initialize_tools_list_and_readonly_diagnostics_do_not_open_storage() {
    let home = unique_temp_dir("mcp-lazy-init");
    let home_text = home.to_str().expect("temp path should be UTF-8").to_owned();
    let environment = EnvironmentConfig::from_pairs(
        PlatformKind::Unix,
        [
            ("HOME", "/home/alice"),
            ("TMPDIR", "/tmp"),
            ("RELAY_KNOWLEDGE_HOME", home_text.as_str()),
            ("RELAY_KNOWLEDGE_MCP_STREAMABLE_HTTP_ENABLED", "true"),
            ("RELAY_KNOWLEDGE_MCP_ALLOWED_SCOPES", "docs"),
        ],
    )
    .expect("environment should parse");
    let runtime = RuntimeConfiguration::from_environment(&environment)
        .await
        .expect("runtime should compose");
    let database_path = runtime.paths.database_file();
    let service = RelayKnowledgeService::new(runtime.clone());
    let server = McpServer::new(service, runtime.network.clone(), runtime.agent.clone());
    let mut router = server.clone().router();

    assert!(!database_path.exists());
    let session_id = initialize_session(&mut router).await;
    let tools = call_mcp_with_session(
        &mut router,
        json!({"jsonrpc": "2.0", "id": "tools", "method": "tools/list"}),
        &session_id,
    )
    .await;
    let health = tool_call(&mut router, "health", "relay_health", json!({})).await;
    let status = tool_call(
        &mut router,
        "service-status",
        "relay_service_status",
        json!({}),
    )
    .await;

    assert!(tool_names(&tools).contains(&"relay_code_query".to_owned()));
    assert_eq!(server.metrics.snapshot().cold_start_total, 1);
    assert!(
        tools["result"]["tools"]
            .as_array()
            .expect("tools")
            .iter()
            .any(|tool| tool["inputSchema"]["properties"]["query"]
                .get("maxLength")
                .is_some())
    );
    assert_eq!(health["result"]["isError"], false);
    assert_eq!(status["result"]["isError"], false);
    assert!(!database_path.exists());
    let index_status =
        tool_call(&mut router, "index-status", "relay_index_status", json!({})).await;
    assert_eq!(index_status["result"]["isError"], false);
    assert!(database_path.exists());
    let _ = fs::remove_dir_all(home);
}

#[tokio::test]
async fn tool_inputs_reject_oversized_queries_and_paths() {
    let server = server_and_service([("RELAY_KNOWLEDGE_MCP_ALLOWED_SCOPES", "docs")])
        .await
        .0;
    let mut router = server.router();
    let too_long_query = "q".repeat(10_001);
    let too_long_path = "p".repeat(4_097);

    let retrieve = tool_call(
        &mut router,
        "long-retrieve",
        "relay_retrieve_context",
        json!({"query": too_long_query, "source_scope": "docs"}),
    )
    .await;
    let code = tool_call(
        &mut router,
        "long-path",
        "relay_code_query",
        json!({
            "repository": "docs",
            "query": "target",
            "path_filters": [too_long_path]
        }),
    )
    .await;

    assert_eq!(
        retrieve["result"]["structuredContent"]["error_kind"],
        "invalid_argument"
    );
    assert_eq!(
        code["result"]["structuredContent"]["error_kind"],
        "invalid_argument"
    );
}

#[tokio::test]
async fn code_query_returns_adaptive_budget_and_container_outlines() {
    let repo = FixtureRepo::create("mcp-issue-283-budget");
    for index in 0..6 {
        repo.write(
            &format!("src/file_{index}.rs"),
            &format!(
                "pub struct Target{index} {{\n    value: u32,\n}}\n\nimpl Target{index} {{\n    pub fn target_method(&self) -> u32 {{\n        self.value\n    }}\n}}\n"
            ),
        );
    }
    repo.git(["add", "."]);
    repo.git(["commit", "-m", "initial"]);
    let (server, service) =
        server_and_service([("RELAY_KNOWLEDGE_MCP_ALLOWED_SCOPES", "fixture")]).await;
    register_and_index_fixture(&service, &repo, "fixture").await;
    let mut router = server.router();

    let response = tool_call(
        &mut router,
        "budgeted-code-query",
        "relay_code_query",
        json!({
            "repository": "fixture",
            "query": "Target target_method",
            "kind": "hybrid",
            "limit": 10,
            "include_code": true,
            "freshness": "wait-until-fresh"
        }),
    )
    .await;

    let structured = &response["result"]["structuredContent"];
    assert_eq!(response["result"]["isError"], false);
    assert_eq!(structured["explore_budget"]["calls"], 1);
    assert_eq!(structured["explore_budget"]["max_files"], 5);
    assert_eq!(structured["results"].as_array().expect("results").len(), 5);
    assert_eq!(
        response["result"]["content"][0]["text"],
        "code query returned 5 result(s)"
    );
    assert_eq!(structured["truncated"], true);
    assert_eq!(structured["agent_output"]["truncated"], true);
    let outline_response = tool_call(
        &mut router,
        "outlined-code-query",
        "relay_code_query",
        json!({
            "repository": "fixture",
            "query": "Target0",
            "kind": "hybrid",
            "limit": 5,
            "include_code": true,
            "freshness": "wait-until-fresh"
        }),
    )
    .await;
    let outlined = &outline_response["result"]["structuredContent"];
    assert!(
        outlined["results"]
            .as_array()
            .expect("results")
            .iter()
            .any(|result| result["source_outline"].as_bool() == Some(true)
                && result["excerpt"]
                    .as_str()
                    .is_some_and(|excerpt| excerpt.contains("pub struct Target")))
    );
}

async fn register_and_index_fixture(
    service: &RelayKnowledgeService,
    repo: &FixtureRepo,
    alias: &str,
) {
    service
        .register_code_repository(
            CodeRepositoryRegisterRequest {
                root_path: repo.path.display().to_string(),
                alias: alias.to_owned(),
                path_filters: vec!["src".to_owned()],
                language_filters: Vec::new(),
            },
            RequestContext::with_ids(InterfaceKind::Cli, "req-register", "trace-register"),
        )
        .await
        .expect("repository should register");
    service
        .index_code_repository(
            CodeIndexRequest {
                repository: CodeRepositorySelector::new(alias, "HEAD", Vec::new(), Vec::new())
                    .expect("selector should validate"),
                mode: CodeIndexMode::Full,
                workspace_detection: Default::default(),
                freshness_policy: FreshnessPolicy::WaitUntilFresh,
                reuse_historical: false,
            },
            RequestContext::with_ids(InterfaceKind::Cli, "req-index", "trace-index"),
        )
        .await
        .expect("repository should index");
}

struct FixtureRepo {
    path: PathBuf,
}

impl FixtureRepo {
    fn create(name: &str) -> Self {
        let path = unique_temp_dir(name);
        fs::create_dir_all(path.join("src")).expect("repo directory should be created");
        let repo = Self { path };
        repo.git(["init"]);
        repo.git(["config", "user.email", "relay@example.invalid"]);
        repo.git(["config", "user.name", "Relay Test"]);
        repo
    }

    fn write(&self, relative: &str, content: &str) {
        let path = self.path.join(relative);
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent).expect("parent directory should exist");
        }
        fs::write(path, content).expect("fixture file should be written");
    }

    fn git<const N: usize>(&self, args: [&str; N]) {
        let output = git_command(&self.path, args)
            .output()
            .expect("git should run");
        assert!(
            output.status.success(),
            "git failed: {}",
            String::from_utf8_lossy(&output.stderr)
        );
    }
}

impl Drop for FixtureRepo {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.path);
    }
}

fn git_command<const N: usize>(path: &Path, args: [&str; N]) -> Command {
    let mut command = Command::new("git");
    command.current_dir(path).args(args);
    command
}

fn unique_temp_dir(name: &str) -> PathBuf {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock should be after epoch")
        .as_nanos();
    let path = std::env::temp_dir().join(format!(
        "relay-knowledge-{name}-{}-{nanos}",
        std::process::id()
    ));
    let _ = fs::remove_dir_all(&path);
    path
}