feat(cli): start fabro mcp stdio server

This commit is contained in:
Bryan Helmkamp 2026-05-11 08:03:55 -04:00
parent c19acaccd3
commit f425e51acb
No known key found for this signature in database
6 changed files with 761 additions and 5 deletions

2
Cargo.lock generated
View file

@ -2026,7 +2026,9 @@ version = "0.230.0-nightly.0"
dependencies = [
"anyhow",
"dirs",
"fabro-client",
"rmcp",
"schemars 1.2.1",
"serde",
"serde_json",
"tokio",

View file

@ -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<String>,
env: HashMap<String, String>,
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
}

View file

@ -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

View file

@ -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<String>) -> 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<T> = Result<T, ToolError>;
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct FabroRunCreateParams {
pub(crate) runs: Vec<CreateRunSpec>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct CreateRunSpec {
pub(crate) workflow: String,
pub(crate) cwd: Option<PathBuf>,
pub(crate) run_id: Option<String>,
pub(crate) goal: Option<String>,
#[serde(default)]
pub(crate) inputs: HashMap<String, Value>,
#[serde(default)]
pub(crate) labels: HashMap<String, String>,
pub(crate) dry_run: Option<bool>,
pub(crate) auto_approve: Option<bool>,
pub(crate) model: Option<String>,
pub(crate) provider: Option<String>,
pub(crate) sandbox: Option<String>,
pub(crate) preserve_sandbox: Option<bool>,
pub(crate) start: Option<bool>,
}
#[derive(Debug)]
pub(crate) struct ValidatedCreateRuns {
pub(crate) runs: Vec<CreateRunSpec>,
}
impl TryFrom<FabroRunCreateParams> for ValidatedCreateRuns {
type Error = ToolError;
fn try_from(params: FabroRunCreateParams) -> Result<Self, Self::Error> {
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<CreatedRunResult>,
}
#[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<Vec<String>>,
pub(crate) workflow: Option<String>,
pub(crate) labels: Option<HashMap<String, String>>,
pub(crate) status: Option<Vec<String>>,
pub(crate) archived: Option<bool>,
pub(crate) created_after: Option<String>,
pub(crate) created_before: Option<String>,
pub(crate) first: Option<usize>,
pub(crate) after: Option<String>,
}
#[derive(Debug)]
pub(crate) struct ValidatedSearchRuns {
pub(crate) raw: FabroRunSearchParams,
}
impl TryFrom<FabroRunSearchParams> for ValidatedSearchRuns {
type Error = ToolError;
fn try_from(params: FabroRunSearchParams) -> Result<Self, Self::Error> {
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<RunSummaryResult>,
pub(crate) next_cursor: Option<String>,
}
#[derive(Debug, Serialize, JsonSchema)]
pub(crate) struct RunSummaryResult {
pub(crate) run_id: String,
pub(crate) workflow_name: String,
pub(crate) workflow_slug: Option<String>,
pub(crate) status: String,
pub(crate) archived: bool,
pub(crate) created_at: String,
pub(crate) started_at: Option<String>,
pub(crate) completed_at: Option<String>,
pub(crate) labels: HashMap<String, String>,
pub(crate) source_directory: Option<String>,
pub(crate) repo_origin_url: Option<String>,
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<String>,
pub(crate) interrupt: Option<bool>,
pub(crate) question_id: Option<String>,
pub(crate) answer: Option<Value>,
}
#[derive(Debug)]
pub(crate) struct ValidatedInteractRun {
pub(crate) raw: FabroRunInteractParams,
}
impl TryFrom<FabroRunInteractParams> for ValidatedInteractRun {
type Error = ToolError;
fn try_from(params: FabroRunInteractParams) -> Result<Self, Self::Error> {
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<String>,
pub(crate) timeout_seconds: Option<u64>,
pub(crate) poll_interval_seconds: Option<u64>,
}
#[derive(Debug)]
pub(crate) struct ValidatedGatherRuns {
pub(crate) run_ids: Vec<String>,
pub(crate) timeout_seconds: u64,
pub(crate) poll_interval_seconds: u64,
}
impl TryFrom<FabroRunGatherParams> for ValidatedGatherRuns {
type Error = ToolError;
fn try_from(params: FabroRunGatherParams) -> Result<Self, Self::Error> {
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<RunSummaryResult>,
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<Vec<String>>,
pub(crate) categories: Option<Vec<String>>,
pub(crate) direction: Option<String>,
pub(crate) created_after: Option<String>,
pub(crate) created_before: Option<String>,
pub(crate) first: Option<usize>,
pub(crate) after: Option<u32>,
pub(crate) event_ids: Option<Vec<String>>,
pub(crate) offset: Option<usize>,
pub(crate) limit: Option<usize>,
pub(crate) max_content_length: Option<usize>,
pub(crate) query: Option<String>,
}
#[derive(Debug)]
pub(crate) struct ValidatedRunEvents {
pub(crate) raw: FabroRunEventsParams,
}
impl TryFrom<FabroRunEventsParams> for ValidatedRunEvents {
type Error = ToolError;
fn try_from(params: FabroRunEventsParams) -> Result<Self, Self::Error> {
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<RunEventResult>,
pub(crate) next_cursor: Option<u32>,
}
#[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<Client>,
_base_cwd: &Path,
_params: ValidatedCreateRuns,
) -> ToolResult<CreateRunsResult> {
yield_now().await;
Err(ToolError::message(
"fabro_run_create is not implemented yet",
))
}
pub(crate) async fn search_runs(
_client: Arc<Client>,
_params: ValidatedSearchRuns,
) -> ToolResult<SearchRunsResult> {
yield_now().await;
Err(ToolError::message(
"fabro_run_search is not implemented yet",
))
}
pub(crate) async fn interact_run(
_client: Arc<Client>,
_params: ValidatedInteractRun,
) -> ToolResult<InteractRunResult> {
yield_now().await;
Err(ToolError::message(
"fabro_run_interact is not implemented yet",
))
}
pub(crate) async fn gather_runs(
_client: Arc<Client>,
_params: ValidatedGatherRuns,
) -> ToolResult<GatherRunsResult> {
yield_now().await;
Err(ToolError::message(
"fabro_run_gather is not implemented yet",
))
}
pub(crate) async fn run_events(
_client: Arc<Client>,
_params: ValidatedRunEvents,
) -> ToolResult<RunEventsResult> {
yield_now().await;
Err(ToolError::message(
"fabro_run_events is not implemented yet",
))
}
pub(crate) fn success_result<T: Serialize>(
value: &T,
text: impl Into<String>,
) -> Result<CallToolResult, rmcp::ErrorData> {
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:#}")
}

View file

@ -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<McpServerSettings>,
client: Arc<OnceCell<Arc<Client>>>,
cwd: PathBuf,
tool_router: ToolRouter<Self>,
}
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<McpServerSettings>) -> 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<run_tools::FabroRunCreateParams>,
) -> Result<CallToolResult, ErrorData> {
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<run_tools::FabroRunSearchParams>,
) -> Result<CallToolResult, ErrorData> {
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<run_tools::FabroRunInteractParams>,
) -> Result<CallToolResult, ErrorData> {
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<run_tools::FabroRunGatherParams>,
) -> Result<CallToolResult, ErrorData> {
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<run_tools::FabroRunEventsParams>,
) -> Result<CallToolResult, ErrorData> {
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<Arc<Client>, 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<Client> {
yield_now().await;
Err(anyhow!("fabro MCP API client is not implemented yet"))
}

View file

@ -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<String, String> {
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,