fabro/lib/crates/fabro-cli/tests/it/cmd/config.rs
Bryan Helmkamp 8014f61054 chore(lint): fix all clippy warnings including --tests
Resolve every clippy warning across the workspace when running with
--tests enabled. Previously only library code was lint-clean; test
code had accumulated issues that were invisible without --tests.

Fixes:
- redundant_closure_for_method_calls: |s| s.as_source() -> InterpString::as_source
  (effective_settings, resolve_cli/root/server/features, run_event/record_serde,
  materialize_run) — add InterpString imports where needed
- absolute_paths: inline fabro_types::settings::* paths -> use imports;
  add #![allow(clippy::absolute_paths)] to fabro-cli and fabro-server
  IT test harnesses (matching the existing pattern in integration.rs)
- bool_assert_comparison: assert_eq!(x, true) -> assert!(x)
- needless_raw_string_hashes: r#"..."# -> r"..." where no inner quotes
- field_reassign_with_default: mut + field assign -> struct literal with ..Default
- match_same_arms: merge Timeout | Disconnected arms in attach.rs
- needless_pass_by_value: signal_rx by ref in attach.rs
- unreadable_literal: 9999999999 -> 9_999_999_999
- default_trait_access: Default::default() -> BTreeMap::default()
- items_after_statements: move use to function top
- large_futures: allow in integration.rs test module (test-only, not prod)
- filter_map_bool_then: .filter_map(bool::then) -> .filter().map()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 10:16:24 -04:00

943 lines
25 KiB
Rust

use std::path::PathBuf;
use fabro_config::parse_settings_layer;
use fabro_test::{fabro_snapshot, test_context};
use fabro_types::settings::SettingsLayer;
use httpmock::MockServer;
use predicates::prelude::*;
use super::support::run_state;
use crate::support::unique_run_id;
#[test]
fn old_config_show_command_is_rejected() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["config", "show"]);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: unrecognized subcommand 'config'
Usage: fabro [OPTIONS] <COMMAND>
For more information, try '--help'.
");
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
fn parse_settings(stdout: &[u8]) -> SettingsLayer {
serde_yaml::from_slice(stdout).expect("stdout should be valid YAML SettingsLayer")
}
fn resolve_cli(settings: &SettingsLayer) -> fabro_types::settings::CliSettings {
fabro_config::resolve_cli_from_file(settings).expect("cli settings should resolve")
}
fn resolve_project(settings: &SettingsLayer) -> fabro_types::settings::ProjectSettings {
fabro_config::resolve_project_from_file(settings).expect("project settings should resolve")
}
fn resolve_run(settings: &SettingsLayer) -> fabro_types::settings::RunSettings {
fabro_config::resolve_run_from_file(settings).expect("run settings should resolve")
}
fn resolve_server(settings: &SettingsLayer) -> fabro_types::settings::ServerSettings {
fabro_config::resolve_server_from_file(settings).expect("server settings should resolve")
}
fn run_goal_inline(settings: &SettingsLayer) -> Option<String> {
match resolve_run(settings).goal {
Some(fabro_types::settings::run::RunGoal::Inline(value)) => Some(value.as_source()),
_ => None,
}
}
fn run_model_name(settings: &SettingsLayer) -> Option<String> {
resolve_run(settings)
.model
.name
.as_ref()
.map(fabro_types::settings::InterpString::as_source)
}
fn run_model_provider(settings: &SettingsLayer) -> Option<String> {
resolve_run(settings)
.model
.provider
.as_ref()
.map(fabro_types::settings::InterpString::as_source)
}
fn run_inputs(settings: &SettingsLayer) -> &std::collections::HashMap<String, toml::Value> {
settings
.run
.as_ref()
.and_then(|run| run.inputs.as_ref())
.expect("run.inputs")
}
fn run_sandbox(settings: &SettingsLayer) -> &fabro_types::settings::run::RunSandboxLayer {
settings
.run
.as_ref()
.and_then(|run| run.sandbox.as_ref())
.expect("run.sandbox")
}
fn run_checkpoint(settings: &SettingsLayer) -> &fabro_types::settings::run::RunCheckpointLayer {
settings
.run
.as_ref()
.and_then(|run| run.checkpoint.as_ref())
.expect("run.checkpoint")
}
fn run_hooks(settings: &SettingsLayer) -> &[fabro_types::settings::run::HookEntry] {
settings
.run
.as_ref()
.map_or(&[], |run| run.hooks.as_slice())
}
fn run_agent_mcps(
settings: &SettingsLayer,
) -> &std::collections::HashMap<String, fabro_types::settings::run::McpEntryLayer> {
settings
.run
.as_ref()
.and_then(|run| run.agent.as_ref())
.map(|agent| &agent.mcps)
.expect("run.agent.mcps")
}
fn auto_approve_enabled(settings: &SettingsLayer) -> bool {
resolve_run(settings).execution.approval == fabro_types::settings::run::ApprovalMode::Auto
}
fn run_prepare_commands(settings: &SettingsLayer) -> Vec<String> {
resolve_run(settings).prepare.commands
}
fn server_storage_root(settings: &SettingsLayer) -> String {
resolve_server(settings).storage.root.as_source()
}
fn server_settings_fixture() -> SettingsLayer {
parse_settings_layer(
r#"
_version = 1
[server.storage]
root = "/srv/fabro-server"
[run.model]
name = "server-model"
provider = "openai"
[run.inputs]
server_only = "1"
shared = "server"
"#,
)
.expect("server settings fixture should parse")
}
fn server_settings_body(settings: &SettingsLayer) -> String {
serde_json::to_string(settings).expect("settings payload should serialize")
}
/// Set up home config and project config for settings command tests.
/// Uses `context.home_dir` for the home directory. Returns project tempdir.
fn setup_settings_fixture(context: &fabro_test::TestContext) -> tempfile::TempDir {
context.write_home(
".fabro/settings.toml",
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "cli-model"
provider = "openai"
[run.inputs]
cli_only = "1"
shared = "cli"
[run.checkpoint]
exclude_globs = ["cli-only", "shared"]
[[run.hooks]]
id = "shared"
name = "shared"
event = "run_start"
script = "echo cli"
[run.agent.mcps.shared]
type = "stdio"
command = ["echo", "cli"]
[run.sandbox]
provider = "daytona"
[run.sandbox.env]
CLI_ONLY = "1"
SHARED = "cli"
[run.sandbox.daytona]
[run.sandbox.daytona.labels]
cli_only = "1"
shared = "cli"
"#,
);
let project = tempfile::tempdir().unwrap();
std::fs::write(
project.path().join("fabro.toml"),
r#"
_version = 1
[project]
directory = "fabro"
[run.model]
name = "project-model"
[run.inputs]
project_only = "1"
shared = "project"
[[run.hooks]]
id = "project"
name = "project"
event = "run_complete"
script = "echo project"
"#,
)
.unwrap();
let workflow_dir = project.path().join("fabro").join("workflows").join("demo");
std::fs::create_dir_all(&workflow_dir).unwrap();
std::fs::write(
workflow_dir.join("workflow.toml"),
r#"
_version = 1
[run]
goal = "demo goal"
[run.model]
name = "run-model"
provider = "anthropic"
[run.inputs]
run_only = "1"
shared = "run"
[run.checkpoint]
exclude_globs = ["run-only", "shared"]
[[run.hooks]]
id = "shared"
name = "shared"
event = "run_start"
script = "echo run"
[[run.hooks]]
id = "run-only"
name = "run-only"
event = "run_complete"
script = "echo run-only"
[run.agent.mcps.shared]
type = "stdio"
command = ["echo", "run"]
[run.agent.mcps.run_only]
type = "stdio"
command = ["echo", "run-only"]
[run.sandbox.env]
RUN_ONLY = "1"
SHARED = "run"
[run.sandbox.daytona]
[run.sandbox.daytona.labels]
run_only = "1"
shared = "run"
"#,
)
.unwrap();
std::fs::write(
project.path().join("standalone.fabro"),
"digraph Test { start -> end }",
)
.unwrap();
project
}
/// Set up an external workflow fixture with a custom storage_dir in settings.toml.
/// Returns (project_tempdir, storage_dir_path).
fn setup_external_workflow_fixture(
context: &mut fabro_test::TestContext,
) -> (tempfile::TempDir, PathBuf) {
let storage_dir = context.home_dir.join("fabro-data");
context.manage_storage_dir(&storage_dir);
context.write_home(
".fabro/settings.toml",
format!(
r#"
_version = 1
[server.storage]
root = "{}"
[run.execution]
approval = "auto"
[[run.prepare.steps]]
script = "cli-setup"
"#,
storage_dir.display()
),
);
let project = tempfile::tempdir().unwrap();
std::fs::write(
project.path().join("fabro.toml"),
r#"
_version = 1
[[run.prepare.steps]]
script = "project-setup"
[run.sandbox]
preserve = true
"#,
)
.unwrap();
std::fs::write(
project.path().join("workflow.fabro"),
r#"
digraph Test {
start [shape=Mdiamond, label="Start"]
exit [shape=Msquare, label="Exit"]
start -> exit
}
"#,
)
.unwrap();
std::fs::write(
project.path().join("workflow.toml"),
r#"
_version = 1
[workflow]
graph = "workflow.fabro"
[run]
goal = "Ship it"
[run.model]
name = "claude-sonnet-4-6"
[[run.prepare.steps]]
script = "workflow-setup"
"#,
)
.unwrap();
(project, storage_dir)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[test]
fn settings_local_merges_cli_and_project_defaults() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let output = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let cfg = parse_settings(&output);
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
assert_eq!(run_model_provider(&cfg).as_deref(), Some("openai"));
assert_eq!(run_goal_inline(&cfg).as_deref(), None);
assert_eq!(resolve_project(&cfg).directory, "fabro");
// v2 R22: run.inputs replaces the inherited map wholesale rather than
// merging by key, so the project layer wipes out the CLI layer's inputs.
let vars = run_inputs(&cfg);
assert_eq!(vars.get("project_only").and_then(|v| v.as_str()), Some("1"));
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("project"));
assert!(
!vars.contains_key("cli_only"),
"run.inputs should replace across layers, not merge by key"
);
// v2 R71: provider-native maps such as run.sandbox.daytona.labels remain
// sticky merge-by-key, so CLI labels persist under the project layer.
let sandbox = run_sandbox(&cfg);
let labels = &sandbox.daytona.as_ref().expect("daytona").labels;
assert_eq!(labels.get("cli_only").map(String::as_str), Some("1"));
assert_eq!(labels.get("shared").map(String::as_str), Some("cli"));
}
#[test]
fn settings_local_workflow_name_applies_run_overlay_and_deep_merges() {
use fabro_types::settings::run::McpEntryLayer;
let context = test_context!();
let project = setup_settings_fixture(&context);
let output = context
.settings()
.current_dir(project.path())
.args(["--local", "demo"])
.assert()
.success()
.get_output()
.stdout
.clone();
let cfg = parse_settings(&output);
assert_eq!(run_goal_inline(&cfg).as_deref(), Some("demo goal"));
assert_eq!(run_model_name(&cfg).as_deref(), Some("run-model"));
assert_eq!(run_model_provider(&cfg).as_deref(), Some("anthropic"));
// v2 R22: run.inputs replaces wholesale, so the workflow layer wins
// over project and cli.
let vars = run_inputs(&cfg);
assert_eq!(vars.get("run_only").and_then(|v| v.as_str()), Some("1"));
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("run"));
// checkpoint.exclude_globs is a security/policy list: replace by default.
let checkpoint = run_checkpoint(&cfg);
assert_eq!(
checkpoint.exclude_globs,
vec!["run-only".to_string(), "shared".to_string()]
);
// Hooks: id-based replacement. The "shared" hook appears in both cli and
// workflow layers and resolves to the workflow entry; project and run-only
// contribute the other two ids.
let hooks = run_hooks(&cfg);
assert!(hooks.len() >= 2);
let shared_hook = hooks
.iter()
.find(|hook| hook.name.as_deref() == Some("shared"))
.expect("shared hook");
assert_eq!(
shared_hook
.script
.as_ref()
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("echo run")
);
assert!(
hooks
.iter()
.any(|hook| hook.name.as_deref() == Some("run-only"))
);
let mcps = run_agent_mcps(&cfg);
match mcps.get("shared").expect("shared mcp") {
McpEntryLayer::Stdio { command, .. } => {
let command = command.as_ref().expect("command");
let parts: Vec<String> = command
.iter()
.map(fabro_types::settings::InterpString::as_source)
.collect();
assert_eq!(parts, vec!["echo".to_string(), "run".to_string()]);
}
other => panic!("unexpected MCP transport: {other:?}"),
}
assert!(mcps.contains_key("run_only"));
// run.sandbox.daytona.labels stays sticky merge-by-key per R71.
let sandbox = run_sandbox(&cfg);
let labels = &sandbox.daytona.as_ref().expect("daytona").labels;
assert_eq!(labels.get("run_only").map(String::as_str), Some("1"));
assert_eq!(labels.get("shared").map(String::as_str), Some("run"));
// run.sandbox.env stays sticky merge-by-key per R71.
let env = &sandbox.env;
assert_eq!(
env.get("CLI_ONLY")
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("1")
);
assert_eq!(
env.get("RUN_ONLY")
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("1")
);
assert_eq!(
env.get("SHARED")
.map(fabro_types::settings::InterpString::as_source)
.as_deref(),
Some("run")
);
}
#[test]
fn settings_local_explicit_workflow_path_uses_workflow_project_layers() {
let mut context = test_context!();
let (project, _storage_dir) = setup_external_workflow_fixture(&mut context);
let cwd = tempfile::tempdir().unwrap();
let workflow = project.path().join("workflow.toml");
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml
let output = context
.settings()
.env_remove("FABRO_STORAGE_DIR")
.current_dir(cwd.path())
.args(["--local", workflow.to_str().unwrap()])
.assert()
.success()
.get_output()
.stdout
.clone();
let cfg = parse_settings(&output);
assert!(auto_approve_enabled(&cfg));
// v2 R30: run.prepare.steps replaces the whole ordered list across layers.
// The highest-precedence layer (workflow) wins.
assert_eq!(
run_prepare_commands(&cfg),
vec!["workflow-setup".to_string()]
);
assert_eq!(run_sandbox(&cfg).preserve, Some(true));
}
#[test]
fn create_explicit_workflow_path_uses_project_config_relative_to_workflow() {
let mut context = test_context!();
let (project, storage_dir) = setup_external_workflow_fixture(&mut context);
let cwd = tempfile::tempdir().unwrap();
let workflow = project.path().join("workflow.toml");
let run_id = unique_run_id();
// Remove FABRO_STORAGE_DIR so the CLI uses storage_dir from settings.toml
context
.command()
.env_remove("FABRO_STORAGE_DIR")
.current_dir(cwd.path())
.args([
"create",
"--dry-run",
"--model",
"gpt-5.2",
"--run-id",
run_id.as_str(),
workflow.to_str().unwrap(),
])
.assert()
.success();
let runs_dir = storage_dir.join("scratch");
let run_dir = std::fs::read_dir(&runs_dir)
.unwrap()
.flatten()
.map(|entry| entry.path())
.find(|path| {
path.is_dir()
&& path
.file_name()
.is_some_and(|name| name.to_string_lossy().ends_with(&run_id))
})
.unwrap_or_else(|| {
panic!(
"expected run directory for {run_id} under {}",
runs_dir.display()
)
});
let state = run_state(&run_dir);
let run_record =
serde_json::to_value(state.run.as_ref().expect("run record should exist")).unwrap();
assert_eq!(
run_record["settings"]["run"]["execution"]["approval"].as_str(),
Some("auto")
);
assert_eq!(
run_record["settings"]["server"]["storage"]["root"].as_str(),
Some(storage_dir.to_str().unwrap())
);
assert_eq!(
run_record["settings"]["run"]["sandbox"]["preserve"].as_bool(),
Some(true)
);
assert_eq!(
run_record["settings"]["run"]["model"]["name"].as_str(),
Some("gpt-5.2")
);
// v2 R30: run.prepare.steps replaces the whole ordered list across layers.
assert_eq!(
run_record["settings"]["run"]["prepare"]["steps"],
serde_json::json!([{"script": "workflow-setup"}])
);
}
#[test]
fn settings_fabro_path_matches_ambient_defaults() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let ambient = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
let graph = context
.settings()
.current_dir(project.path())
.args(["--local", "standalone.fabro"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(parse_settings(&graph), parse_settings(&ambient));
}
#[test]
fn settings_missing_run_config_errors() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let mut cmd = context.settings();
cmd.current_dir(project.path());
cmd.args(["--local", "missing.toml"]);
let output = cmd.output().expect("command should execute");
assert!(!output.status.success());
assert!(String::from_utf8_lossy(&output.stdout).trim().is_empty());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("error: Workflow not found:"),
"stderr should report missing workflow path, got:\n{stderr}"
);
assert!(
stderr.contains("missing.toml"),
"stderr should include missing workflow filename, got:\n{stderr}"
);
}
#[test]
fn settings_legacy_cli_config_warns_and_ignores_it() {
let context = test_context!();
let project = tempfile::tempdir().unwrap();
context.write_home(
".fabro/cli.toml",
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "legacy-model"
"#,
);
let assert = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success()
.stderr(predicate::str::contains("ignoring legacy config file"))
.stderr(predicate::str::contains("Rename it to"));
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(
resolve_cli(&cfg).output.verbosity,
fabro_types::settings::cli::OutputVerbosity::Normal
);
assert!(
cfg.run
.as_ref()
.and_then(|run| run.model.as_ref())
.is_none()
);
}
#[test]
fn settings_user_config_wins_over_legacy_cli_config() {
let context = test_context!();
let project = setup_settings_fixture(&context);
context.write_home(
".fabro/cli.toml",
r#"
_version = 1
[run.model]
name = "legacy-model"
[run.inputs]
shared = "legacy"
"#,
);
let assert = context
.settings()
.arg("--local")
.current_dir(project.path())
.assert()
.success()
.stderr(predicate::str::contains("ignoring legacy config file"));
let cfg = parse_settings(&assert.get_output().stdout);
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
let vars = run_inputs(&cfg);
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("project"));
}
#[test]
fn settings_uses_fabro_home_for_home_config_resolution() {
let context = test_context!();
let fabro_home = tempfile::tempdir().unwrap();
std::fs::write(
fabro_home.path().join("settings.toml"),
r#"
_version = 1
[cli.output]
verbosity = "verbose"
[run.model]
name = "from-fabro-home"
"#,
)
.unwrap();
let output = context
.settings()
.args(["--local", "--json"])
.env("FABRO_HOME", fabro_home.path())
.env_remove("FABRO_STORAGE_DIR")
.output()
.expect("command should execute");
assert!(
output.status.success(),
"settings command failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let cfg: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(cfg["cli"]["output"]["verbosity"].as_str(), Some("verbose"));
assert_eq!(
cfg["run"]["model"]["name"].as_str(),
Some("from-fabro-home")
);
}
#[test]
fn settings_rejects_server_url_flag() {
let context = test_context!();
context
.command()
.args(["--server-url", "https://cli.example.com", "settings"])
.assert()
.failure()
.stderr(predicate::str::contains(
"unexpected argument '--server-url' found",
));
}
#[test]
fn settings_rejects_storage_dir_flag() {
let context = test_context!();
context
.settings()
.args(["--storage-dir", "/tmp/fabro-settings"])
.assert()
.failure()
.stderr(predicate::str::contains(
"unexpected argument '--storage-dir' found",
));
}
#[test]
fn settings_rejects_local_and_server_combination() {
let context = test_context!();
context
.settings()
.args(["--local", "--server", "https://cli.example.com"])
.assert()
.failure()
.stderr(predicate::str::contains(
"the argument '--local' cannot be used with '--server <SERVER>'",
));
}
#[test]
fn settings_fetches_server_settings_and_merges_with_local_config() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let server = MockServer::start();
let server_settings = server_settings_fixture();
let mock = server.mock(|when, then| {
when.method("GET").path("/api/v1/settings");
then.status(200)
.header("Content-Type", "application/json")
.body(server_settings_body(&server_settings));
});
context.write_home(
".fabro/settings.toml",
format!(
r#"
_version = 1
[cli.target]
type = "http"
url = "{}/api/v1"
[cli.output]
verbosity = "verbose"
[run.model]
name = "cli-model"
provider = "openai"
[run.inputs]
cli_only = "1"
shared = "cli"
"#,
server.base_url()
),
);
let output = context
.settings()
.current_dir(project.path())
.assert()
.success()
.get_output()
.stdout
.clone();
mock.assert();
let cfg = parse_settings(&output);
assert_eq!(run_model_name(&cfg).as_deref(), Some("project-model"));
assert_eq!(run_model_provider(&cfg).as_deref(), Some("openai"));
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
assert_eq!(
resolve_cli(&cfg).output.verbosity,
fabro_types::settings::cli::OutputVerbosity::Verbose
);
// R22: run.inputs replaces wholesale across layers. Project is the
// highest-precedence layer that sets inputs, so project's vars win
// and server-side vars are discarded rather than merged.
let vars = run_inputs(&cfg);
assert_eq!(vars.get("project_only").and_then(|v| v.as_str()), Some("1"));
assert_eq!(vars.get("shared").and_then(|v| v.as_str()), Some("project"));
assert!(
!vars.contains_key("server_only"),
"v2 merge matrix replaces run.inputs wholesale; server_only should be dropped"
);
}
#[test]
fn settings_cli_server_target_overrides_configured_server_target() {
let context = test_context!();
let project = setup_settings_fixture(&context);
let configured_server = MockServer::start();
let configured_mock = configured_server.mock(|when, then| {
when.method("GET").path("/api/v1/settings");
then.status(500)
.body("configured-server-should-not-be-used");
});
let cli_server = MockServer::start();
let cli_server_settings = server_settings_fixture();
let cli_mock = cli_server.mock(|when, then| {
when.method("GET").path("/api/v1/settings");
then.status(200)
.header("Content-Type", "application/json")
.body(server_settings_body(&cli_server_settings));
});
context.write_home(
".fabro/settings.toml",
format!(
r#"
_version = 1
[cli.target]
type = "http"
url = "{}/api/v1"
[cli.output]
verbosity = "verbose"
"#,
configured_server.base_url()
),
);
let output = context
.settings()
.current_dir(project.path())
.args(["--server", &format!("{}/api/v1", cli_server.base_url())])
.assert()
.success()
.get_output()
.stdout
.clone();
cli_mock.assert();
configured_mock.assert_calls(0);
let cfg = parse_settings(&output);
assert_eq!(server_storage_root(&cfg), "/srv/fabro-server");
}
#[test]
fn settings_unreachable_http_target_fails_clearly() {
let context = test_context!();
let project = setup_settings_fixture(&context);
context
.settings()
.current_dir(project.path())
.args(["--server", "http://127.0.0.1:9"])
.assert()
.failure()
.stderr(
predicate::str::contains("retrieve_server_settings")
.or(predicate::str::contains("error sending request")),
);
}