diff --git a/lib/crates/fabro-server/src/demo/mod.rs b/lib/crates/fabro-server/src/demo/mod.rs index 024a5244c..ae2c3923b 100644 --- a/lib/crates/fabro-server/src/demo/mod.rs +++ b/lib/crates/fabro-server/src/demo/mod.rs @@ -1325,59 +1325,39 @@ mod runs { } pub(super) fn settings() -> serde_json::Value { - serde_json::to_value(fabro_types::Settings { - version: Some(1), - goal: Some("Add rate limiting to auth endpoints".into()), - graph: Some("implement.fabro".into()), - work_dir: Some("/workspace/api-server".into()), - llm: Some(fabro_types::settings::run::LlmSettings { - model: Some("claude-opus-4-6".into()), - provider: Some("anthropic".into()), - fallbacks: None, - }), - setup: Some(fabro_types::settings::run::SetupSettings { - commands: vec!["bun install".into(), "bun run typecheck".into()], - timeout_ms: Some(120_000), - }), - sandbox: Some(fabro_types::settings::sandbox::SandboxSettings { - provider: Some("daytona".into()), - preserve: None, - devcontainer: None, - local: None, - daytona: Some(fabro_sandbox::daytona::DaytonaConfig { - auto_stop_interval: Some(60), - labels: Some(std::collections::HashMap::from([( - "project".into(), - "api-server".into(), - )])), - snapshot: Some(fabro_sandbox::daytona::DaytonaSnapshotConfig { - name: "api-server-dev".into(), - cpu: Some(4), - memory: Some(8), - disk: Some(10), - dockerfile: None, - }), - network: Some(fabro_sandbox::daytona::DaytonaNetwork::Block), - skip_clone: false, - }), - env: None, - }), - vars: Some(std::collections::HashMap::from([ - ( - "repo_url".into(), - "https://github.com/org/api-server".into(), - ), - ("branch".into(), "feature/rate-limiting".into()), - ])), - hooks: vec![], - checkpoint: Default::default(), - pull_request: None, - artifacts: None, - mcp_servers: Default::default(), - github: None, - ..Default::default() + // v2 SettingsFile shape — matches what /api/v1/runs/:id/settings + // returns in production, so the demo renders identically. + serde_json::json!({ + "_version": 1, + "run": { + "goal": "Add rate limiting to auth endpoints", + "working_dir": "/workspace/api-server", + "model": { + "provider": "anthropic", + "name": "claude-opus-4-6" + }, + "prepare": { + "steps": [ + { "command": ["bun", "install"] }, + { "command": ["bun", "run", "typecheck"] } + ], + "timeout": "120s" + }, + "sandbox": { + "provider": "daytona", + "daytona": { + "auto_stop_interval": 60, + "labels": { "project": "api-server" }, + "snapshot": { + "name": "api-server-dev", + "cpu": 4, + "memory": "8GB", + "disk": "10GB" + } + } + } + } }) - .unwrap() } } @@ -1489,68 +1469,64 @@ mod insights { } mod settings { - use fabro_types::Settings; - use fabro_types::settings::server::*; - pub(super) fn server_settings() -> serde_json::Value { - serde_json::to_value(Settings { - storage_dir: Some("/home/fabro/.fabro".into()), - max_concurrent_runs: Some(10), - web: Some(WebSettings { - enabled: true, - url: "https://fabro.example.com".into(), - auth: AuthSettings { - provider: AuthProvider::Github, - allowed_usernames: vec!["brynary".into(), "alice".into()], + // v2 SettingsFile shape — matches what /api/v1/settings returns in + // production, so the demo renders identically. + serde_json::json!({ + "_version": 1, + "server": { + "storage": { + "root": "/home/fabro/.fabro" }, - }), - api: Some(ApiSettings { - base_url: "https://api.fabro.example.com".into(), - authentication_strategies: vec![ApiAuthStrategy::Jwt], - tls: None, - }), - git: Some(GitSettings { - provider: GitProvider::Github, - app_id: Some("12345".into()), - client_id: Some("Iv1.abc123".into()), - slug: Some("fabro-dev".into()), - author: Default::default(), - webhooks: None, - }), - features: Some(FeaturesSettings { - session_sandboxes: false, - retros: false, - }), - log: Default::default(), - llm: Some(fabro_types::settings::run::LlmSettings { - model: Some("claude-sonnet".into()), - provider: Some("anthropic".into()), - fallbacks: None, - }), - setup: None, - sandbox: Some(fabro_types::settings::sandbox::SandboxSettings { - provider: Some("daytona".into()), - preserve: None, - devcontainer: None, - local: None, - daytona: Some(fabro_sandbox::daytona::DaytonaConfig { - auto_stop_interval: Some(60), - labels: None, - snapshot: None, - network: Some(fabro_sandbox::daytona::DaytonaNetwork::Block), - skip_clone: false, - }), - env: None, - }), - vars: None, - checkpoint: Default::default(), - pull_request: None, - artifacts: None, - hooks: vec![], - mcp_servers: Default::default(), - github: None, - ..Default::default() + "scheduler": { + "max_concurrent_runs": 10 + }, + "api": { + "url": "https://api.fabro.example.com" + }, + "web": { + "enabled": true, + "url": "https://fabro.example.com" + }, + "auth": { + "api": { + "jwt": { "enabled": true } + }, + "web": { + "allowed_usernames": ["brynary", "alice"], + "providers": { + "github": { + "enabled": true, + "client_id": "Iv1.abc123" + } + } + } + }, + "integrations": { + "github": { + "app_id": "12345", + "client_id": "Iv1.abc123", + "slug": "fabro-dev" + } + } + }, + "run": { + "model": { + "provider": "anthropic", + "name": "claude-sonnet" + }, + "sandbox": { + "provider": "daytona", + "daytona": { + "auto_stop_interval": 60, + "network": "block" + } + } + }, + "features": { + "session_sandboxes": false, + "retros": false + } }) - .unwrap() } } diff --git a/lib/crates/fabro-server/src/lib.rs b/lib/crates/fabro-server/src/lib.rs index a2465afdb..4a7da2c01 100644 --- a/lib/crates/fabro-server/src/lib.rs +++ b/lib/crates/fabro-server/src/lib.rs @@ -17,7 +17,6 @@ pub mod server; mod settings_view; pub mod static_files; pub mod server_config { - pub use fabro_types::Settings; pub use fabro_types::settings::server::*; } pub mod tls; diff --git a/lib/crates/fabro-server/tests/it/openapi_conformance.rs b/lib/crates/fabro-server/tests/it/openapi_conformance.rs index 2c41c72d2..da24d4b0b 100644 --- a/lib/crates/fabro-server/tests/it/openapi_conformance.rs +++ b/lib/crates/fabro-server/tests/it/openapi_conformance.rs @@ -1,4 +1,4 @@ -//! Conformance tests: spec ↔ router ↔ Rust struct consistency. +//! Conformance tests: spec ↔ router consistency. #![allow( clippy::absolute_paths, @@ -8,21 +8,11 @@ )] use super::helpers::test_app_state; -use std::collections::BTreeSet; use axum::body::Body; use axum::http::{Method, Request, StatusCode}; -use fabro_hooks::*; -use fabro_sandbox::daytona::*; use fabro_server::jwt_auth::AuthMode; use fabro_server::server::build_router; -use fabro_server::server_config::*; -use fabro_types::settings::run::*; -use fabro_types::settings::sandbox::SandboxSettings; -use fabro_types::settings::{ - ClientTlsSettings, ExecSettings, OutputFormat, PermissionLevel, ProjectSettings, - ServerSettings as UserServerSettings, -}; use tower::ServiceExt; fn load_spec() -> openapiv3::OpenAPI { @@ -105,332 +95,10 @@ async fn all_spec_routes_are_routable() { assert!(checked > 0, "No routes were checked — is the spec empty?"); } -// ── ServerConfig ↔ OpenAPI schema drift detection ────────────────────── - -/// Load the spec as serde_json::Value for schema introspection. -fn load_spec_json() -> serde_json::Value { - let spec_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .join("docs/api-reference/fabro-api.yaml"); - let text = std::fs::read_to_string(&spec_path).expect("read spec"); - serde_yaml::from_str(&text).expect("parse spec") -} - -/// Follow a `$ref` pointer, or return the value unchanged. -fn resolve_ref<'a>( - value: &'a serde_json::Value, - root: &'a serde_json::Value, -) -> &'a serde_json::Value { - match value.get("$ref").and_then(|v| v.as_str()) { - Some(ref_str) => { - let mut cur = root; - for seg in ref_str.trim_start_matches("#/").split('/') { - cur = &cur[seg]; - } - cur - } - None => value, - } -} - -/// Collect property names from an OpenAPI schema object. -fn spec_keys(schema: &serde_json::Value) -> BTreeSet { - schema - .get("properties") - .and_then(|p| p.as_object()) - .map(|m| m.keys().cloned().collect()) - .unwrap_or_default() -} - -/// Recursively compare serialized JSON keys against OpenAPI schema properties. -fn compare_schema( - path: &str, - json: &serde_json::Value, - schema: &serde_json::Value, - root: &serde_json::Value, - errors: &mut Vec, -) { - let obj = match json.as_object() { - Some(o) => o, - None => return, - }; - - // Skip pure-map schemas (additionalProperties without properties). - if schema.get("additionalProperties").is_some() && schema.get("properties").is_none() { - return; - } - - let json_keys: BTreeSet = obj.keys().cloned().collect(); - let schema_keys = spec_keys(schema); - - for key in json_keys.difference(&schema_keys) { - errors.push(format!( - "{path}.{key}: in Rust but missing from OpenAPI spec" - )); - } - for key in schema_keys.difference(&json_keys) { - errors.push(format!( - "{path}.{key}: in OpenAPI spec but missing from Rust" - )); - } - - let properties = match schema.get("properties").and_then(|p| p.as_object()) { - Some(p) => p, - None => return, - }; - - for key in json_keys.intersection(&schema_keys) { - let json_val = &obj[key]; - let prop_schema = resolve_ref(&properties[key], root); - - // Skip maps and union types. - if prop_schema.get("additionalProperties").is_some() || prop_schema.get("oneOf").is_some() { - continue; - } - - match json_val { - serde_json::Value::Object(_) => { - compare_schema( - &format!("{path}.{key}"), - json_val, - prop_schema, - root, - errors, - ); - } - serde_json::Value::Array(arr) => { - // Union keys across all array elements. - let union: BTreeSet = arr - .iter() - .filter_map(|e| e.as_object()) - .flat_map(|o| o.keys().cloned()) - .collect(); - if union.is_empty() { - continue; - } - let items = match prop_schema.get("items") { - Some(i) => resolve_ref(i, root), - None => continue, - }; - let synthetic = serde_json::Value::Object( - union - .into_iter() - .map(|k| (k, serde_json::Value::Null)) - .collect(), - ); - compare_schema(&format!("{path}.{key}[]"), &synthetic, items, root, errors); - } - _ => {} - } - } -} - -/// Build a Settings with every Option set to Some so all keys appear -/// in the serialized JSON. -fn fully_populated_server_config() -> Settings { - Settings { - version: Some(1), - goal: Some("default goal".into()), - goal_file: Some("/tmp/goal.txt".into()), - graph: Some("workflow.fabro".into()), - labels: std::collections::HashMap::from([("scope".into(), "server".into())]), - server: Some(UserServerSettings { - target: Some("https://server.example.com".into()), - tls: Some(ClientTlsSettings { - cert: "client-cert.pem".into(), - key: "client-key.pem".into(), - ca: "ca.pem".into(), - }), - }), - exec: Some(ExecSettings { - provider: Some("openai".into()), - model: Some("gpt-5.4".into()), - permissions: Some(PermissionLevel::ReadWrite), - output_format: Some(OutputFormat::Json), - }), - prevent_idle_sleep: Some(true), - verbose: Some(true), - upgrade_check: Some(false), - dry_run: Some(true), - auto_approve: Some(true), - no_retro: Some(true), - storage_dir: Some("/data".into()), - max_concurrent_runs: Some(10), - web: Some(WebSettings { - enabled: true, - url: "https://example.com".into(), - auth: AuthSettings { - provider: AuthProvider::Github, - allowed_usernames: vec!["user".into()], - }, - }), - api: Some(ApiSettings { - base_url: "https://api.example.com".into(), - authentication_strategies: vec![ApiAuthStrategy::Jwt], - tls: Some(TlsSettings { - cert: "c".into(), - key: "k".into(), - ca: "ca".into(), - }), - }), - git: Some(GitSettings { - provider: GitProvider::Github, - app_id: Some("123".into()), - client_id: Some("456".into()), - slug: Some("fabro".into()), - author: GitAuthorSettings { - name: Some("bot".into()), - email: Some("bot@x".into()), - }, - webhooks: Some(WebhookSettings { - strategy: WebhookStrategy::TailscaleFunnel, - }), - }), - features: Some(FeaturesSettings { - session_sandboxes: true, - retros: false, - }), - log: Some(LogSettings { - level: Some("debug".into()), - }), - work_dir: Some("/work".into()), - llm: Some(LlmSettings { - model: Some("m".into()), - provider: Some("p".into()), - fallbacks: Some(Default::default()), - }), - setup: Some(SetupSettings { - commands: vec!["echo hi".into()], - timeout_ms: Some(5000), - }), - sandbox: Some(SandboxSettings { - provider: Some("daytona".into()), - preserve: Some(true), - devcontainer: None, - local: None, - daytona: Some(DaytonaConfig { - auto_stop_interval: Some(60), - labels: Some(Default::default()), - snapshot: Some(DaytonaSnapshotConfig { - name: "snap".into(), - cpu: Some(2), - memory: Some(4), - disk: Some(10), - dockerfile: Some(DockerfileSource::Inline("FROM x".into())), - }), - network: Some(DaytonaNetwork::Block), - skip_clone: false, - }), - env: Some(Default::default()), - }), - vars: Some(Default::default()), - checkpoint: CheckpointSettings { - exclude_globs: vec!["**/node_modules/**".into()], - }, - pull_request: Some(PullRequestSettings { - enabled: true, - draft: false, - auto_merge: false, - merge_strategy: MergeStrategy::Squash, - }), - artifacts: Some(ArtifactsSettings { - include: vec!["test-results/**".into()], - }), - // One hook per HookType variant so the key union covers all fields. - hooks: vec![ - HookDefinition { - name: Some("cmd".into()), - event: HookEvent::RunStart, - command: Some("echo".into()), - hook_type: None, - matcher: Some("*".into()), - blocking: Some(true), - timeout_ms: Some(5000), - sandbox: Some(true), - }, - HookDefinition { - name: Some("http".into()), - event: HookEvent::RunStart, - command: None, - hook_type: Some(HookType::Http { - url: "http://x".into(), - headers: Some(Default::default()), - allowed_env_vars: vec!["X".into()], - tls: TlsMode::Verify, - }), - matcher: None, - blocking: None, - timeout_ms: None, - sandbox: None, - }, - HookDefinition { - name: Some("prompt".into()), - event: HookEvent::RunStart, - command: None, - hook_type: Some(HookType::Prompt { - prompt: "hi".into(), - model: Some("m".into()), - }), - matcher: None, - blocking: None, - timeout_ms: None, - sandbox: None, - }, - HookDefinition { - name: Some("agent".into()), - event: HookEvent::RunStart, - command: None, - hook_type: Some(HookType::Agent { - prompt: "hi".into(), - model: Some("m".into()), - max_tool_rounds: Some(5), - }), - matcher: None, - blocking: None, - timeout_ms: None, - sandbox: None, - }, - ], - mcp_servers: std::collections::HashMap::from([( - "test".into(), - fabro_types::settings::mcp::McpServerEntry { - transport: fabro_types::settings::mcp::McpTransport::Stdio { - command: vec!["echo".into()], - env: Default::default(), - }, - startup_timeout_secs: fabro_types::settings::mcp::default_startup_timeout_secs(), - tool_timeout_secs: fabro_types::settings::mcp::default_tool_timeout_secs(), - }, - )]), - github: Some(GitHubSettings { - permissions: std::collections::HashMap::from([("contents".into(), "read".into())]), - }), - fabro: Some(ProjectSettings { - root: "fabro".into(), - }), - ..Default::default() - } -} - -#[test] -fn server_settings_keys_match_openapi_spec() { - let settings = fully_populated_server_config(); - let json = serde_json::to_value(&settings).expect("serialize ServerSettings"); - let spec = load_spec_json(); - let schema = &spec["components"]["schemas"]["ServerSettings"]; - - let mut errors = Vec::new(); - compare_schema("ServerSettings", &json, schema, &spec, &mut errors); - - if !errors.is_empty() { - panic!( - "ServerSettings ↔ OpenAPI schema drift:\n {}", - errors.join("\n ") - ); - } -} +// Note: the earlier `server_settings_keys_match_openapi_spec` drift check +// was deleted in Stage 6.3b alongside the legacy flat `fabro_types::Settings` +// struct that it instantiated. The v2 `/api/v1/settings` and +// `/api/v1/runs/:id/settings` endpoints now return the freely-shaped +// `SettingsFile` tree which the OpenAPI spec declares as +// `type: object, additionalProperties: true`, so there is nothing to diff +// at the property-key level. diff --git a/lib/crates/fabro-store/src/run_state.rs b/lib/crates/fabro-store/src/run_state.rs index 6b6660342..c64ef0e20 100644 --- a/lib/crates/fabro-store/src/run_state.rs +++ b/lib/crates/fabro-store/src/run_state.rs @@ -599,9 +599,10 @@ mod tests { use super::{NodeState, RunProjection}; use crate::{EventEnvelope, EventPayload, StageId}; use fabro_types::run_event::{InterviewCompletedProps, InterviewOption, InterviewStartedProps}; + use fabro_types::settings::SettingsFile; use fabro_types::{ Checkpoint, EventBody, InterviewQuestionType, RunBlobId, RunControlAction, RunEvent, - Settings, fixtures, + fixtures, }; fn test_event(seq: u32, body: EventBody, node_id: Option<&str>) -> EventEnvelope { @@ -808,7 +809,7 @@ mod tests { "run_id": fixtures::RUN_1, "event": "run.created", "properties": { - "settings": Settings::default(), + "settings": SettingsFile::default(), "graph": { "name": "test", "nodes": {}, diff --git a/lib/crates/fabro-types/src/lib.rs b/lib/crates/fabro-types/src/lib.rs index 6749e5545..3f00da289 100644 --- a/lib/crates/fabro-types/src/lib.rs +++ b/lib/crates/fabro-types/src/lib.rs @@ -53,7 +53,7 @@ pub use run_event::{EventBody, RunEvent, RunNoticeLevel}; pub use run_id::RunId; pub use run_id::fixtures; pub use sandbox_record::SandboxRecord; -pub use settings::{ArtifactStorageBackend, ArtifactStorageSettings, Settings}; +pub use settings::{ArtifactStorageBackend, ArtifactStorageSettings}; pub use stage_id::StageId; pub use start::StartRecord; pub use status::{ diff --git a/lib/crates/fabro-types/src/run_event/mod.rs b/lib/crates/fabro-types/src/run_event/mod.rs index f4817e3ed..fce22ee3f 100644 --- a/lib/crates/fabro-types/src/run_event/mod.rs +++ b/lib/crates/fabro-types/src/run_event/mod.rs @@ -683,7 +683,8 @@ mod tests { use serde_json::json; - use crate::{Edge, Graph, Node, RunBlobId, Settings, fixtures}; + use crate::settings::SettingsFile; + use crate::{Edge, Graph, Node, RunBlobId, fixtures}; use super::*; @@ -729,7 +730,7 @@ mod tests { #[test] fn run_event_deserializes_adjacent_layout() { - let settings = Settings::default(); + let settings = SettingsFile::default(); let graph = Graph { name: "test".to_string(), nodes: HashMap::from([( @@ -774,7 +775,7 @@ mod tests { "run_id": fixtures::RUN_1, "event": "run.created", "properties": { - "settings": Settings::default(), + "settings": SettingsFile::default(), "graph": Graph::new("test"), "labels": {}, "run_dir": "/tmp/run", diff --git a/lib/crates/fabro-types/src/settings/mod.rs b/lib/crates/fabro-types/src/settings/mod.rs index b8438e881..5492f34be 100644 --- a/lib/crates/fabro-types/src/settings/mod.rs +++ b/lib/crates/fabro-types/src/settings/mod.rs @@ -1,27 +1,23 @@ -//! Legacy flat `Settings` shape plus the v2 namespaced schema. +//! v2 namespaced config schema plus transitional runtime shapes. //! //! The authoritative config schema lives in [`v2`] — it is the namespaced //! parse tree that `_version = 1` TOML files decode into. Value-language //! helpers, the merge matrix, and strict unknown-key validation all live //! there. //! -//! The flat [`Settings`] type and its submodules (`hook`, `mcp`, `project`, -//! `run`, `sandbox`, `server`, `user`) are the **runtime shapes** that -//! downstream crates (fabro-workflow, fabro-sandbox, fabro-mcp, -//! fabro-hooks) still consume at execution time. Stage 6.1 deleted the -//! `Settings` parse path; Stage 6.2 deleted the `bridge_to_old` -//! catch-all converter. Narrow v2→runtime helpers live in -//! [`v2::to_runtime`] and build these runtime shapes from specific v2 -//! subtrees on demand. +//! The submodules `hook`, `mcp`, `project`, `run`, `sandbox`, `server`, +//! and `user` still hold **runtime shapes** that downstream crates +//! (fabro-workflow, fabro-sandbox, fabro-mcp, fabro-hooks) consume at +//! execution time. Stage 6.1 deleted the flat `Settings` parse path; +//! Stage 6.2 deleted the `bridge_to_old` catch-all converter; Stage 6.3b +//! deleted the legacy flat `Settings` struct itself, its inherent +//! helpers, and its `Combine`-driven layering. Narrow v2→runtime helpers +//! live in [`v2::to_runtime`] and build these runtime shapes from +//! specific v2 subtrees on demand. //! -//! Stage 6.3 deletes these runtime types entirely in favor of v2-native -//! replacements, at which point this module and the helper modules -//! around it go away too. - -use std::collections::HashMap; -use std::path::PathBuf; - -use serde::{Deserialize, Serialize}; +//! A follow-up pass will either promote these runtime shapes into their +//! owning consumer crates or replace their call sites with v2-native +//! accessors, at which point this module goes away. pub mod hook; pub mod mcp; @@ -59,8 +55,8 @@ pub use user::{ClientTlsSettings, ExecSettings, OutputFormat, PermissionLevel, S // `fabro_types::settings::InterpString` / `fabro_types::settings::Duration` // without the `::v2::` prefix. The `v2` module itself stays until the // remaining legacy files under `settings/{project,run,server,...}.rs` -// are deleted in Stage 6.3, because the v2 submodules and the legacy -// submodules share those file names. +// are deleted in a follow-up pass, because the v2 submodules and the +// legacy submodules share those file names. pub use v2::{ CURRENT_VERSION, CliLayer, Duration, FeaturesLayer, InterpString, ModelRef, ParseDurationError, ParseError, ParseModelRefError, ParseSizeError, ProjectLayer, Provenance, ResolveEnvError, @@ -68,86 +64,3 @@ pub use v2::{ SpliceArray, SpliceArrayError, VersionError, WorkflowLayer, parse_settings_file, validate_version, }; - -fn is_default_checkpoint(c: &CheckpointSettings) -> bool { - c.exclude_globs.is_empty() -} - -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] -pub struct Settings { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub version: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub goal: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub goal_file: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub graph: Option, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub labels: HashMap, - #[serde(default, alias = "directory", skip_serializing_if = "Option::is_none")] - pub work_dir: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub llm: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub setup: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub vars: Option>, - #[serde(default, skip_serializing_if = "is_default_checkpoint")] - pub checkpoint: CheckpointSettings, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub pull_request: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub hooks: Vec, - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub mcp_servers: HashMap, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub server: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub exec: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prevent_idle_sleep: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub verbose: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub upgrade_check: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dry_run: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub auto_approve: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub no_retro: Option, - #[serde(default, alias = "data_dir", skip_serializing_if = "Option::is_none")] - pub storage_dir: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_concurrent_runs: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifact_storage: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub features: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub log: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub git: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub fabro: Option, -} - -// All inherent helpers on `Settings` are gone -- the v2 `SettingsFile` -// accessors in `settings::v2::accessors` are the single source of truth -// for reading merged configuration. The flat `Settings` struct itself -// lingers for the OpenAPI legacy `ServerSettings` response shape and a -// handful of demo-route payloads; Stage 6.6 finishes the deletion once -// the OpenAPI spec is rewritten to return v2 DTOs. diff --git a/lib/crates/fabro-workflow/tests/it/integration.rs b/lib/crates/fabro-workflow/tests/it/integration.rs index 383eeef3b..abf857568 100644 --- a/lib/crates/fabro-workflow/tests/it/integration.rs +++ b/lib/crates/fabro-workflow/tests/it/integration.rs @@ -28,7 +28,7 @@ use fabro_llm::provider::Provider; use fabro_store::{ArtifactStore, Database}; use fabro_types::settings::v2::SettingsFile; use fabro_types::settings::v2::run::{RunArtifactsLayer, RunLayer}; -use fabro_types::{RunEvent, RunId, Settings, StageId}; +use fabro_types::{RunEvent, RunId, StageId}; use fabro_validate::{Severity, validate, validate_or_raise}; use fabro_workflow::context::Context; use fabro_workflow::error::{FabroError, FailureSignatureExt}; @@ -8089,41 +8089,10 @@ async fn hook_config_merge_run_overrides_by_name() { assert_eq!(outcome.status, StageStatus::Success); } -// --- TOML config parsing integration --- - -#[test] -fn hook_toml_run_config_parsing() { - let toml = r#" -version = 1 -goal = "Test hooks in run config" -graph = "test.fabro" - -[[hooks]] -event = "stage_start" -command = "./scripts/pre-check.sh" -matcher = "agent_loop" -blocking = true -timeout_ms = 30000 -sandbox = false - -[[hooks]] -event = "run_complete" -command = "echo done" -"#; - - let cfg: Settings = toml::from_str(toml).unwrap(); - assert_eq!(cfg.hooks.len(), 2); - assert_eq!(cfg.hooks[0].event, fabro_hooks::HookEvent::StageStart); - assert_eq!(cfg.hooks[0].matcher.as_deref(), Some("agent_loop")); - assert!(cfg.hooks[0].is_blocking()); - assert!(!cfg.hooks[0].runs_in_sandbox()); - assert_eq!( - cfg.hooks[0].timeout(), - std::time::Duration::from_millis(30000) - ); - assert_eq!(cfg.hooks[1].event, fabro_hooks::HookEvent::RunComplete); - assert!(!cfg.hooks[1].is_blocking()); // RunComplete non-blocking by default -} +// The legacy `Settings`-based TOML parsing tests were deleted in Stage +// 6.3b. Hook TOML parsing now flows through the v2 `SettingsFile` path, +// with coverage in `fabro-types::settings::v2::tree::tests` and the +// fabro-cli integration tests under `cmd::config`. // --- Blocking vs non-blocking behavior --- @@ -8270,59 +8239,9 @@ async fn hook_sandbox_false_runs_on_host() { assert_eq!(std::fs::read_to_string(&marker).unwrap().trim(), "host"); } -// --- Prompt and Agent hook TOML parsing --- - -#[test] -fn hook_toml_prompt_and_agent_parsing() { - let toml = r#" -version = 1 -goal = "Test prompt/agent hooks" -graph = "test.fabro" - -[[hooks]] -event = "stage_start" -type = "prompt" -prompt = "Should this stage proceed?" -model = "haiku" - -[[hooks]] -event = "run_complete" -type = "agent" -prompt = "Verify all tests pass." -model = "sonnet" -max_tool_rounds = 10 -timeout_ms = 120000 -"#; - - let cfg: Settings = toml::from_str(toml).unwrap(); - assert_eq!(cfg.hooks.len(), 2); - - // Prompt hook - assert_eq!(cfg.hooks[0].event, fabro_hooks::HookEvent::StageStart); - assert!(matches!( - cfg.hooks[0].resolved_hook_type().as_deref(), - Some(fabro_hooks::HookType::Prompt { prompt, model }) - if prompt == "Should this stage proceed?" && *model == Some("haiku".into()) - )); - assert_eq!( - cfg.hooks[0].timeout(), - std::time::Duration::from_millis(30000) - ); - - // Agent hook - assert_eq!(cfg.hooks[1].event, fabro_hooks::HookEvent::RunComplete); - assert!(matches!( - cfg.hooks[1].resolved_hook_type().as_deref(), - Some(fabro_hooks::HookType::Agent { prompt, model, max_tool_rounds }) - if prompt == "Verify all tests pass." - && *model == Some("sonnet".into()) - && *max_tool_rounds == Some(10) - )); - assert_eq!( - cfg.hooks[1].timeout(), - std::time::Duration::from_millis(120000) - ); -} +// Prompt and Agent hook TOML parsing: the legacy `Settings`-based +// variant of this test was deleted in Stage 6.3b; v2 coverage lives in +// `fabro-types::settings::v2::tree::tests`. // --- Events emitted correctly alongside hooks ---