diff --git a/Cargo.lock b/Cargo.lock index eac5d2242..6f7ded875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2026,7 +2026,9 @@ version = "0.230.0-nightly.0" dependencies = [ "anyhow", "dirs", + "fabro-client", "rmcp", + "schemars 1.2.1", "serde", "serde_json", "tokio", diff --git a/lib/crates/fabro-cli/tests/it/cmd/mcp.rs b/lib/crates/fabro-cli/tests/it/cmd/mcp.rs index 7f9081553..cb0a5f61b 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/mcp.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/mcp.rs @@ -2,9 +2,18 @@ clippy::disallowed_methods, reason = "integration tests stage MCP config files with sync std::fs" )] +#![expect( + clippy::disallowed_types, + reason = "raw stdio regression test intentionally uses blocking std pipes outside Tokio" +)] +use std::collections::HashMap; +use std::io::{BufRead as _, Write as _}; use std::path::{Path, PathBuf}; +use std::process::Stdio; +use fabro_mcp::client::McpClient; +use fabro_mcp::config::{McpServerSettings, McpTransport}; use fabro_test::{fabro_json_snapshot, fabro_snapshot, test_context}; #[test] @@ -336,6 +345,78 @@ fn init_invalid_json_fails_without_overwrite() { assert_eq!(std::fs::read_to_string(config_path).unwrap(), "{not json"); } +#[tokio::test(flavor = "multi_thread")] +async fn stdio_server_initializes_and_lists_run_tools() { + let context = test_context!(); + let client = spawn_mcp_client(&context, &[]).await; + + let tools = client.list_tools().await.unwrap(); + let names: Vec<_> = tools.iter().map(|(name, _, _)| name.as_str()).collect(); + assert_eq!(names, vec![ + "fabro_run_create", + "fabro_run_events", + "fabro_run_gather", + "fabro_run_interact", + "fabro_run_search", + ]); + for (_, _, schema) in tools { + assert!( + schema.is_object(), + "tool should have input schema: {schema}" + ); + } +} + +#[test] +fn stdio_start_writes_only_json_rpc_to_stdout() { + let context = test_context!(); + let fixture = mcp_stdio_fixture(&context, &[]); + let mut cmd = std::process::Command::new(&fixture.command[0]); + cmd.args(&fixture.command[1..]) + .env_clear() + .envs(&fixture.env) + .current_dir(&fixture.current_dir) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = cmd.spawn().unwrap(); + let mut stdin = child.stdin.take().unwrap(); + writeln!( + stdin, + r#"{{"jsonrpc":"2.0","id":1,"method":"initialize","params":{{"protocolVersion":"2025-06-18","capabilities":{{}},"clientInfo":{{"name":"fabro-test","version":"0.0.0"}}}}}}"# + ) + .unwrap(); + + let stdout = child.stdout.take().unwrap(); + let (tx, rx) = std::sync::mpsc::channel(); + std::thread::spawn(move || { + let mut line = String::new(); + let result = std::io::BufReader::new(stdout).read_line(&mut line); + let _ = tx.send(result.map(|_| line)); + }); + + let line = rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("initialize response should arrive") + .expect("stdout should be readable"); + let value: serde_json::Value = serde_json::from_str(line.trim()).unwrap(); + assert_eq!(value["jsonrpc"], "2.0"); + + let _ = child.kill(); + let _ = child.wait(); +} + +#[tokio::test(flavor = "multi_thread")] +async fn stdio_startup_and_list_tools_is_fast() { + let context = test_context!(); + let start = std::time::Instant::now(); + let client = spawn_mcp_client(&context, &[]).await; + let tools = client.list_tools().await.unwrap(); + assert_eq!(tools.len(), 5); + assert!(start.elapsed() < std::time::Duration::from_secs(2)); +} + fn expected_claude_config_path(home_dir: &Path) -> PathBuf { #[cfg(target_os = "macos")] { @@ -361,3 +442,49 @@ fn expected_claude_config_path(home_dir: &Path) -> PathBuf { .join("claude_desktop_config.json") } } + +struct McpStdioFixture { + command: Vec, + env: HashMap, + current_dir: PathBuf, +} + +fn mcp_stdio_fixture(context: &fabro_test::TestContext, extra_args: &[&str]) -> McpStdioFixture { + let mut command = vec![ + env!("CARGO_BIN_EXE_fabro").to_string(), + "mcp".to_string(), + "start".to_string(), + ]; + command.extend(extra_args.iter().map(|arg| (*arg).to_string())); + + let mut env = fabro_test::isolated_env(&context.home_dir); + env.insert( + "FABRO_HOME".to_string(), + context.home_dir.join(".fabro").display().to_string(), + ); + + McpStdioFixture { + command, + env, + current_dir: context.temp_dir.clone(), + } +} + +async fn spawn_mcp_client(context: &fabro_test::TestContext, extra_args: &[&str]) -> McpClient { + let fixture = mcp_stdio_fixture(context, extra_args); + let config = McpServerSettings { + name: "fabro-under-test".to_string(), + transport: McpTransport::Stdio { + command: fixture.command, + env: fixture.env, + }, + startup_timeout_secs: 10, + tool_timeout_secs: 30, + }; + let client = McpClient::new(&config).expect("MCP client should build"); + client + .initialize(config.startup_timeout()) + .await + .expect("MCP server should initialize"); + client +} diff --git a/lib/crates/fabro-mcp-server/Cargo.toml b/lib/crates/fabro-mcp-server/Cargo.toml index 94f1119fb..963d8aabc 100644 --- a/lib/crates/fabro-mcp-server/Cargo.toml +++ b/lib/crates/fabro-mcp-server/Cargo.toml @@ -15,7 +15,9 @@ workspace = true [dependencies] anyhow.workspace = true dirs.workspace = true +fabro-client = { path = "../fabro-client" } rmcp = { workspace = true, features = ["server", "macros", "schemars", "transport-io"] } +schemars = "1.2.1" serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/lib/crates/fabro-mcp-server/src/run_tools.rs b/lib/crates/fabro-mcp-server/src/run_tools.rs index 2bcd6d29e..7164f5885 100644 --- a/lib/crates/fabro-mcp-server/src/run_tools.rs +++ b/lib/crates/fabro-mcp-server/src/run_tools.rs @@ -1 +1,419 @@ -// Run-management MCP tools are implemented after the stdio server skeleton. +#![allow( + dead_code, + reason = "The MCP server skeleton defines the full first-slice contract before each tool body is implemented." +)] + +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use fabro_client::Client; +use rmcp::model::{CallToolResult, Content}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tokio::task::yield_now; + +#[derive(Debug)] +pub(crate) struct ToolError { + message: String, +} + +impl ToolError { + pub(crate) fn message(message: impl Into) -> Self { + Self { + message: message.into(), + } + } + + pub(crate) fn from_anyhow(err: &anyhow::Error) -> Self { + Self::message(format_tool_error(err)) + } + + pub(crate) fn as_str(&self) -> &str { + &self.message + } +} + +pub(crate) type ToolResult = Result; + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct FabroRunCreateParams { + pub(crate) runs: Vec, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct CreateRunSpec { + pub(crate) workflow: String, + pub(crate) cwd: Option, + pub(crate) run_id: Option, + pub(crate) goal: Option, + #[serde(default)] + pub(crate) inputs: HashMap, + #[serde(default)] + pub(crate) labels: HashMap, + pub(crate) dry_run: Option, + pub(crate) auto_approve: Option, + pub(crate) model: Option, + pub(crate) provider: Option, + pub(crate) sandbox: Option, + pub(crate) preserve_sandbox: Option, + pub(crate) start: Option, +} + +#[derive(Debug)] +pub(crate) struct ValidatedCreateRuns { + pub(crate) runs: Vec, +} + +impl TryFrom for ValidatedCreateRuns { + type Error = ToolError; + + fn try_from(params: FabroRunCreateParams) -> Result { + validate_len("runs", params.runs.len(), 1, 50)?; + Ok(Self { runs: params.runs }) + } +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct CreateRunsResult { + pub(crate) runs: Vec, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct CreatedRunResult { + pub(crate) run_id: String, + pub(crate) workflow: String, + pub(crate) started: bool, + pub(crate) status: String, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct FabroRunSearchParams { + pub(crate) run_ids: Option>, + pub(crate) workflow: Option, + pub(crate) labels: Option>, + pub(crate) status: Option>, + pub(crate) archived: Option, + pub(crate) created_after: Option, + pub(crate) created_before: Option, + pub(crate) first: Option, + pub(crate) after: Option, +} + +#[derive(Debug)] +pub(crate) struct ValidatedSearchRuns { + pub(crate) raw: FabroRunSearchParams, +} + +impl TryFrom for ValidatedSearchRuns { + type Error = ToolError; + + fn try_from(params: FabroRunSearchParams) -> Result { + if params.first.is_some_and(|first| first > 100) { + return Err(ToolError::message("first must be <= 100")); + } + Ok(Self { raw: params }) + } +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct SearchRunsResult { + pub(crate) runs: Vec, + pub(crate) next_cursor: Option, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct RunSummaryResult { + pub(crate) run_id: String, + pub(crate) workflow_name: String, + pub(crate) workflow_slug: Option, + pub(crate) status: String, + pub(crate) archived: bool, + pub(crate) created_at: String, + pub(crate) started_at: Option, + pub(crate) completed_at: Option, + pub(crate) labels: HashMap, + pub(crate) source_directory: Option, + pub(crate) repo_origin_url: Option, + pub(crate) goal: String, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum RunInteractAction { + Get, + Start, + Message, + Cancel, + Archive, + Unarchive, + GetQuestions, + Answer, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct FabroRunInteractParams { + pub(crate) action: RunInteractAction, + pub(crate) run_id: String, + pub(crate) message: Option, + pub(crate) interrupt: Option, + pub(crate) question_id: Option, + pub(crate) answer: Option, +} + +#[derive(Debug)] +pub(crate) struct ValidatedInteractRun { + pub(crate) raw: FabroRunInteractParams, +} + +impl TryFrom for ValidatedInteractRun { + type Error = ToolError; + + fn try_from(params: FabroRunInteractParams) -> Result { + if params.run_id.trim().is_empty() { + return Err(ToolError::message("run_id is required")); + } + if matches!(params.action, RunInteractAction::Message) + && params + .message + .as_deref() + .is_none_or(|message| message.trim().is_empty()) + { + return Err(ToolError::message("message is required for action message")); + } + if matches!(params.action, RunInteractAction::Answer) { + if params.question_id.as_deref().is_none_or(str::is_empty) { + return Err(ToolError::message( + "question_id is required for action answer", + )); + } + if params.answer.is_none() { + return Err(ToolError::message("answer is required for action answer")); + } + } + Ok(Self { raw: params }) + } +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct InteractRunResult { + pub(crate) run_id: String, + pub(crate) action: RunInteractAction, + pub(crate) result: Value, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct FabroRunGatherParams { + pub(crate) run_ids: Vec, + pub(crate) timeout_seconds: Option, + pub(crate) poll_interval_seconds: Option, +} + +#[derive(Debug)] +pub(crate) struct ValidatedGatherRuns { + pub(crate) run_ids: Vec, + pub(crate) timeout_seconds: u64, + pub(crate) poll_interval_seconds: u64, +} + +impl TryFrom for ValidatedGatherRuns { + type Error = ToolError; + + fn try_from(params: FabroRunGatherParams) -> Result { + validate_len("run_ids", params.run_ids.len(), 1, 50)?; + Ok(Self { + run_ids: params.run_ids, + timeout_seconds: params.timeout_seconds.unwrap_or(300).min(600), + poll_interval_seconds: params.poll_interval_seconds.unwrap_or(15).max(5), + }) + } +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct GatherRunsResult { + pub(crate) runs: Vec, + pub(crate) timed_out: bool, + pub(crate) elapsed_seconds: u64, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub(crate) enum RunEventsAction { + List, + Details, + Search, +} + +#[derive(Debug, Deserialize, JsonSchema)] +pub(crate) struct FabroRunEventsParams { + pub(crate) action: RunEventsAction, + pub(crate) run_id: String, + pub(crate) event_types: Option>, + pub(crate) categories: Option>, + pub(crate) direction: Option, + pub(crate) created_after: Option, + pub(crate) created_before: Option, + pub(crate) first: Option, + pub(crate) after: Option, + pub(crate) event_ids: Option>, + pub(crate) offset: Option, + pub(crate) limit: Option, + pub(crate) max_content_length: Option, + pub(crate) query: Option, +} + +#[derive(Debug)] +pub(crate) struct ValidatedRunEvents { + pub(crate) raw: FabroRunEventsParams, +} + +impl TryFrom for ValidatedRunEvents { + type Error = ToolError; + + fn try_from(params: FabroRunEventsParams) -> Result { + if params.run_id.trim().is_empty() { + return Err(ToolError::message("run_id is required")); + } + let first = params.first.or(params.limit).unwrap_or(50); + if first > 200 { + return Err(ToolError::message("first must be <= 200")); + } + Ok(Self { raw: params }) + } +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct RunEventsResult { + pub(crate) run_id: String, + pub(crate) action: RunEventsAction, + pub(crate) events: Vec, + pub(crate) next_cursor: Option, +} + +#[derive(Debug, Serialize, JsonSchema)] +pub(crate) struct RunEventResult { + pub(crate) event_id: String, + pub(crate) sequence: u32, + pub(crate) event: Value, + pub(crate) truncated: bool, +} + +pub(crate) async fn create_runs( + _client: Arc, + _base_cwd: &Path, + _params: ValidatedCreateRuns, +) -> ToolResult { + yield_now().await; + Err(ToolError::message( + "fabro_run_create is not implemented yet", + )) +} + +pub(crate) async fn search_runs( + _client: Arc, + _params: ValidatedSearchRuns, +) -> ToolResult { + yield_now().await; + Err(ToolError::message( + "fabro_run_search is not implemented yet", + )) +} + +pub(crate) async fn interact_run( + _client: Arc, + _params: ValidatedInteractRun, +) -> ToolResult { + yield_now().await; + Err(ToolError::message( + "fabro_run_interact is not implemented yet", + )) +} + +pub(crate) async fn gather_runs( + _client: Arc, + _params: ValidatedGatherRuns, +) -> ToolResult { + yield_now().await; + Err(ToolError::message( + "fabro_run_gather is not implemented yet", + )) +} + +pub(crate) async fn run_events( + _client: Arc, + _params: ValidatedRunEvents, +) -> ToolResult { + yield_now().await; + Err(ToolError::message( + "fabro_run_events is not implemented yet", + )) +} + +pub(crate) fn success_result( + value: &T, + text: impl Into, +) -> Result { + let structured_content = serde_json::to_value(value).map_err(|err| { + rmcp::ErrorData::internal_error( + format!("failed to serialize Fabro MCP tool result: {err}"), + None, + ) + })?; + let mut result = CallToolResult::structured(structured_content); + result.content = vec![Content::text(text.into())]; + Ok(result) +} + +pub(crate) fn error_result(err: ToolError) -> CallToolResult { + CallToolResult::error(vec![Content::text(err.message)]) +} + +pub(crate) fn create_runs_text(result: &CreateRunsResult) -> String { + let started = result.runs.iter().filter(|run| run.started).count(); + format!( + "created {} Fabro run(s), started {started}", + result.runs.len() + ) +} + +pub(crate) fn search_runs_text(result: &SearchRunsResult) -> String { + format!("found {} Fabro run(s)", result.runs.len()) +} + +pub(crate) fn interact_run_text(result: &InteractRunResult) -> String { + format!( + "completed {:?} for Fabro run {}", + result.action, result.run_id + ) +} + +pub(crate) fn gather_runs_text(result: &GatherRunsResult) -> String { + format!( + "gathered {} Fabro run(s), timed_out={}", + result.runs.len(), + result.timed_out + ) +} + +pub(crate) fn run_events_text(result: &RunEventsResult) -> String { + format!("returned {} Fabro event(s)", result.events.len()) +} + +fn validate_len(name: &str, len: usize, min: usize, max: usize) -> ToolResult<()> { + if len < min { + return Err(ToolError::message(format!( + "{name} must contain at least {min} item(s)" + ))); + } + if len > max { + return Err(ToolError::message(format!( + "{name} must contain no more than {max} item(s)" + ))); + } + Ok(()) +} + +fn format_tool_error(err: &anyhow::Error) -> String { + format!("{err:#}") +} diff --git a/lib/crates/fabro-mcp-server/src/server.rs b/lib/crates/fabro-mcp-server/src/server.rs index 1cf3cd462..59b18c6c1 100644 --- a/lib/crates/fabro-mcp-server/src/server.rs +++ b/lib/crates/fabro-mcp-server/src/server.rs @@ -1,7 +1,177 @@ -use anyhow::{Result, bail}; +use std::path::PathBuf; +use std::sync::Arc; -use crate::McpServerSettings; +use anyhow::{Result, anyhow}; +use fabro_client::Client; +use rmcp::handler::server::router::tool::ToolRouter; +use rmcp::handler::server::wrapper::Parameters; +use rmcp::model::{CallToolResult, ServerCapabilities, ServerInfo}; +use rmcp::transport::stdio; +use rmcp::{ErrorData, ServerHandler, serve_server, tool, tool_handler, tool_router}; +use tokio::sync::OnceCell; +use tokio::task::yield_now; -pub async fn start(_settings: McpServerSettings) -> Result<()> { - bail!("fabro mcp start is not implemented yet") +use crate::{McpServerSettings, run_tools}; + +#[derive(Clone)] +pub(crate) struct FabroMcpServer { + settings: Arc, + client: Arc>>, + cwd: PathBuf, + tool_router: ToolRouter, +} + +pub async fn start(settings: McpServerSettings) -> Result<()> { + let server = FabroMcpServer::new(Arc::new(settings)); + let service = serve_server(server, stdio()).await?; + service.waiting().await?; + Ok(()) +} + +#[tool_handler(router = self.tool_router)] +impl ServerHandler for FabroMcpServer { + fn get_info(&self) -> ServerInfo { + ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + .with_instructions("Use these tools to create, inspect, control, wait for, and read events from Fabro workflow runs.") + } +} + +#[tool_router(router = tool_router)] +impl FabroMcpServer { + pub(crate) fn new(settings: Arc) -> Self { + let cwd = settings.cwd.clone(); + Self { + settings, + client: Arc::new(OnceCell::new()), + cwd, + tool_router: Self::tool_router(), + } + } + + #[tool( + name = "fabro_run_create", + description = "Create one or more Fabro workflow runs, starting them by default." + )] + async fn fabro_run_create( + &self, + params: Parameters, + ) -> Result { + let params = match run_tools::ValidatedCreateRuns::try_from(params.0) { + Ok(params) => params, + Err(err) => return Ok(run_tools::error_result(err)), + }; + let client = match self.client().await { + Ok(client) => client, + Err(err) => return Ok(run_tools::error_result(err)), + }; + match run_tools::create_runs(client, &self.cwd, params).await { + Ok(result) => run_tools::success_result(&result, run_tools::create_runs_text(&result)), + Err(err) => Ok(run_tools::error_result(err)), + } + } + + #[tool( + name = "fabro_run_search", + description = "Search Fabro workflow runs by id, workflow, labels, status, archival state, and creation time." + )] + async fn fabro_run_search( + &self, + params: Parameters, + ) -> Result { + let params = match run_tools::ValidatedSearchRuns::try_from(params.0) { + Ok(params) => params, + Err(err) => return Ok(run_tools::error_result(err)), + }; + let client = match self.client().await { + Ok(client) => client, + Err(err) => return Ok(run_tools::error_result(err)), + }; + match run_tools::search_runs(client, params).await { + Ok(result) => run_tools::success_result(&result, run_tools::search_runs_text(&result)), + Err(err) => Ok(run_tools::error_result(err)), + } + } + + #[tool( + name = "fabro_run_interact", + description = "Get, start, message, cancel, archive, unarchive, inspect questions, or answer a Fabro run." + )] + async fn fabro_run_interact( + &self, + params: Parameters, + ) -> Result { + let params = match run_tools::ValidatedInteractRun::try_from(params.0) { + Ok(params) => params, + Err(err) => return Ok(run_tools::error_result(err)), + }; + let client = match self.client().await { + Ok(client) => client, + Err(err) => return Ok(run_tools::error_result(err)), + }; + match run_tools::interact_run(client, params).await { + Ok(result) => run_tools::success_result(&result, run_tools::interact_run_text(&result)), + Err(err) => Ok(run_tools::error_result(err)), + } + } + + #[tool( + name = "fabro_run_gather", + description = "Wait for Fabro runs to reach terminal states, returning current state on timeout." + )] + async fn fabro_run_gather( + &self, + params: Parameters, + ) -> Result { + let params = match run_tools::ValidatedGatherRuns::try_from(params.0) { + Ok(params) => params, + Err(err) => return Ok(run_tools::error_result(err)), + }; + let client = match self.client().await { + Ok(client) => client, + Err(err) => return Ok(run_tools::error_result(err)), + }; + match run_tools::gather_runs(client, params).await { + Ok(result) => run_tools::success_result(&result, run_tools::gather_runs_text(&result)), + Err(err) => Ok(run_tools::error_result(err)), + } + } + + #[tool( + name = "fabro_run_events", + description = "List, inspect, or search stored events for a Fabro workflow run." + )] + async fn fabro_run_events( + &self, + params: Parameters, + ) -> Result { + let params = match run_tools::ValidatedRunEvents::try_from(params.0) { + Ok(params) => params, + Err(err) => return Ok(run_tools::error_result(err)), + }; + let client = match self.client().await { + Ok(client) => client, + Err(err) => return Ok(run_tools::error_result(err)), + }; + match run_tools::run_events(client, params).await { + Ok(result) => run_tools::success_result(&result, run_tools::run_events_text(&result)), + Err(err) => Ok(run_tools::error_result(err)), + } + } + + async fn client(&self) -> Result, run_tools::ToolError> { + self.client + .get_or_try_init(|| async { + client_from_settings(&self.settings) + .await + .map(Arc::new) + .map_err(|err| run_tools::ToolError::from_anyhow(&err)) + }) + .await + .map(Arc::clone) + } +} + +async fn client_from_settings(_settings: &McpServerSettings) -> Result { + yield_now().await; + Err(anyhow!("fabro MCP API client is not implemented yet")) } diff --git a/lib/crates/fabro-test/src/lib.rs b/lib/crates/fabro-test/src/lib.rs index 77841bce7..8d90c1a3f 100644 --- a/lib/crates/fabro-test/src/lib.rs +++ b/lib/crates/fabro-test/src/lib.rs @@ -152,6 +152,43 @@ pub fn apply_test_isolation(cmd: &mut std::process::Command, home_dir: &Path) { apply_test_isolation_with_lookup(cmd, home_dir, |name| std::env::var_os(name)); } +#[must_use] +pub fn isolated_env(home_dir: &Path) -> HashMap { + let mut env = HashMap::new(); + if let Some(coverage) = + std::env::var_os(EnvVars::LLVM_PROFILE_FILE).and_then(|value| value.into_string().ok()) + { + env.insert(EnvVars::LLVM_PROFILE_FILE.to_string(), coverage); + } + if let Some(path) = std::env::var_os(EnvVars::PATH).and_then(|value| value.into_string().ok()) { + env.insert(EnvVars::PATH.to_string(), path); + } + env.insert(EnvVars::NO_COLOR.to_string(), "1".to_string()); + env.insert(EnvVars::HOME.to_string(), home_dir.display().to_string()); + env.insert( + EnvVars::FABRO_NO_UPGRADE_CHECK.to_string(), + "true".to_string(), + ); + env.insert( + EnvVars::FABRO_HTTP_PROXY_POLICY.to_string(), + "disabled".to_string(), + ); + env.insert(EnvVars::FABRO_TELEMETRY.to_string(), "off".to_string()); + env.insert( + EnvVars::FABRO_SUPPRESS_OPEN_BROWSER.to_string(), + "1".to_string(), + ); + env.insert( + EnvVars::FABRO_SERVER_MAX_CONCURRENT_RUNS.to_string(), + "64".to_string(), + ); + env.insert( + EnvVars::FABRO_TEST_IN_MEMORY_STORE.to_string(), + "1".to_string(), + ); + env +} + fn apply_test_isolation_with_lookup( cmd: &mut std::process::Command, home_dir: &Path,