mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-14 23:22:51 +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 {
|
pub(super) fn settings() -> serde_json::Value {
|
||||||
serde_json::to_value(fabro_types::Settings {
|
// v2 SettingsFile shape — matches what /api/v1/runs/:id/settings
|
||||||
version: Some(1),
|
// returns in production, so the demo renders identically.
|
||||||
goal: Some("Add rate limiting to auth endpoints".into()),
|
serde_json::json!({
|
||||||
graph: Some("implement.fabro".into()),
|
"_version": 1,
|
||||||
work_dir: Some("/workspace/api-server".into()),
|
"run": {
|
||||||
llm: Some(fabro_types::settings::run::LlmSettings {
|
"goal": "Add rate limiting to auth endpoints",
|
||||||
model: Some("claude-opus-4-6".into()),
|
"working_dir": "/workspace/api-server",
|
||||||
provider: Some("anthropic".into()),
|
"model": {
|
||||||
fallbacks: None,
|
"provider": "anthropic",
|
||||||
}),
|
"name": "claude-opus-4-6"
|
||||||
setup: Some(fabro_types::settings::run::SetupSettings {
|
},
|
||||||
commands: vec!["bun install".into(), "bun run typecheck".into()],
|
"prepare": {
|
||||||
timeout_ms: Some(120_000),
|
"steps": [
|
||||||
}),
|
{ "command": ["bun", "install"] },
|
||||||
sandbox: Some(fabro_types::settings::sandbox::SandboxSettings {
|
{ "command": ["bun", "run", "typecheck"] }
|
||||||
provider: Some("daytona".into()),
|
],
|
||||||
preserve: None,
|
"timeout": "120s"
|
||||||
devcontainer: None,
|
},
|
||||||
local: None,
|
"sandbox": {
|
||||||
daytona: Some(fabro_sandbox::daytona::DaytonaConfig {
|
"provider": "daytona",
|
||||||
auto_stop_interval: Some(60),
|
"daytona": {
|
||||||
labels: Some(std::collections::HashMap::from([(
|
"auto_stop_interval": 60,
|
||||||
"project".into(),
|
"labels": { "project": "api-server" },
|
||||||
"api-server".into(),
|
"snapshot": {
|
||||||
)])),
|
"name": "api-server-dev",
|
||||||
snapshot: Some(fabro_sandbox::daytona::DaytonaSnapshotConfig {
|
"cpu": 4,
|
||||||
name: "api-server-dev".into(),
|
"memory": "8GB",
|
||||||
cpu: Some(4),
|
"disk": "10GB"
|
||||||
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()
|
|
||||||
})
|
})
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1489,68 +1469,64 @@ mod insights {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod settings {
|
mod settings {
|
||||||
use fabro_types::Settings;
|
|
||||||
use fabro_types::settings::server::*;
|
|
||||||
|
|
||||||
pub(super) fn server_settings() -> serde_json::Value {
|
pub(super) fn server_settings() -> serde_json::Value {
|
||||||
serde_json::to_value(Settings {
|
// v2 SettingsFile shape — matches what /api/v1/settings returns in
|
||||||
storage_dir: Some("/home/fabro/.fabro".into()),
|
// production, so the demo renders identically.
|
||||||
max_concurrent_runs: Some(10),
|
serde_json::json!({
|
||||||
web: Some(WebSettings {
|
"_version": 1,
|
||||||
enabled: true,
|
"server": {
|
||||||
url: "https://fabro.example.com".into(),
|
"storage": {
|
||||||
auth: AuthSettings {
|
"root": "/home/fabro/.fabro"
|
||||||
provider: AuthProvider::Github,
|
|
||||||
allowed_usernames: vec!["brynary".into(), "alice".into()],
|
|
||||||
},
|
},
|
||||||
}),
|
"scheduler": {
|
||||||
api: Some(ApiSettings {
|
"max_concurrent_runs": 10
|
||||||
base_url: "https://api.fabro.example.com".into(),
|
},
|
||||||
authentication_strategies: vec![ApiAuthStrategy::Jwt],
|
"api": {
|
||||||
tls: None,
|
"url": "https://api.fabro.example.com"
|
||||||
}),
|
},
|
||||||
git: Some(GitSettings {
|
"web": {
|
||||||
provider: GitProvider::Github,
|
"enabled": true,
|
||||||
app_id: Some("12345".into()),
|
"url": "https://fabro.example.com"
|
||||||
client_id: Some("Iv1.abc123".into()),
|
},
|
||||||
slug: Some("fabro-dev".into()),
|
"auth": {
|
||||||
author: Default::default(),
|
"api": {
|
||||||
webhooks: None,
|
"jwt": { "enabled": true }
|
||||||
}),
|
},
|
||||||
features: Some(FeaturesSettings {
|
"web": {
|
||||||
session_sandboxes: false,
|
"allowed_usernames": ["brynary", "alice"],
|
||||||
retros: false,
|
"providers": {
|
||||||
}),
|
"github": {
|
||||||
log: Default::default(),
|
"enabled": true,
|
||||||
llm: Some(fabro_types::settings::run::LlmSettings {
|
"client_id": "Iv1.abc123"
|
||||||
model: Some("claude-sonnet".into()),
|
}
|
||||||
provider: Some("anthropic".into()),
|
}
|
||||||
fallbacks: None,
|
}
|
||||||
}),
|
},
|
||||||
setup: None,
|
"integrations": {
|
||||||
sandbox: Some(fabro_types::settings::sandbox::SandboxSettings {
|
"github": {
|
||||||
provider: Some("daytona".into()),
|
"app_id": "12345",
|
||||||
preserve: None,
|
"client_id": "Iv1.abc123",
|
||||||
devcontainer: None,
|
"slug": "fabro-dev"
|
||||||
local: None,
|
}
|
||||||
daytona: Some(fabro_sandbox::daytona::DaytonaConfig {
|
}
|
||||||
auto_stop_interval: Some(60),
|
},
|
||||||
labels: None,
|
"run": {
|
||||||
snapshot: None,
|
"model": {
|
||||||
network: Some(fabro_sandbox::daytona::DaytonaNetwork::Block),
|
"provider": "anthropic",
|
||||||
skip_clone: false,
|
"name": "claude-sonnet"
|
||||||
}),
|
},
|
||||||
env: None,
|
"sandbox": {
|
||||||
}),
|
"provider": "daytona",
|
||||||
vars: None,
|
"daytona": {
|
||||||
checkpoint: Default::default(),
|
"auto_stop_interval": 60,
|
||||||
pull_request: None,
|
"network": "block"
|
||||||
artifacts: None,
|
}
|
||||||
hooks: vec![],
|
}
|
||||||
mcp_servers: Default::default(),
|
},
|
||||||
github: None,
|
"features": {
|
||||||
..Default::default()
|
"session_sandboxes": false,
|
||||||
|
"retros": false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ pub mod server;
|
||||||
mod settings_view;
|
mod settings_view;
|
||||||
pub mod static_files;
|
pub mod static_files;
|
||||||
pub mod server_config {
|
pub mod server_config {
|
||||||
pub use fabro_types::Settings;
|
|
||||||
pub use fabro_types::settings::server::*;
|
pub use fabro_types::settings::server::*;
|
||||||
}
|
}
|
||||||
pub mod tls;
|
pub mod tls;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Conformance tests: spec ↔ router ↔ Rust struct consistency.
|
//! Conformance tests: spec ↔ router consistency.
|
||||||
|
|
||||||
#![allow(
|
#![allow(
|
||||||
clippy::absolute_paths,
|
clippy::absolute_paths,
|
||||||
|
|
@ -8,21 +8,11 @@
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use super::helpers::test_app_state;
|
use super::helpers::test_app_state;
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
use axum::body::Body;
|
use axum::body::Body;
|
||||||
use axum::http::{Method, Request, StatusCode};
|
use axum::http::{Method, Request, StatusCode};
|
||||||
use fabro_hooks::*;
|
|
||||||
use fabro_sandbox::daytona::*;
|
|
||||||
use fabro_server::jwt_auth::AuthMode;
|
use fabro_server::jwt_auth::AuthMode;
|
||||||
use fabro_server::server::build_router;
|
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;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
fn load_spec() -> openapiv3::OpenAPI {
|
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?");
|
assert!(checked > 0, "No routes were checked — is the spec empty?");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── ServerConfig ↔ OpenAPI schema drift detection ──────────────────────
|
// Note: the earlier `server_settings_keys_match_openapi_spec` drift check
|
||||||
|
// was deleted in Stage 6.3b alongside the legacy flat `fabro_types::Settings`
|
||||||
/// Load the spec as serde_json::Value for schema introspection.
|
// struct that it instantiated. The v2 `/api/v1/settings` and
|
||||||
fn load_spec_json() -> serde_json::Value {
|
// `/api/v1/runs/:id/settings` endpoints now return the freely-shaped
|
||||||
let spec_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
// `SettingsFile` tree which the OpenAPI spec declares as
|
||||||
.parent()
|
// `type: object, additionalProperties: true`, so there is nothing to diff
|
||||||
.unwrap()
|
// at the property-key level.
|
||||||
.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 ")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -599,9 +599,10 @@ mod tests {
|
||||||
use super::{NodeState, RunProjection};
|
use super::{NodeState, RunProjection};
|
||||||
use crate::{EventEnvelope, EventPayload, StageId};
|
use crate::{EventEnvelope, EventPayload, StageId};
|
||||||
use fabro_types::run_event::{InterviewCompletedProps, InterviewOption, InterviewStartedProps};
|
use fabro_types::run_event::{InterviewCompletedProps, InterviewOption, InterviewStartedProps};
|
||||||
|
use fabro_types::settings::SettingsFile;
|
||||||
use fabro_types::{
|
use fabro_types::{
|
||||||
Checkpoint, EventBody, InterviewQuestionType, RunBlobId, RunControlAction, RunEvent,
|
Checkpoint, EventBody, InterviewQuestionType, RunBlobId, RunControlAction, RunEvent,
|
||||||
Settings, fixtures,
|
fixtures,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn test_event(seq: u32, body: EventBody, node_id: Option<&str>) -> EventEnvelope {
|
fn test_event(seq: u32, body: EventBody, node_id: Option<&str>) -> EventEnvelope {
|
||||||
|
|
@ -808,7 +809,7 @@ mod tests {
|
||||||
"run_id": fixtures::RUN_1,
|
"run_id": fixtures::RUN_1,
|
||||||
"event": "run.created",
|
"event": "run.created",
|
||||||
"properties": {
|
"properties": {
|
||||||
"settings": Settings::default(),
|
"settings": SettingsFile::default(),
|
||||||
"graph": {
|
"graph": {
|
||||||
"name": "test",
|
"name": "test",
|
||||||
"nodes": {},
|
"nodes": {},
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ pub use run_event::{EventBody, RunEvent, RunNoticeLevel};
|
||||||
pub use run_id::RunId;
|
pub use run_id::RunId;
|
||||||
pub use run_id::fixtures;
|
pub use run_id::fixtures;
|
||||||
pub use sandbox_record::SandboxRecord;
|
pub use sandbox_record::SandboxRecord;
|
||||||
pub use settings::{ArtifactStorageBackend, ArtifactStorageSettings, Settings};
|
pub use settings::{ArtifactStorageBackend, ArtifactStorageSettings};
|
||||||
pub use stage_id::StageId;
|
pub use stage_id::StageId;
|
||||||
pub use start::StartRecord;
|
pub use start::StartRecord;
|
||||||
pub use status::{
|
pub use status::{
|
||||||
|
|
|
||||||
|
|
@ -683,7 +683,8 @@ mod tests {
|
||||||
|
|
||||||
use serde_json::json;
|
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::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
@ -729,7 +730,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn run_event_deserializes_adjacent_layout() {
|
fn run_event_deserializes_adjacent_layout() {
|
||||||
let settings = Settings::default();
|
let settings = SettingsFile::default();
|
||||||
let graph = Graph {
|
let graph = Graph {
|
||||||
name: "test".to_string(),
|
name: "test".to_string(),
|
||||||
nodes: HashMap::from([(
|
nodes: HashMap::from([(
|
||||||
|
|
@ -774,7 +775,7 @@ mod tests {
|
||||||
"run_id": fixtures::RUN_1,
|
"run_id": fixtures::RUN_1,
|
||||||
"event": "run.created",
|
"event": "run.created",
|
||||||
"properties": {
|
"properties": {
|
||||||
"settings": Settings::default(),
|
"settings": SettingsFile::default(),
|
||||||
"graph": Graph::new("test"),
|
"graph": Graph::new("test"),
|
||||||
"labels": {},
|
"labels": {},
|
||||||
"run_dir": "/tmp/run",
|
"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
|
//! The authoritative config schema lives in [`v2`] — it is the namespaced
|
||||||
//! parse tree that `_version = 1` TOML files decode into. Value-language
|
//! parse tree that `_version = 1` TOML files decode into. Value-language
|
||||||
//! helpers, the merge matrix, and strict unknown-key validation all live
|
//! helpers, the merge matrix, and strict unknown-key validation all live
|
||||||
//! there.
|
//! there.
|
||||||
//!
|
//!
|
||||||
//! The flat [`Settings`] type and its submodules (`hook`, `mcp`, `project`,
|
//! The submodules `hook`, `mcp`, `project`, `run`, `sandbox`, `server`,
|
||||||
//! `run`, `sandbox`, `server`, `user`) are the **runtime shapes** that
|
//! and `user` still hold **runtime shapes** that downstream crates
|
||||||
//! downstream crates (fabro-workflow, fabro-sandbox, fabro-mcp,
|
//! (fabro-workflow, fabro-sandbox, fabro-mcp, fabro-hooks) consume at
|
||||||
//! fabro-hooks) still consume at execution time. Stage 6.1 deleted the
|
//! execution time. Stage 6.1 deleted the flat `Settings` parse path;
|
||||||
//! `Settings` parse path; Stage 6.2 deleted the `bridge_to_old`
|
//! Stage 6.2 deleted the `bridge_to_old` catch-all converter; Stage 6.3b
|
||||||
//! catch-all converter. Narrow v2→runtime helpers live in
|
//! deleted the legacy flat `Settings` struct itself, its inherent
|
||||||
//! [`v2::to_runtime`] and build these runtime shapes from specific v2
|
//! helpers, and its `Combine`-driven layering. Narrow v2→runtime helpers
|
||||||
//! subtrees on demand.
|
//! 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
|
//! A follow-up pass will either promote these runtime shapes into their
|
||||||
//! replacements, at which point this module and the helper modules
|
//! owning consumer crates or replace their call sites with v2-native
|
||||||
//! around it go away too.
|
//! accessors, at which point this module goes away.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub mod hook;
|
pub mod hook;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
|
|
@ -59,8 +55,8 @@ pub use user::{ClientTlsSettings, ExecSettings, OutputFormat, PermissionLevel, S
|
||||||
// `fabro_types::settings::InterpString` / `fabro_types::settings::Duration`
|
// `fabro_types::settings::InterpString` / `fabro_types::settings::Duration`
|
||||||
// without the `::v2::` prefix. The `v2` module itself stays until the
|
// without the `::v2::` prefix. The `v2` module itself stays until the
|
||||||
// remaining legacy files under `settings/{project,run,server,...}.rs`
|
// remaining legacy files under `settings/{project,run,server,...}.rs`
|
||||||
// are deleted in Stage 6.3, because the v2 submodules and the legacy
|
// are deleted in a follow-up pass, because the v2 submodules and the
|
||||||
// submodules share those file names.
|
// legacy submodules share those file names.
|
||||||
pub use v2::{
|
pub use v2::{
|
||||||
CURRENT_VERSION, CliLayer, Duration, FeaturesLayer, InterpString, ModelRef, ParseDurationError,
|
CURRENT_VERSION, CliLayer, Duration, FeaturesLayer, InterpString, ModelRef, ParseDurationError,
|
||||||
ParseError, ParseModelRefError, ParseSizeError, ProjectLayer, Provenance, ResolveEnvError,
|
ParseError, ParseModelRefError, ParseSizeError, ProjectLayer, Provenance, ResolveEnvError,
|
||||||
|
|
@ -68,86 +64,3 @@ pub use v2::{
|
||||||
SpliceArray, SpliceArrayError, VersionError, WorkflowLayer, parse_settings_file,
|
SpliceArray, SpliceArrayError, VersionError, WorkflowLayer, parse_settings_file,
|
||||||
validate_version,
|
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_store::{ArtifactStore, Database};
|
||||||
use fabro_types::settings::v2::SettingsFile;
|
use fabro_types::settings::v2::SettingsFile;
|
||||||
use fabro_types::settings::v2::run::{RunArtifactsLayer, RunLayer};
|
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_validate::{Severity, validate, validate_or_raise};
|
||||||
use fabro_workflow::context::Context;
|
use fabro_workflow::context::Context;
|
||||||
use fabro_workflow::error::{FabroError, FailureSignatureExt};
|
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);
|
assert_eq!(outcome.status, StageStatus::Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- TOML config parsing integration ---
|
// The legacy `Settings`-based TOML parsing tests were deleted in Stage
|
||||||
|
// 6.3b. Hook TOML parsing now flows through the v2 `SettingsFile` path,
|
||||||
#[test]
|
// with coverage in `fabro-types::settings::v2::tree::tests` and the
|
||||||
fn hook_toml_run_config_parsing() {
|
// fabro-cli integration tests under `cmd::config`.
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Blocking vs non-blocking behavior ---
|
// --- 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");
|
assert_eq!(std::fs::read_to_string(&marker).unwrap().trim(), "host");
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Prompt and Agent hook TOML parsing ---
|
// Prompt and Agent hook TOML parsing: the legacy `Settings`-based
|
||||||
|
// variant of this test was deleted in Stage 6.3b; v2 coverage lives in
|
||||||
#[test]
|
// `fabro-types::settings::v2::tree::tests`.
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Events emitted correctly alongside hooks ---
|
// --- Events emitted correctly alongside hooks ---
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue