mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
Materialize builtin settings defaults
Embed defaults.toml as a base settings layer and apply it when materializing effective settings and resolving typed settings. This also fixes partial CLI table merging so builtin fields survive higher-precedence overrides, and updates the affected CLI tests and snapshots.
This commit is contained in:
parent
48edaa009d
commit
ecbae9b732
16 changed files with 498 additions and 72 deletions
|
|
@ -585,8 +585,35 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
"run_dir": "[RUN_DIR]",
|
||||
"settings": {
|
||||
"workflow": {
|
||||
"graph": "workflow.fabro"
|
||||
},
|
||||
"cli": {
|
||||
"exec": {
|
||||
"prevent_idle_sleep": false
|
||||
},
|
||||
"output": {
|
||||
"format": "text",
|
||||
"verbosity": "normal"
|
||||
},
|
||||
"target": {
|
||||
"path": "[CLI_SOCKET]",
|
||||
"type": "unix"
|
||||
},
|
||||
"updates": {
|
||||
"check": true
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"session_sandboxes": false
|
||||
},
|
||||
"project": {
|
||||
"directory": "."
|
||||
},
|
||||
"run": {
|
||||
"execution": {
|
||||
"approval": "prompt",
|
||||
"mode": "normal",
|
||||
"retros": false
|
||||
},
|
||||
"goal": "Wait for approval",
|
||||
|
|
@ -594,15 +621,17 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"name": "gpt-5.4",
|
||||
"provider": "openai"
|
||||
},
|
||||
"prepare": {
|
||||
"timeout": "5m"
|
||||
},
|
||||
"sandbox": {
|
||||
"devcontainer": false,
|
||||
"local": {
|
||||
"worktree_mode": "clean"
|
||||
},
|
||||
"preserve": false,
|
||||
"provider": "local"
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"target": {
|
||||
"path": "[CLI_SOCKET]",
|
||||
"type": "unix"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workflow_slug": "human-gate",
|
||||
|
|
|
|||
|
|
@ -383,6 +383,32 @@ fn settings_local_merges_cli_and_project_defaults() {
|
|||
.clone();
|
||||
|
||||
let cfg = parse_settings(&output);
|
||||
assert_eq!(
|
||||
cfg.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.directory.as_deref()),
|
||||
Some(".")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.execution.as_ref())
|
||||
.and_then(|execution| execution.approval),
|
||||
Some(fabro_types::settings::run::ApprovalMode::Prompt)
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.sandbox.as_ref())
|
||||
.and_then(|sandbox| sandbox.provider.as_deref()),
|
||||
Some("daytona")
|
||||
);
|
||||
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);
|
||||
|
|
@ -852,6 +878,25 @@ shared = "cli"
|
|||
|
||||
mock.assert();
|
||||
let cfg = parse_settings(&output);
|
||||
assert_eq!(
|
||||
cfg.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.directory.as_deref()),
|
||||
Some(".")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.execution.as_ref())
|
||||
.and_then(|execution| execution.approval),
|
||||
Some(fabro_types::settings::run::ApprovalMode::Prompt)
|
||||
);
|
||||
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");
|
||||
|
|
|
|||
|
|
@ -932,9 +932,35 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
},
|
||||
"run_dir": "[RUN_DIR]",
|
||||
"settings": {
|
||||
"workflow": {
|
||||
"graph": "workflow.fabro"
|
||||
},
|
||||
"cli": {
|
||||
"exec": {
|
||||
"prevent_idle_sleep": false
|
||||
},
|
||||
"output": {
|
||||
"format": "text",
|
||||
"verbosity": "normal"
|
||||
},
|
||||
"target": {
|
||||
"path": "[CLI_SOCKET]",
|
||||
"type": "unix"
|
||||
},
|
||||
"updates": {
|
||||
"check": true
|
||||
}
|
||||
},
|
||||
"features": {
|
||||
"session_sandboxes": false
|
||||
},
|
||||
"project": {
|
||||
"directory": "."
|
||||
},
|
||||
"run": {
|
||||
"execution": {
|
||||
"approval": "auto",
|
||||
"mode": "normal",
|
||||
"retros": false
|
||||
},
|
||||
"goal": "Route through the default approval path",
|
||||
|
|
@ -942,15 +968,17 @@ fn json_run_implies_auto_approve_for_human_gates() {
|
|||
"name": "[LLM_MODEL]",
|
||||
"provider": "[LLM_PROVIDER]"
|
||||
},
|
||||
"prepare": {
|
||||
"timeout": "5m"
|
||||
},
|
||||
"sandbox": {
|
||||
"devcontainer": false,
|
||||
"local": {
|
||||
"worktree_mode": "clean"
|
||||
},
|
||||
"preserve": false,
|
||||
"provider": "local"
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"target": {
|
||||
"path": "[CLI_SOCKET]",
|
||||
"type": "unix"
|
||||
}
|
||||
}
|
||||
},
|
||||
"workflow_slug": "human-gate",
|
||||
|
|
|
|||
21
lib/crates/fabro-config/src/defaults.rs
Normal file
21
lib/crates/fabro-config/src/defaults.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
use std::sync::LazyLock;
|
||||
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
|
||||
use crate::merge::combine_files;
|
||||
use crate::parse_settings_layer;
|
||||
|
||||
static DEFAULTS_LAYER: LazyLock<SettingsLayer> = LazyLock::new(|| {
|
||||
parse_settings_layer(include_str!("defaults.toml"))
|
||||
.expect("embedded defaults.toml must parse as a valid SettingsLayer")
|
||||
});
|
||||
|
||||
#[must_use]
|
||||
pub fn defaults_layer() -> &'static SettingsLayer {
|
||||
&DEFAULTS_LAYER
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn apply_builtin_defaults(layer: SettingsLayer) -> SettingsLayer {
|
||||
combine_files(defaults_layer().clone(), layer)
|
||||
}
|
||||
54
lib/crates/fabro-config/src/defaults.toml
Normal file
54
lib/crates/fabro-config/src/defaults.toml
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# Fabro built-in defaults — embedded at compile time.
|
||||
# Dynamic and presence-gated defaults remain in Rust.
|
||||
_version = 1
|
||||
|
||||
[project]
|
||||
directory = "."
|
||||
|
||||
[workflow]
|
||||
graph = "workflow.fabro"
|
||||
|
||||
[run.execution]
|
||||
mode = "normal"
|
||||
approval = "prompt"
|
||||
retros = true
|
||||
|
||||
[run.prepare]
|
||||
timeout = "5m"
|
||||
|
||||
[run.sandbox]
|
||||
provider = "local"
|
||||
preserve = false
|
||||
devcontainer = false
|
||||
|
||||
[run.sandbox.local]
|
||||
worktree_mode = "clean"
|
||||
|
||||
[cli.output]
|
||||
format = "text"
|
||||
verbosity = "normal"
|
||||
|
||||
[cli.exec]
|
||||
prevent_idle_sleep = false
|
||||
|
||||
[cli.updates]
|
||||
check = true
|
||||
|
||||
[server.web]
|
||||
enabled = true
|
||||
url = "http://localhost:3000"
|
||||
|
||||
[server.scheduler]
|
||||
max_concurrent_runs = 5
|
||||
|
||||
[server.artifacts]
|
||||
provider = "local"
|
||||
prefix = "artifacts"
|
||||
|
||||
[server.slatedb]
|
||||
provider = "local"
|
||||
prefix = ""
|
||||
flush_interval = "1ms"
|
||||
|
||||
[features]
|
||||
session_sandboxes = false
|
||||
|
|
@ -14,7 +14,7 @@ use fabro_types::settings::run::{RunExecutionLayer, RunLayer};
|
|||
use fabro_types::settings::server::ServerLayer;
|
||||
|
||||
use crate::merge::combine_files;
|
||||
use crate::{Error, Result};
|
||||
use crate::{Error, Result, apply_builtin_defaults};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum EffectiveSettingsMode {
|
||||
|
|
@ -61,11 +61,10 @@ pub fn resolve_settings(
|
|||
user,
|
||||
} = layers;
|
||||
|
||||
match mode {
|
||||
EffectiveSettingsMode::LocalOnly => Ok(combine_files(
|
||||
combine_files(combine_files(user, project), workflow),
|
||||
args,
|
||||
)),
|
||||
let settings = match mode {
|
||||
EffectiveSettingsMode::LocalOnly => {
|
||||
combine_files(combine_files(combine_files(user, project), workflow), args)
|
||||
}
|
||||
EffectiveSettingsMode::RemoteServer | EffectiveSettingsMode::LocalDaemon => {
|
||||
let server_settings = server_settings.ok_or(Error::MissingServerSettings)?;
|
||||
// Owner-specific domains (cli, server) may only come from the
|
||||
|
|
@ -99,9 +98,11 @@ pub fn resolve_settings(
|
|||
let server = settings.server.get_or_insert_with(ServerLayer::default);
|
||||
server.storage = Some(server_root);
|
||||
}
|
||||
Ok(settings)
|
||||
settings
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(apply_builtin_defaults(settings))
|
||||
}
|
||||
|
||||
fn strip_owner_domains(file: &mut SettingsLayer) {
|
||||
|
|
@ -181,7 +182,8 @@ fn apply_local_daemon_overrides(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use fabro_types::settings::run::RunGoalLayer;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_types::settings::run::{ApprovalMode, RunGoalLayer};
|
||||
use fabro_types::settings::server::{ServerLayer, ServerSchedulerLayer, ServerStorageLayer};
|
||||
use fabro_types::settings::{InterpString, SettingsLayer};
|
||||
|
||||
|
|
@ -258,6 +260,28 @@ shared = "user"
|
|||
!inputs.contains_key("user_only"),
|
||||
"project.inputs should replace user.inputs wholesale"
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.directory.as_deref()),
|
||||
Some(".")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.execution.as_ref())
|
||||
.and_then(|execution| execution.approval),
|
||||
Some(ApprovalMode::Prompt)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -389,6 +413,21 @@ root = "/tmp/should-be-inert"
|
|||
.as_deref(),
|
||||
Some("project goal")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.sandbox.as_ref())
|
||||
.and_then(|sandbox| sandbox.provider.as_deref()),
|
||||
Some("local")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -431,5 +470,21 @@ root = "/tmp/should-be-inert"
|
|||
.and_then(|scheduler| scheduler.max_concurrent_runs),
|
||||
Some(7)
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.sandbox.as_ref())
|
||||
.and_then(|sandbox| sandbox.provider.as_deref()),
|
||||
Some("local")
|
||||
);
|
||||
assert_eq!(
|
||||
settings
|
||||
.cli
|
||||
.as_ref()
|
||||
.and_then(|cli| cli.output.as_ref())
|
||||
.and_then(|output| output.format),
|
||||
Some(OutputFormat::Text)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
extern crate self as fabro_config;
|
||||
|
||||
mod defaults;
|
||||
|
||||
pub mod effective_settings;
|
||||
pub mod envfile;
|
||||
pub mod error;
|
||||
|
|
@ -16,6 +18,7 @@ pub mod user;
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
pub use defaults::{apply_builtin_defaults, defaults_layer};
|
||||
pub use error::{Error, Result};
|
||||
use fabro_types::settings::{Settings, SettingsLayer};
|
||||
pub use fabro_util::path::expand_tilde;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use fabro_types::settings::cli::{
|
||||
CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliTargetLayer,
|
||||
CliExecAgentLayer, CliExecLayer, CliExecModelLayer, CliLayer, CliOutputLayer, CliTargetLayer,
|
||||
CliUpdatesLayer,
|
||||
};
|
||||
use fabro_types::settings::layer::SettingsLayer;
|
||||
use fabro_types::settings::project::ProjectLayer;
|
||||
|
|
@ -343,8 +344,8 @@ fn combine_cli(lower: CliLayer, higher: CliLayer) -> CliLayer {
|
|||
target: merge_option(lower.target, higher.target, combine_cli_target),
|
||||
auth: higher.auth.or(lower.auth),
|
||||
exec: merge_option(lower.exec, higher.exec, combine_cli_exec),
|
||||
output: higher.output.or(lower.output),
|
||||
updates: higher.updates.or(lower.updates),
|
||||
output: merge_option(lower.output, higher.output, combine_cli_output),
|
||||
updates: merge_option(lower.updates, higher.updates, combine_cli_updates),
|
||||
logging: higher.logging.or(lower.logging),
|
||||
}
|
||||
}
|
||||
|
|
@ -382,6 +383,19 @@ fn combine_cli_exec_agent(
|
|||
}
|
||||
}
|
||||
|
||||
fn combine_cli_output(lower: CliOutputLayer, higher: CliOutputLayer) -> CliOutputLayer {
|
||||
CliOutputLayer {
|
||||
format: higher.format.or(lower.format),
|
||||
verbosity: higher.verbosity.or(lower.verbosity),
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_cli_updates(lower: CliUpdatesLayer, higher: CliUpdatesLayer) -> CliUpdatesLayer {
|
||||
CliUpdatesLayer {
|
||||
check: higher.check.or(lower.check),
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------- server -------------------
|
||||
|
||||
fn combine_server(lower: ServerLayer, higher: ServerLayer) -> ServerLayer {
|
||||
|
|
@ -681,4 +695,52 @@ a = "replaced"
|
|||
assert_eq!(meta.len(), 1);
|
||||
assert_eq!(meta.get("a"), Some(&"replaced".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_output_merges_by_field() {
|
||||
let lower = parse(
|
||||
r#"
|
||||
[cli.output]
|
||||
format = "text"
|
||||
verbosity = "normal"
|
||||
"#,
|
||||
);
|
||||
let higher = parse(
|
||||
r#"
|
||||
[cli.output]
|
||||
verbosity = "verbose"
|
||||
"#,
|
||||
);
|
||||
|
||||
let merged = combine_files(lower, higher);
|
||||
let output = merged.cli.unwrap().output.unwrap();
|
||||
assert_eq!(
|
||||
output.format,
|
||||
Some(fabro_types::settings::cli::OutputFormat::Text)
|
||||
);
|
||||
assert_eq!(
|
||||
output.verbosity,
|
||||
Some(fabro_types::settings::cli::OutputVerbosity::Verbose)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_updates_merges_by_field() {
|
||||
let lower = parse(
|
||||
r#"
|
||||
[cli.updates]
|
||||
check = true
|
||||
"#,
|
||||
);
|
||||
let higher = parse(
|
||||
r#"
|
||||
[cli.logging]
|
||||
level = "debug"
|
||||
"#,
|
||||
);
|
||||
|
||||
let merged = combine_files(lower, higher);
|
||||
let updates = merged.cli.unwrap().updates.unwrap();
|
||||
assert_eq!(updates.check, Some(true));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use fabro_types::settings::cli::{
|
||||
CliAuthSettings, CliExecAgentSettings, CliExecLayer, CliExecModelSettings, CliExecSettings,
|
||||
CliLayer, CliLoggingSettings, CliOutputSettings, CliSettings, CliTargetLayer,
|
||||
CliTargetSettings, CliTargetTlsSettings, CliUpdatesSettings, OutputFormat, OutputVerbosity,
|
||||
CliTargetSettings, CliTargetTlsSettings, CliUpdatesSettings,
|
||||
};
|
||||
|
||||
use super::{ResolveError, require_interp};
|
||||
|
|
@ -18,19 +18,19 @@ pub fn resolve_cli(layer: &CliLayer, errors: &mut Vec<ResolveError>) -> CliSetti
|
|||
.output
|
||||
.as_ref()
|
||||
.and_then(|output| output.format)
|
||||
.unwrap_or(OutputFormat::Text),
|
||||
.expect("defaults.toml should provide cli.output.format"),
|
||||
verbosity: layer
|
||||
.output
|
||||
.as_ref()
|
||||
.and_then(|output| output.verbosity)
|
||||
.unwrap_or(OutputVerbosity::Normal),
|
||||
.expect("defaults.toml should provide cli.output.verbosity"),
|
||||
},
|
||||
updates: CliUpdatesSettings {
|
||||
check: layer
|
||||
.updates
|
||||
.as_ref()
|
||||
.and_then(|updates| updates.check)
|
||||
.unwrap_or(true),
|
||||
.expect("defaults.toml should provide cli.updates.check"),
|
||||
},
|
||||
logging: CliLoggingSettings {
|
||||
level: layer
|
||||
|
|
@ -62,12 +62,12 @@ fn resolve_target(
|
|||
}
|
||||
|
||||
fn resolve_exec(exec: Option<&CliExecLayer>) -> CliExecSettings {
|
||||
let Some(exec) = exec else {
|
||||
return CliExecSettings::default();
|
||||
};
|
||||
let exec = exec.expect("defaults.toml should provide cli.exec defaults");
|
||||
|
||||
CliExecSettings {
|
||||
prevent_idle_sleep: exec.prevent_idle_sleep.unwrap_or(false),
|
||||
prevent_idle_sleep: exec
|
||||
.prevent_idle_sleep
|
||||
.expect("defaults.toml should provide cli.exec.prevent_idle_sleep"),
|
||||
model: CliExecModelSettings {
|
||||
provider: exec.model.as_ref().and_then(|model| model.provider.clone()),
|
||||
name: exec.model.as_ref().and_then(|model| model.name.clone()),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ pub fn resolve_features(
|
|||
_errors: &mut Vec<ResolveError>,
|
||||
) -> FeaturesSettings {
|
||||
FeaturesSettings {
|
||||
session_sandboxes: layer.session_sandboxes.unwrap_or(false),
|
||||
session_sandboxes: layer
|
||||
.session_sandboxes
|
||||
.expect("defaults.toml should provide features.session_sandboxes"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ pub use run::resolve_run;
|
|||
pub use server::resolve_server;
|
||||
pub use workflow::resolve_workflow;
|
||||
|
||||
use crate::apply_builtin_defaults;
|
||||
|
||||
pub fn resolve(file: &SettingsLayer) -> Result<Settings, Vec<ResolveError>> {
|
||||
let file = apply_builtin_defaults(file.clone());
|
||||
let mut errors = Vec::new();
|
||||
let project_layer = file.project.clone().unwrap_or_default();
|
||||
let workflow_layer = file.workflow.clone().unwrap_or_default();
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ use fabro_types::settings::project::{ProjectLayer, ProjectSettings};
|
|||
|
||||
use super::ResolveError;
|
||||
|
||||
const DEFAULT_PROJECT_DIRECTORY: &str = ".";
|
||||
|
||||
pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) -> ProjectSettings {
|
||||
ProjectSettings {
|
||||
name: layer.name.clone(),
|
||||
|
|
@ -11,7 +9,7 @@ pub fn resolve_project(layer: &ProjectLayer, _errors: &mut Vec<ResolveError>) ->
|
|||
directory: layer
|
||||
.directory
|
||||
.clone()
|
||||
.unwrap_or_else(|| DEFAULT_PROJECT_DIRECTORY.to_string()),
|
||||
.expect("defaults.toml should provide project.directory"),
|
||||
metadata: layer.metadata.clone(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::run::{
|
||||
ApprovalMode, ArtifactsSettings, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSettings,
|
||||
ArtifactsSettings, DaytonaDockerfileLayer, DaytonaSandboxLayer, DaytonaSettings,
|
||||
DaytonaSnapshotSettings, DockerfileSource, GitAuthorSettings, HookAgentMarker, HookDefinition,
|
||||
HookEntry, HookTlsMode, HookType, InterviewProviderLayer, InterviewProviderSettings,
|
||||
InterviewsLayer, LocalSandboxSettings, McpEntryLayer, McpServerSettings, McpTransport,
|
||||
|
|
@ -8,7 +8,7 @@ use fabro_types::settings::run::{
|
|||
NotificationRouteLayer, NotificationRouteSettings, PullRequestSettings, RunAgentLayer,
|
||||
RunAgentSettings, RunArtifactsLayer, RunCheckpointLayer, RunCheckpointSettings,
|
||||
RunExecutionLayer, RunExecutionSettings, RunGitLayer, RunGitSettings, RunGoal, RunGoalLayer,
|
||||
RunInterviewsSettings, RunLayer, RunMode, RunModelLayer, RunModelSettings, RunPrepareLayer,
|
||||
RunInterviewsSettings, RunLayer, RunModelLayer, RunModelSettings, RunPrepareLayer,
|
||||
RunPrepareSettings, RunPullRequestLayer, RunSandboxLayer, RunSandboxSettings, RunScmLayer,
|
||||
RunScmSettings, RunSettings, ScmGitHubSettings, StringOrSplice, TlsMode,
|
||||
};
|
||||
|
|
@ -87,9 +87,7 @@ fn resolve_prepare(
|
|||
prepare: Option<&RunPrepareLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> RunPrepareSettings {
|
||||
let Some(prepare) = prepare else {
|
||||
return RunPrepareSettings::default();
|
||||
};
|
||||
let prepare = prepare.expect("defaults.toml should provide run.prepare defaults");
|
||||
|
||||
let mut commands = Vec::new();
|
||||
for (index, step) in prepare.steps.iter().enumerate() {
|
||||
|
|
@ -117,14 +115,18 @@ fn resolve_prepare(
|
|||
}
|
||||
|
||||
fn resolve_execution(execution: Option<&RunExecutionLayer>) -> RunExecutionSettings {
|
||||
let Some(execution) = execution else {
|
||||
return RunExecutionSettings::default();
|
||||
};
|
||||
let execution = execution.expect("defaults.toml should provide run.execution defaults");
|
||||
|
||||
RunExecutionSettings {
|
||||
mode: execution.mode.unwrap_or(RunMode::Normal),
|
||||
approval: execution.approval.unwrap_or(ApprovalMode::Prompt),
|
||||
retros: execution.retros.unwrap_or(true),
|
||||
mode: execution
|
||||
.mode
|
||||
.expect("defaults.toml should provide run.execution.mode"),
|
||||
approval: execution
|
||||
.approval
|
||||
.expect("defaults.toml should provide run.execution.approval"),
|
||||
retros: execution
|
||||
.retros
|
||||
.expect("defaults.toml should provide run.execution.retros"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,14 +142,12 @@ fn resolve_sandbox(
|
|||
sandbox: Option<&RunSandboxLayer>,
|
||||
errors: &mut Vec<ResolveError>,
|
||||
) -> RunSandboxSettings {
|
||||
let Some(sandbox) = sandbox else {
|
||||
return RunSandboxSettings::default();
|
||||
};
|
||||
let sandbox = sandbox.expect("defaults.toml should provide run.sandbox defaults");
|
||||
|
||||
let provider = sandbox
|
||||
.provider
|
||||
.clone()
|
||||
.unwrap_or_else(|| "local".to_string());
|
||||
.expect("defaults.toml should provide run.sandbox.provider");
|
||||
match provider.as_str() {
|
||||
"local" | "docker" | "daytona" => {}
|
||||
other => errors.push(ResolveError::Invalid {
|
||||
|
|
@ -158,8 +158,12 @@ fn resolve_sandbox(
|
|||
|
||||
RunSandboxSettings {
|
||||
provider,
|
||||
preserve: sandbox.preserve.unwrap_or(false),
|
||||
devcontainer: sandbox.devcontainer.unwrap_or(false),
|
||||
preserve: sandbox
|
||||
.preserve
|
||||
.expect("defaults.toml should provide run.sandbox.preserve"),
|
||||
devcontainer: sandbox
|
||||
.devcontainer
|
||||
.expect("defaults.toml should provide run.sandbox.devcontainer"),
|
||||
env: sandbox.env.clone(),
|
||||
local: resolve_local_sandbox(sandbox),
|
||||
daytona: sandbox.daytona.as_ref().map(resolve_daytona),
|
||||
|
|
@ -167,12 +171,15 @@ fn resolve_sandbox(
|
|||
}
|
||||
|
||||
fn resolve_local_sandbox(sandbox: &RunSandboxLayer) -> LocalSandboxSettings {
|
||||
let local = sandbox
|
||||
.local
|
||||
.as_ref()
|
||||
.expect("defaults.toml should provide run.sandbox.local");
|
||||
|
||||
LocalSandboxSettings {
|
||||
worktree_mode: sandbox
|
||||
.local
|
||||
.as_ref()
|
||||
.and_then(|local| local.worktree_mode)
|
||||
.unwrap_or_default(),
|
||||
worktree_mode: local
|
||||
.worktree_mode
|
||||
.expect("defaults.toml should provide run.sandbox.local.worktree_mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use fabro_types::settings::InterpString;
|
||||
use fabro_types::settings::server::{
|
||||
DiscordIntegrationSettings, GithubIntegrationSettings, GithubOauthSettings,
|
||||
|
|
@ -39,7 +37,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec<ResolveError>) -> Se
|
|||
.scheduler
|
||||
.as_ref()
|
||||
.and_then(|scheduler| scheduler.max_concurrent_runs)
|
||||
.unwrap_or(5),
|
||||
.expect("defaults.toml should provide server.scheduler.max_concurrent_runs"),
|
||||
},
|
||||
logging: ServerLoggingSettings {
|
||||
level: layer
|
||||
|
|
@ -107,11 +105,16 @@ fn resolve_tls(
|
|||
}
|
||||
|
||||
fn resolve_web(_api: Option<&ServerApiLayer>, layer: Option<&ServerWebLayer>) -> ServerWebSettings {
|
||||
let layer = layer.expect("defaults.toml should provide server.web defaults");
|
||||
|
||||
ServerWebSettings {
|
||||
enabled: layer.and_then(|web| web.enabled).unwrap_or(true),
|
||||
enabled: layer
|
||||
.enabled
|
||||
.expect("defaults.toml should provide server.web.enabled"),
|
||||
url: layer
|
||||
.and_then(|web| web.url.clone())
|
||||
.unwrap_or_else(|| InterpString::parse("http://localhost:3000")),
|
||||
.url
|
||||
.clone()
|
||||
.expect("defaults.toml should provide server.web.url"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -174,12 +177,12 @@ fn resolve_artifacts(
|
|||
) -> ServerArtifactsSettings {
|
||||
let provider = layer
|
||||
.and_then(|artifacts| artifacts.provider)
|
||||
.unwrap_or(ObjectStoreProvider::Local);
|
||||
.expect("defaults.toml should provide server.artifacts.provider");
|
||||
|
||||
ServerArtifactsSettings {
|
||||
prefix: layer
|
||||
.and_then(|artifacts| artifacts.prefix.clone())
|
||||
.unwrap_or_else(|| InterpString::parse("artifacts")),
|
||||
.expect("defaults.toml should provide server.artifacts.prefix"),
|
||||
store: resolve_object_store(
|
||||
provider,
|
||||
layer.and_then(|artifacts| artifacts.local.as_ref()),
|
||||
|
|
@ -198,12 +201,12 @@ fn resolve_slatedb(
|
|||
) -> ServerSlateDbSettings {
|
||||
let provider = layer
|
||||
.and_then(|slatedb| slatedb.provider)
|
||||
.unwrap_or(ObjectStoreProvider::Local);
|
||||
.expect("defaults.toml should provide server.slatedb.provider");
|
||||
|
||||
ServerSlateDbSettings {
|
||||
prefix: layer
|
||||
.and_then(|slatedb| slatedb.prefix.clone())
|
||||
.unwrap_or_else(|| InterpString::parse("")),
|
||||
.expect("defaults.toml should provide server.slatedb.prefix"),
|
||||
store: resolve_object_store(
|
||||
provider,
|
||||
layer.and_then(|slatedb| slatedb.local.as_ref()),
|
||||
|
|
@ -214,7 +217,8 @@ fn resolve_slatedb(
|
|||
),
|
||||
flush_interval: layer
|
||||
.and_then(|slatedb| slatedb.flush_interval)
|
||||
.map_or_else(|| Duration::from_millis(1), |duration| duration.as_std()),
|
||||
.map(|duration| duration.as_std())
|
||||
.expect("defaults.toml should provide server.slatedb.flush_interval"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ use fabro_types::settings::workflow::{WorkflowLayer, WorkflowSettings};
|
|||
|
||||
use super::ResolveError;
|
||||
|
||||
const DEFAULT_WORKFLOW_GRAPH: &str = "workflow.fabro";
|
||||
|
||||
pub fn resolve_workflow(
|
||||
layer: &WorkflowLayer,
|
||||
_errors: &mut Vec<ResolveError>,
|
||||
|
|
@ -14,7 +12,7 @@ pub fn resolve_workflow(
|
|||
graph: layer
|
||||
.graph
|
||||
.clone()
|
||||
.unwrap_or_else(|| DEFAULT_WORKFLOW_GRAPH.to_string()),
|
||||
.expect("defaults.toml should provide workflow.graph"),
|
||||
metadata: layer.metadata.clone(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
117
lib/crates/fabro-config/tests/defaults.rs
Normal file
117
lib/crates/fabro-config/tests/defaults.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
use fabro_config::{apply_builtin_defaults, defaults_layer, parse_settings_layer, resolve};
|
||||
use fabro_types::settings::SettingsLayer;
|
||||
use fabro_types::settings::cli::OutputFormat;
|
||||
use fabro_types::settings::run::{ApprovalMode, RunMode, WorktreeMode};
|
||||
use fabro_types::settings::server::ObjectStoreProvider;
|
||||
|
||||
fn parse(source: &str) -> SettingsLayer {
|
||||
parse_settings_layer(source).expect("fixture should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_defaults_parse_successfully() {
|
||||
let defaults = defaults_layer();
|
||||
|
||||
assert_eq!(
|
||||
defaults
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.directory.as_deref()),
|
||||
Some(".")
|
||||
);
|
||||
assert_eq!(
|
||||
defaults
|
||||
.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_builtin_defaults_materializes_expected_layer() {
|
||||
let layer = apply_builtin_defaults(SettingsLayer::default());
|
||||
|
||||
assert_eq!(
|
||||
layer
|
||||
.project
|
||||
.as_ref()
|
||||
.and_then(|project| project.directory.as_deref()),
|
||||
Some(".")
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.workflow
|
||||
.as_ref()
|
||||
.and_then(|workflow| workflow.graph.as_deref()),
|
||||
Some("workflow.fabro")
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.execution.as_ref())
|
||||
.and_then(|execution| execution.mode),
|
||||
Some(RunMode::Normal)
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.execution.as_ref())
|
||||
.and_then(|execution| execution.approval),
|
||||
Some(ApprovalMode::Prompt)
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.run
|
||||
.as_ref()
|
||||
.and_then(|run| run.sandbox.as_ref())
|
||||
.and_then(|sandbox| sandbox.local.as_ref())
|
||||
.and_then(|local| local.worktree_mode),
|
||||
Some(WorktreeMode::Clean)
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.cli
|
||||
.as_ref()
|
||||
.and_then(|cli| cli.output.as_ref())
|
||||
.and_then(|output| output.format),
|
||||
Some(OutputFormat::Text)
|
||||
);
|
||||
assert_eq!(
|
||||
layer
|
||||
.server
|
||||
.as_ref()
|
||||
.and_then(|server| server.artifacts.as_ref())
|
||||
.and_then(|artifacts| artifacts.provider),
|
||||
Some(ObjectStoreProvider::Local)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_empty_settings_still_produces_valid_defaults() {
|
||||
let settings = resolve(&SettingsLayer::default()).expect("empty settings should resolve");
|
||||
|
||||
assert_eq!(settings.project.directory, ".");
|
||||
assert_eq!(settings.workflow.graph, "workflow.fabro");
|
||||
assert_eq!(settings.run.execution.mode, RunMode::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn higher_precedence_values_override_builtin_defaults() {
|
||||
let layer = parse(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.execution]
|
||||
mode = "dry_run"
|
||||
"#,
|
||||
);
|
||||
|
||||
let settings = resolve(&layer).expect("settings should resolve");
|
||||
|
||||
assert_eq!(settings.run.execution.mode, RunMode::DryRun);
|
||||
assert_eq!(settings.run.execution.approval, ApprovalMode::Prompt);
|
||||
assert_eq!(settings.workflow.graph, "workflow.fabro");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue