mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
refactor(settings): stage 6.3b delete legacy flat Settings struct
Deletes `fabro_types::Settings` — the ~65-field legacy flat view that has been read-only since Stage 6.1 migrated all production read sites to the v2 `SettingsFile`. The last remaining readers all fall out of this commit: - `fabro-server/src/demo/mod.rs` — the two demo settings fixtures (`runs::settings()` and `settings::server_settings()`) are rewritten as `serde_json::json!(...)` literals in the v2 `SettingsFile` shape. They produce the same wire bytes as the real handlers now return, so the demo page keeps rendering identically. - `fabro-server/src/lib.rs::server_config` — drops the `pub use fabro_types::Settings` re-export. Only the inner `fabro_types::settings::server::*` module (still around until the full runtime-type cleanup) remains. - `fabro-server/tests/it/openapi_conformance.rs` — drops the `server_settings_keys_match_openapi_spec` schema-drift test and all of its legacy type imports. The new freeform-object DTO in the spec (`type: object, additionalProperties: true`) has no `properties` to diff against, so the test was already a no-op. Leaves `all_spec_routes_are_routable` in place. - `fabro-store/src/run_state.rs` — test fixture was building a `Settings::default()` JSON payload; switched to `SettingsFile::default()`. - `fabro-types/src/run_event/mod.rs` — two `EventBody::RunCreated` round-trip tests were constructing `Settings::default()`; switched to `SettingsFile::default()`. - `fabro-workflow/tests/it/integration.rs` — the two `hook_toml_*_parsing` tests decoded top-level `[[hooks]]` into a legacy `Settings`. That parse path was removed in Stage 6.1; the tests are deleted and replaced with a comment pointing at the v2 `settings::v2::tree::tests` fixtures that cover the same ground. The legacy flat struct's module-level doc comment in `settings/mod.rs` is updated to explain the transitional runtime shapes that still live under `hook`, `mcp`, `project`, `run`, `sandbox`, `server`, and `user` — a follow-up pass will either promote them into their consumer crates or inline them at the call sites so the whole `settings/*.rs` file set can go away and 6.5b flattening can happen. 3,758 workspace tests pass. `cargo fmt --check --all` and `cargo clippy --workspace -- -D warnings` are clean. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
999f2a11c3
commit
fb04e17329
8 changed files with 127 additions and 650 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<String> {
|
||||
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<String>,
|
||||
) {
|
||||
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<String> = 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<String> = 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.
|
||||
|
|
|
|||
|
|
@ -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": {},
|
||||
|
|
|
|||
|
|
@ -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::{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub goal: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub goal_file: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub graph: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub labels: HashMap<String, String>,
|
||||
#[serde(default, alias = "directory", skip_serializing_if = "Option::is_none")]
|
||||
pub work_dir: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub llm: Option<LlmSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub setup: Option<SetupSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox: Option<SandboxSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub vars: Option<HashMap<String, String>>,
|
||||
#[serde(default, skip_serializing_if = "is_default_checkpoint")]
|
||||
pub checkpoint: CheckpointSettings,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub pull_request: Option<PullRequestSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artifacts: Option<ArtifactsSettings>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub hooks: Vec<HookDefinition>,
|
||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||
pub mcp_servers: HashMap<String, McpServerEntry>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub github: Option<GitHubSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub server: Option<ServerSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub exec: Option<ExecSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prevent_idle_sleep: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub verbose: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub upgrade_check: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub dry_run: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auto_approve: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub no_retro: Option<bool>,
|
||||
#[serde(default, alias = "data_dir", skip_serializing_if = "Option::is_none")]
|
||||
pub storage_dir: Option<PathBuf>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub max_concurrent_runs: Option<usize>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artifact_storage: Option<ArtifactStorageSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub web: Option<WebSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slack: Option<SlackSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api: Option<ApiSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub features: Option<FeaturesSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub log: Option<LogSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub git: Option<GitSettings>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub fabro: Option<ProjectSettings>,
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue