From 947db0713cc86e58e2682086df4c1370d007e673 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 29 Mar 2026 22:42:13 -0400 Subject: [PATCH] Snapshot run settings for execution --- .../fabro-cli/src/commands/run/detached.rs | 11 +- lib/crates/fabro-cli/src/shared/github.rs | 15 +- lib/crates/fabro-github/Cargo.toml | 2 +- lib/crates/fabro-github/src/lib.rs | 18 + lib/crates/fabro-server/src/serve.rs | 142 ++--- lib/crates/fabro-server/src/server.rs | 562 ++++++++++-------- lib/crates/fabro-server/src/sessions.rs | 24 +- lib/crates/fabro-server/tests/it/api.rs | 29 +- lib/crates/fabro-server/tests/it/helpers.rs | 12 - .../tests/it/openapi_conformance.rs | 4 +- .../fabro-server/tests/it/pagination.rs | 4 +- lib/crates/fabro-workflows/src/git.rs | 15 + .../src/handler/manager_loop.rs | 5 - .../fabro-workflows/src/lifecycle/git.rs | 9 +- .../fabro-workflows/src/operations/start.rs | 96 ++- .../fabro-workflows/src/pipeline/execute.rs | 2 +- .../src/pipeline/execute/tests.rs | 1 - .../fabro-workflows/src/pipeline/finalize.rs | 4 +- .../src/pipeline/initialize.rs | 1 - .../fabro-workflows/src/pipeline/retro.rs | 1 - lib/crates/fabro-workflows/src/run_options.rs | 8 +- .../tests/it/daytona_integration.rs | 6 - .../fabro-workflows/tests/it/integration.rs | 114 ---- 23 files changed, 478 insertions(+), 607 deletions(-) diff --git a/lib/crates/fabro-cli/src/commands/run/detached.rs b/lib/crates/fabro-cli/src/commands/run/detached.rs index a6a69e7c3..8af85814c 100644 --- a/lib/crates/fabro-cli/src/commands/run/detached.rs +++ b/lib/crates/fabro-cli/src/commands/run/detached.rs @@ -6,7 +6,6 @@ use fabro_config::FabroSettingsExt; use fabro_interview::FileInterviewer; use fabro_store::RuntimeState; use fabro_workflows::event::EventEmitter; -use fabro_workflows::git::GitAuthor; use fabro_workflows::operations::{ StartServices, open_or_hydrate_run, resume as resume_run, start as start_run, }; @@ -14,8 +13,6 @@ use fabro_workflows::records::{RunRecord, RunRecordExt}; use crate::shared; use crate::store; -use crate::user_config; - pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bool) -> Result<()> { let _ = fabro_proctitle::init(); @@ -24,7 +21,6 @@ pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bo }); let run_record = RunRecord::load(&run_dir)?; - let cli_settings = user_config::load_user_settings()?; let on_node: fabro_workflows::OnNodeCallback = Some({ let short_id = super::short_run_id(&run_record.run_id).to_string(); fabro_proctitle::set(&format!("fabro: {short_id}")); @@ -35,11 +31,7 @@ pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bo let store = store::build_store(&run_record.settings.storage_dir())?; let run_store = open_or_hydrate_run(store.as_ref(), &run_dir).await?; - let github_app = shared::github::build_github_app_credentials(cli_settings.app_id()); - let git_author = GitAuthor::from_options( - cli_settings.git_author().and_then(|a| a.name.clone()), - cli_settings.git_author().and_then(|a| a.email.clone()), - ); + let github_app = shared::github::build_github_app_credentials(run_record.settings.app_id()); let runtime_state = RuntimeState::new(&run_dir); let services = StartServices { @@ -51,7 +43,6 @@ pub(crate) async fn execute(run_dir: PathBuf, launcher_path: PathBuf, resume: bo runtime_state.interview_claim_path(), )), run_store, - git_author, github_app, on_node, registry_override: None, diff --git a/lib/crates/fabro-cli/src/shared/github.rs b/lib/crates/fabro-cli/src/shared/github.rs index 79b176e39..d7ca8fb39 100644 --- a/lib/crates/fabro-cli/src/shared/github.rs +++ b/lib/crates/fabro-cli/src/shared/github.rs @@ -1,18 +1,5 @@ -use base64::Engine as _; -use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use fabro_github::GitHubAppCredentials; pub(crate) fn build_github_app_credentials(app_id: Option<&str>) -> Option { - let app_id = app_id?; - let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?; - let private_key_pem = if raw.starts_with("-----") { - raw - } else { - let pem_bytes = BASE64_STANDARD.decode(&raw).ok()?; - String::from_utf8(pem_bytes).ok()? - }; - Some(GitHubAppCredentials { - app_id: app_id.to_string(), - private_key_pem, - }) + GitHubAppCredentials::from_env(app_id) } diff --git a/lib/crates/fabro-github/Cargo.toml b/lib/crates/fabro-github/Cargo.toml index 2c2c8469a..f98474f2c 100644 --- a/lib/crates/fabro-github/Cargo.toml +++ b/lib/crates/fabro-github/Cargo.toml @@ -20,7 +20,7 @@ jsonwebtoken.workspace = true chrono.workspace = true tracing.workspace = true tokio = { workspace = true } +base64.workspace = true [dev-dependencies] tokio = { workspace = true, features = ["test-util", "macros"] } -base64.workspace = true diff --git a/lib/crates/fabro-github/src/lib.rs b/lib/crates/fabro-github/src/lib.rs index dec94f05c..49cb6ac35 100644 --- a/lib/crates/fabro-github/src/lib.rs +++ b/lib/crates/fabro-github/src/lib.rs @@ -53,6 +53,24 @@ pub struct GitHubAppCredentials { pub private_key_pem: String, } +impl GitHubAppCredentials { + pub fn from_env(app_id: Option<&str>) -> Option { + let app_id = app_id?; + let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?; + let private_key_pem = if raw.starts_with("-----") { + raw + } else { + let pem_bytes = + base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &raw).ok()?; + String::from_utf8(pem_bytes).ok()? + }; + Some(Self { + app_id: app_id.to_string(), + private_key_pem, + }) + } +} + /// HTTP method used in GitHub API calls. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HttpMethod { diff --git a/lib/crates/fabro-server/src/serve.rs b/lib/crates/fabro-server/src/serve.rs index b178ba67a..9acaa3b5e 100644 --- a/lib/crates/fabro-server/src/serve.rs +++ b/lib/crates/fabro-server/src/serve.rs @@ -3,9 +3,7 @@ use std::sync::{Arc, RwLock}; use std::time::Duration; use fabro_config::server::{load_server_settings, resolve_storage_dir}; -use fabro_model::{Catalog, Provider}; use fabro_util::terminal::Styles; -use fabro_workflows::git::GitAuthor; use object_store::local::LocalFileSystem; use tokio::net::TcpListener; use tokio::time::interval; @@ -21,7 +19,6 @@ use crate::server::{build_router, create_app_state_with_store, spawn_scheduler}; use crate::tls::{ClientAuth, build_rustls_config, serve_tls}; use fabro_llm::client::Client as LlmClient; use fabro_sandbox::SandboxProvider; -use fabro_workflows::pipeline::LlmSpec; #[derive(Args)] pub struct ServeArgs { @@ -58,6 +55,27 @@ pub struct ServeArgs { pub config: Option, } +fn apply_serve_overrides( + base: &FabroSettings, + args: &ServeArgs, + dry_run_mode: bool, +) -> FabroSettings { + let mut settings = base.clone(); + if dry_run_mode { + settings.dry_run = Some(true); + } + if let Some(ref model) = args.model { + settings.llm.get_or_insert_default().model = Some(model.clone()); + } + if let Some(ref provider) = args.provider { + settings.llm.get_or_insert_default().provider = Some(provider.clone()); + } + if let Some(sandbox) = args.sandbox { + settings.sandbox.get_or_insert_default().provider = Some(sandbox.to_string()); + } + settings +} + /// Start the HTTP API server. /// /// # Errors @@ -93,33 +111,16 @@ pub async fn serve_command( }; // Initialize data directory and SQLite database - let config_path = args.config; - let server_settings = load_server_settings(config_path.as_deref())?; - let data_dir = storage_dir_override.unwrap_or_else(|| resolve_storage_dir(&server_settings)); + let config_path = args.config.clone(); + let disk_settings = load_server_settings(config_path.as_deref())?; + let data_dir = storage_dir_override.unwrap_or_else(|| resolve_storage_dir(&disk_settings)); // Shared config for live reloading - let shared_settings = Arc::new(RwLock::new(server_settings)); - - // CLI overrides take precedence over config file values, even after reload - let cli_model = args.model; - let cli_provider = args.provider; - - // Build registry factory that reads live config - let settings_for_factory = Arc::clone(&shared_settings); - let factory = move || { - let (model, provider_enum) = resolve_model_provider( - &settings_for_factory, - cli_model.as_deref(), - cli_provider.as_deref(), - ); - LlmSpec { - model, - provider: provider_enum, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: dry_run_mode, - } - }; + let shared_settings = Arc::new(RwLock::new(apply_serve_overrides( + &disk_settings, + &args, + dry_run_mode, + ))); std::fs::create_dir_all(&data_dir)?; let db = fabro_db::connect(&data_dir.join("fabro.db")).await?; fabro_db::initialize_db(&db).await?; @@ -141,18 +142,6 @@ pub async fn serve_command( (auth_mode, client_auth, max_concurrent_runs) }; - let git_author = { - let cfg = shared_settings.read().expect("config lock poisoned"); - let author = cfg.git_author(); - GitAuthor::from_options( - author.and_then(|a| a.name.clone()), - author.and_then(|a| a.email.clone()), - ) - }; - let hooks = { - let cfg = shared_settings.read().expect("config lock poisoned"); - cfg.hooks.clone() - }; let store_path = data_dir.join("store"); std::fs::create_dir_all(&store_path)?; let object_store = Arc::new(LocalFileSystem::new_with_prefix(&store_path)?); @@ -161,15 +150,8 @@ pub async fn serve_command( "", Duration::from_millis(5), )); - let state = create_app_state_with_store( - db, - factory, - dry_run_mode, - max_concurrent_runs, - git_author, - hooks, - store, - ); + let state = + create_app_state_with_store(db, Arc::clone(&shared_settings), max_concurrent_runs, store); spawn_scheduler(Arc::clone(&state)); let router = build_router(state, auth_mode); @@ -222,20 +204,32 @@ pub async fn serve_command( // Spawn config polling task let settings_for_poll = Arc::clone(&shared_settings); let config_path_for_poll = config_path.clone(); + let args_for_poll = ServeArgs { + port: args.port, + host: args.host.clone(), + model: args.model.clone(), + provider: args.provider.clone(), + dry_run: args.dry_run, + sandbox: args.sandbox, + max_concurrent_runs: args.max_concurrent_runs, + config: config_path.clone(), + }; tokio::spawn(async move { let mut interval = interval(Duration::from_secs(5)); interval.tick().await; // skip first immediate tick loop { interval.tick().await; match load_server_settings(config_path_for_poll.as_deref()) { - Ok(new_settings) => { + Ok(new_disk_settings) => { + let effective = + apply_serve_overrides(&new_disk_settings, &args_for_poll, dry_run_mode); let changed = { let cfg = settings_for_poll.read().expect("config lock poisoned"); - *cfg != new_settings + *cfg != effective }; if changed { let mut cfg = settings_for_poll.write().expect("config lock poisoned"); - *cfg = new_settings; + *cfg = effective; info!("Server config reloaded"); } } @@ -274,50 +268,6 @@ pub async fn serve_command( Ok(()) } -/// Resolve model and provider from shared config, with CLI overrides taking precedence. -fn resolve_model_provider( - shared_settings: &RwLock, - cli_model: Option<&str>, - cli_provider: Option<&str>, -) -> (String, Provider) { - let cfg = shared_settings.read().expect("config lock poisoned"); - let config_provider = cfg.llm.as_ref().and_then(|l| l.provider.as_deref()); - let config_model = cfg.llm.as_ref().and_then(|l| l.model.as_deref()); - - let provider_str = cli_provider.or(config_provider); - let model = cli_model - .map(std::string::ToString::to_string) - .or_else(|| config_model.map(std::string::ToString::to_string)) - .unwrap_or_else(|| { - // Look up default model from catalog for the given provider, - // falling back to the best provider with an API key configured. - provider_str - .and_then(|s| s.parse::().ok()) - .and_then(|p| Catalog::builtin().default_for_provider(p)) - .unwrap_or_else(|| Catalog::builtin().default_from_env()) - .id - .clone() - }); - - // Resolve model alias through catalog - let (model, provider_str) = match Catalog::builtin().get(&model) { - Some(info) => ( - info.id.clone(), - provider_str - .map(std::string::ToString::to_string) - .or(Some(info.provider.to_string())), - ), - None => (model, provider_str.map(std::string::ToString::to_string)), - }; - - let provider_enum: Provider = provider_str - .as_deref() - .and_then(|s| s.parse::().ok()) - .unwrap_or_else(Provider::default_from_env); - - (model, provider_enum) -} - /// Read the GitHub App private key from the environment, decoding base64 if needed. fn read_github_private_key() -> Option { let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?; diff --git a/lib/crates/fabro-server/src/server.rs b/lib/crates/fabro-server/src/server.rs index 4609745d8..849f7c7fd 100644 --- a/lib/crates/fabro-server/src/server.rs +++ b/lib/crates/fabro-server/src/server.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, RwLock}; use std::time::Duration; use axum::extract::{self as axum_extract, Path, Query, State}; @@ -9,19 +9,17 @@ use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::{Json, Router}; -use fabro_config::sandbox::SandboxSettings; +use fabro_config::FabroSettings; use fabro_llm::client::Client as LlmClient; use fabro_llm::generate::{GenerateParams, generate, generate_object}; use fabro_llm::types::{ ContentPart, FinishReason, Message as LlmMessage, Request as LlmRequest, Response as LlmResponse, Role, StreamEvent, ToolChoice, ToolDefinition, Usage, }; -use fabro_retro::retro; use fabro_retro::retro::Retro; use fabro_store::{InMemoryStore, Store}; use fabro_util::redact::redact_jsonl_line; use fabro_workflows::error::FabroError; -use fabro_workflows::git::GitAuthor; use fabro_workflows::handler::HandlerRegistry; use futures_util::stream; use tokio::sync::broadcast; @@ -42,14 +40,11 @@ use crate::sessions as sessions_mod; use crate::sessions::{SessionStore, new_session_store}; use fabro_interview::{Answer, Interviewer, QuestionType, WebInterviewer}; use fabro_retro::RetroExt; -use fabro_sandbox::SandboxSpec; use fabro_workflows::context::Context; use fabro_workflows::event::{EventEmitter, WorkflowRunEvent}; use fabro_workflows::operations::{self, CreateRunInput, WorkflowInput}; -use fabro_workflows::pipeline::{self, InitOptions, LlmSpec, Persisted, SandboxEnvSpec}; +use fabro_workflows::pipeline::Persisted; use fabro_workflows::records::{Checkpoint, CheckpointExt}; -use fabro_workflows::run_options::LifecycleOptions; -use fabro_workflows::run_options::RunOptions; pub use fabro_api_types::{ ApiQuestion, ApiQuestionOption, PaginatedRunList, PaginationMeta, @@ -118,7 +113,6 @@ struct AggregateUsageTotals { by_model: HashMap, } -type LlmSpecFactory = dyn Fn() -> LlmSpec + Send + Sync; type RegistryFactoryOverride = dyn Fn(Arc) -> HandlerRegistry + Send + Sync; /// Shared application state for the server. @@ -126,16 +120,19 @@ pub struct AppState { runs: Mutex>, aggregate_usage: Mutex, store: Arc, - llm_spec_factory: Box, - registry_factory_override: Option>, - pub dry_run: bool, pub db: sqlx::SqlitePool, max_concurrent_runs: usize, scheduler_notify: Notify, - pub hooks: Vec, - git_author: GitAuthor, pub sessions: SessionStore, llm_client: OnceCell, + settings: Arc>, + registry_factory_override: Option>, +} + +impl AppState { + pub(crate) fn dry_run(&self) -> bool { + self.settings.read().unwrap().dry_run_enabled() + } } /// Build the axum Router with all run endpoints. @@ -381,103 +378,65 @@ async fn get_aggregate_usage( } /// Create an `AppState` with the given LLM spec factory and database pool. -pub fn create_app_state( - db: sqlx::SqlitePool, - llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static, -) -> Arc { - create_app_state_with_options( - db, - llm_spec_factory, - false, - 5, - GitAuthor::default(), - Vec::new(), - ) +pub fn create_app_state(db: sqlx::SqlitePool) -> Arc { + create_app_state_with_options(db, FabroSettings::default(), 5) } #[doc(hidden)] pub fn create_app_state_with_registry_factory( db: sqlx::SqlitePool, - llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static, registry_factory_override: impl Fn(Arc) -> HandlerRegistry + Send + Sync + 'static, ) -> Arc { build_app_state( db, - Box::new(llm_spec_factory), + Arc::new(RwLock::new(FabroSettings::default())), Some(Box::new(registry_factory_override)), - false, 5, - GitAuthor::default(), - Vec::new(), Arc::new(InMemoryStore::default()), ) } -/// Create an `AppState` with the given database pool, LLM spec factory, dry-run flag, and concurrency limit. +/// Create an `AppState` with the given database pool, settings, and concurrency limit. pub fn create_app_state_with_options( db: sqlx::SqlitePool, - llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static, - dry_run: bool, + settings: FabroSettings, max_concurrent_runs: usize, - git_author: GitAuthor, - hooks: Vec, ) -> Arc { create_app_state_with_store( db, - llm_spec_factory, - dry_run, + Arc::new(RwLock::new(settings)), max_concurrent_runs, - git_author, - hooks, Arc::new(InMemoryStore::default()), ) } pub fn create_app_state_with_store( db: sqlx::SqlitePool, - llm_spec_factory: impl Fn() -> LlmSpec + Send + Sync + 'static, - dry_run: bool, + settings: Arc>, max_concurrent_runs: usize, - git_author: GitAuthor, - hooks: Vec, store: Arc, ) -> Arc { - build_app_state( - db, - Box::new(llm_spec_factory), - None, - dry_run, - max_concurrent_runs, - git_author, - hooks, - store, - ) + build_app_state(db, settings, None, max_concurrent_runs, store) } fn build_app_state( db: sqlx::SqlitePool, - llm_spec_factory: Box, + settings: Arc>, registry_factory_override: Option>, - dry_run: bool, max_concurrent_runs: usize, - git_author: GitAuthor, - hooks: Vec, store: Arc, ) -> Arc { Arc::new(AppState { runs: Mutex::new(HashMap::new()), aggregate_usage: Mutex::new(AggregateUsageTotals::default()), store, - llm_spec_factory, - registry_factory_override, - dry_run, db, max_concurrent_runs, scheduler_notify: Notify::new(), - hooks, - git_author, sessions: new_session_store(), llm_client: OnceCell::new(), + settings, + registry_factory_override, }) } @@ -539,15 +498,7 @@ async fn start_run( let run_id = ulid::Ulid::new().to_string(); info!(run_id = %run_id, "Run queued"); let run_dir = std::env::temp_dir().join(format!("fabro-{}", uuid::Uuid::new_v4())); - let settings = fabro_config::FabroSettings { - dry_run: Some(state.dry_run), - hooks: state.hooks.clone(), - sandbox: Some(SandboxSettings { - provider: Some("local".to_string()), - ..Default::default() - }), - ..Default::default() - }; + let settings = state.settings.read().unwrap().clone(); let created = match operations::create(CreateRunInput { workflow: WorkflowInput::DotSource { source: req.dot_source.clone(), @@ -626,7 +577,7 @@ async fn start_run( /// Execute a single run: transitions queued → starting → running → completed/failed/cancelled. async fn execute_run(state: Arc, run_id: String) { // Transition to Starting and set up cancel infrastructure - let (cancel_rx, run_dir) = { + let (cancel_rx, run_dir, event_tx, cancel_token) = { let mut runs = state.runs.lock().expect("runs lock poisoned"); let managed_run = match runs.get_mut(&run_id) { Some(r) if r.status == RunStatus::Queued => r, @@ -645,38 +596,23 @@ async fn execute_run(state: Arc, run_id: String) { managed_run.cancel_token = Some(Arc::clone(&cancel_token)); managed_run.event_tx = Some(event_tx); - (cancel_rx, run_dir) + ( + cancel_rx, + run_dir, + managed_run.event_tx.clone(), + cancel_token, + ) }; - // Create interviewer, sandbox, engine (this is the "provisioning" phase) + // Create interviewer and event plumbing (this is the "provisioning" phase) let interviewer = Arc::new(WebInterviewer::new()); let context = Context::new(); - - let event_tx = { - let runs = state.runs.lock().expect("runs lock poisoned"); - runs.get(&run_id).and_then(|r| r.event_tx.clone()) - }; - let emitter = EventEmitter::new(); if let Some(tx_clone) = event_tx { emitter.on_event(move |event| { let _ = tx_clone.send(event.clone()); }); } - - let cancel_token = { - let runs = state.runs.lock().expect("runs lock poisoned"); - runs.get(&run_id).and_then(|r| r.cancel_token.clone()) - }; - let Some(cancel_token) = cancel_token else { - return; - }; - - let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); - let sandbox = SandboxSpec::Local { - working_directory: cwd, - }; - let llm = (state.llm_spec_factory)(); let registry_override = state .registry_factory_override .as_ref() @@ -726,118 +662,46 @@ async fn execute_run(state: Arc, run_id: String) { return; } }; - let run_record = persisted.run_record().clone(); - let run_options = RunOptions { - settings: run_record.settings, - run_dir: run_dir.clone(), - cancel_token: Some(cancel_token), - run_id: run_id.clone(), - labels: run_record.labels, - git_author: state.git_author.clone(), - workflow_slug: run_record.workflow_slug, - github_app: None, - base_branch: run_record.base_branch, - display_base_sha: None, - host_repo_path: run_record.host_repo_path.map(Into::into), - git: None, + let github_app = + fabro_github::GitHubAppCredentials::from_env(persisted.run_record().settings.app_id()); + let services = operations::StartServices { + cancel_token: Some(Arc::clone(&cancel_token)), + emitter: Arc::clone(&emitter), + interviewer: Arc::clone(&interviewer) as Arc, + run_store: Arc::clone(&run_store), + github_app, + on_node: None, + registry_override, }; - let execution = { - let emitter = Arc::clone(&emitter); - let interviewer = Arc::clone(&interviewer) as Arc; - let run_id = run_id.clone(); - let run_options = run_options.clone(); - let run_store = Arc::clone(&run_store); - let hooks = state.hooks.clone(); - let dry_run = state.dry_run; - async move { - let initialized = pipeline::initialize( - persisted, - InitOptions { - run_id, - run_store, - dry_run, - emitter, - sandbox, - llm, - interviewer, - lifecycle: LifecycleOptions { - setup_commands: Vec::new(), - setup_command_timeout_ms: 300_000, - devcontainer_phases: Vec::new(), - }, - run_options, - hooks: fabro_hooks::HookConfig { hooks }, - sandbox_env: SandboxEnvSpec { - devcontainer_env: HashMap::new(), - toml_env: HashMap::new(), - github_permissions: None, - origin_url: None, - }, - devcontainer: None, - git: None, - worktree_mode: None, - registry_override, - checkpoint: None, - seed_context: None, - }, - ) - .await?; - Ok::<_, FabroError>(pipeline::execute(initialized).await) - } - }; - - let (result, final_context) = tokio::select! { - result = execution => match result { - Ok(executed) => (executed.outcome, Some(executed.final_context)), - Err(err) => (Err(err), None), - }, + let result = tokio::select! { + result = operations::start(&run_dir, services) => result, _ = cancel_rx => { - let mut runs = state.runs.lock().expect("runs lock poisoned"); - if let Some(managed_run) = runs.get_mut(&run_id) { - managed_run.status = RunStatus::Cancelled; - managed_run.event_tx = None; - } - state.scheduler_notify.notify_one(); - return; + cancel_token.store(true, Ordering::SeqCst); + Err(FabroError::Cancelled) } }; // Save final checkpoint let checkpoint = match run_store.get_checkpoint().await { - Ok(checkpoint) => checkpoint - .or_else(|| Checkpoint::load(&run_options.run_dir.join("checkpoint.json")).ok()), + Ok(checkpoint) => { + checkpoint.or_else(|| Checkpoint::load(&run_dir.join("checkpoint.json")).ok()) + } Err(err) => { tracing::warn!(run_id = %run_id, error = %err, "Failed to load checkpoint from store"); - Checkpoint::load(&run_options.run_dir.join("checkpoint.json")).ok() + Checkpoint::load(&run_dir.join("checkpoint.json")).ok() } }; - // Auto-derive retro and accumulate aggregate usage + // Accumulate aggregate usage after execution completes. if let Some(ref cp) = checkpoint { - let failed = result.is_err(); - let completed_stages = fabro_workflows::build_completed_stages(cp, failed); let stage_durations = match run_store.list_events().await { Ok(events) => fabro_workflows::extract_stage_durations_from_events(&events), Err(err) => { tracing::warn!(run_id = %run_id, error = %err, "Failed to load run events from store"); - retro::extract_stage_durations(&run_options.run_dir) + fabro_retro::retro::extract_stage_durations(&run_dir) } }; - let retro = retro::derive_retro( - &run_id, - "workflow", - "", - completed_stages, - 0, - &stage_durations, - ); - let _ = retro.save(&run_options.run_dir); - if let Err(err) = run_store.put_retro(&retro).await { - tracing::warn!(run_id = %run_id, error = %err, "Failed to save retro to store"); - } - - // Accumulate aggregate usage let mut agg = state .aggregate_usage .lock() @@ -860,11 +724,22 @@ async fn execute_run(state: Arc, run_id: String) { let mut runs = state.runs.lock().expect("runs lock poisoned"); if let Some(managed_run) = runs.get_mut(&run_id) { - match result { - Ok(_) => { - info!(run_id = %run_id, "Run completed"); - managed_run.status = RunStatus::Completed; - } + match &result { + Ok(started) => match &started.finalized.outcome { + Ok(_) => { + info!(run_id = %run_id, "Run completed"); + managed_run.status = RunStatus::Completed; + } + Err(FabroError::Cancelled) => { + info!(run_id = %run_id, "Run cancelled"); + managed_run.status = RunStatus::Cancelled; + } + Err(e) => { + error!(run_id = %run_id, error = %e, "Run failed"); + managed_run.status = RunStatus::Failed; + managed_run.error = Some(e.to_string()); + } + }, Err(FabroError::Cancelled) => { info!(run_id = %run_id, "Run cancelled"); managed_run.status = RunStatus::Cancelled; @@ -876,11 +751,15 @@ async fn execute_run(state: Arc, run_id: String) { } } managed_run.checkpoint = checkpoint; - if let Some(ctx) = final_context { - managed_run.context = Some(ctx); + if let Ok(started) = &result { + if let Some(ctx) = &started.final_context { + managed_run.context = Some(ctx.clone()); + } } - managed_run.run_dir = Some(run_options.run_dir.clone()); + managed_run.run_dir = Some(run_dir); managed_run.event_tx = None; + managed_run.cancel_tx = None; + managed_run.cancel_token = None; } drop(runs); state.scheduler_notify.notify_one(); @@ -1230,7 +1109,7 @@ async fn test_model( return ApiError::not_found(format!("Model not found: {id}")).into_response(); }; - if state.dry_run { + if state.dry_run() { return Json(serde_json::json!({ "model_id": id, "status": "ok", @@ -1408,7 +1287,7 @@ async fn create_completion( let use_stream = req.stream && req.schema.is_none(); // Dry-run mode returns a stub response - if state.dry_run { + if state.dry_run() { let msg_id = ulid::Ulid::new().to_string(); if use_stream { let finish_event = StreamEvent::finish( @@ -1646,6 +1525,7 @@ mod tests { use super::*; use axum::body::Body; use axum::http::Request; + use fabro_workflows::records::{RunRecord, RunRecordExt}; use tower::ServiceExt; const MINIMAL_DOT: &str = r#"digraph Test { @@ -1655,16 +1535,25 @@ mod tests { start -> exit }"#; - fn test_llm_spec() -> LlmSpec { - LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, + fn dry_run_settings() -> FabroSettings { + FabroSettings { + dry_run: Some(true), + ..Default::default() } } + fn command_dot(command: &str) -> String { + format!( + r#"digraph Test {{ + graph [goal="Test"] + start [shape=Mdiamond] + exit [shape=Msquare] + command [shape=parallelogram, tool_command="{command}"] + start -> command -> exit + }}"# + ) + } + async fn test_db() -> sqlx::SqlitePool { let pool = fabro_db::connect_memory().await.unwrap(); fabro_db::initialize_db(&pool).await.unwrap(); @@ -1672,7 +1561,7 @@ mod tests { } fn test_app_with(db: sqlx::SqlitePool) -> Router { - let state = create_app_state(db, test_llm_spec); + let state = create_app_state(db); build_router(state, AuthMode::Disabled) } @@ -1722,14 +1611,7 @@ mod tests { #[tokio::test] async fn test_model_dry_run_returns_ok() { - let state = create_app_state_with_options( - test_db().await, - test_llm_spec, - true, - 5, - GitAuthor::default(), - Vec::new(), - ); + let state = create_app_state_with_options(test_db().await, dry_run_settings(), 5); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -1749,14 +1631,7 @@ mod tests { #[tokio::test] async fn test_model_dry_run_unknown_returns_404() { - let state = create_app_state_with_options( - test_db().await, - test_llm_spec, - true, - 5, - GitAuthor::default(), - Vec::new(), - ); + let state = create_app_state_with_options(test_db().await, dry_run_settings(), 5); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -1810,7 +1685,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_run_status_returns_status() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = test_app_with_scheduler(state); // Start a run @@ -1868,7 +1743,7 @@ mod tests { #[tokio::test] async fn get_questions_returns_empty_list() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); // Start a run @@ -1933,7 +1808,7 @@ mod tests { #[tokio::test] async fn get_checkpoint_returns_null_initially() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); // Start a run @@ -1963,7 +1838,7 @@ mod tests { #[tokio::test] async fn get_context_returns_map() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); // Start a run @@ -1996,7 +1871,7 @@ mod tests { #[tokio::test] async fn cancel_run_succeeds() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); // Start a run @@ -2045,7 +1920,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn get_events_returns_sse_stream() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = test_app_with_scheduler(state); // Start a run @@ -2096,7 +1971,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn run_completes_and_status_is_completed() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = test_app_with_scheduler(state); // Start a run @@ -2135,7 +2010,7 @@ mod tests { #[tokio::test] async fn get_graph_returns_svg() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); // Start a run @@ -2203,7 +2078,7 @@ mod tests { #[tokio::test] async fn list_runs_returns_started_run() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); // List should be empty initially @@ -2252,7 +2127,7 @@ mod tests { #[tokio::test] async fn get_aggregate_usage_returns_zeros_initially() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(Arc::clone(&state), AuthMode::Disabled); let req = Request::builder() @@ -2275,7 +2150,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn aggregate_usage_increments_after_run_completes() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = test_app_with_scheduler(state); // Start a run @@ -2326,7 +2201,7 @@ mod tests { #[tokio::test] async fn post_runs_returns_queued_status() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -2355,9 +2230,120 @@ mod tests { assert_eq!(body["status"].as_str().unwrap(), "queued"); } + #[tokio::test] + async fn start_run_persists_full_settings_snapshot() { + let settings = FabroSettings { + dry_run: Some(true), + llm: Some(fabro_config::run::LlmSettings { + model: Some("claude-sonnet-4-5".to_string()), + provider: Some("anthropic".to_string()), + fallbacks: None, + }), + sandbox: Some(fabro_config::sandbox::SandboxSettings { + provider: Some("local".to_string()), + ..Default::default() + }), + hooks: vec![fabro_hooks::HookDefinition { + name: Some("snapshot-hook".to_string()), + event: fabro_hooks::HookEvent::RunStart, + command: Some("echo snapshot".to_string()), + hook_type: None, + matcher: None, + blocking: Some(false), + timeout_ms: Some(1_000), + sandbox: Some(false), + }], + git: Some(fabro_config::server::GitSettings { + app_id: Some("12345".to_string()), + author: fabro_config::server::GitAuthorSettings { + name: Some("Snapshot Bot".to_string()), + email: Some("snapshot@example.com".to_string()), + }, + ..Default::default() + }), + web: Some(fabro_config::server::WebSettings { + url: "http://example.test".to_string(), + ..Default::default() + }), + api: Some(fabro_config::server::ApiSettings { + base_url: "http://api.example.test".to_string(), + ..Default::default() + }), + log: Some(fabro_config::server::LogSettings { + level: Some("debug".to_string()), + }), + ..Default::default() + }; + let state = create_app_state_with_options(test_db().await, settings.clone(), 5); + let app = build_router(Arc::clone(&state), AuthMode::Disabled); + + let req = Request::builder() + .method("POST") + .uri("/runs") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({"dot_source": MINIMAL_DOT})).unwrap(), + )) + .unwrap(); + + let response = app.oneshot(req).await.unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + let body = body_json(response.into_body()).await; + let run_id = body["id"].as_str().unwrap().to_string(); + + let run_dir = { + let runs = state.runs.lock().expect("runs lock poisoned"); + runs.get(&run_id) + .and_then(|run| run.run_dir.clone()) + .expect("run_dir should be recorded") + }; + let run_record = RunRecord::load(&run_dir).unwrap(); + let mut expected_settings = settings; + expected_settings.goal = Some("Test".to_string()); + + assert_eq!(run_record.settings, expected_settings); + } + + #[tokio::test] + async fn config_change_after_submission_does_not_affect_execution() { + let output_dir = tempfile::tempdir().unwrap(); + let output_path = output_dir.path().join("executed.txt"); + let dot = command_dot(&format!("printf snapshot > {}", output_path.display())); + let initial_settings = dry_run_settings(); + let state = create_app_state_with_options(test_db().await, initial_settings.clone(), 5); + let app = build_router(Arc::clone(&state), AuthMode::Disabled); + + let req = Request::builder() + .method("POST") + .uri("/runs") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({ "dot_source": dot })).unwrap(), + )) + .unwrap(); + let response = app.clone().oneshot(req).await.unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + let body = body_json(response.into_body()).await; + let run_id = body["id"].as_str().unwrap().to_string(); + + *state.settings.write().unwrap() = FabroSettings::default(); + + execute_run(Arc::clone(&state), run_id.clone()).await; + + let runs = state.runs.lock().expect("runs lock poisoned"); + let managed_run = runs.get(&run_id).expect("run should still exist"); + assert_eq!(managed_run.status, RunStatus::Completed); + drop(runs); + + assert!( + !output_path.exists(), + "run should still use snapshotted dry-run settings" + ); + } + #[tokio::test] async fn cancel_queued_run_succeeds() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(state, AuthMode::Disabled); // Submit a run (no scheduler, stays queued) @@ -2396,9 +2382,86 @@ mod tests { assert_eq!(body["status"].as_str().unwrap(), "cancelled"); } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn cancel_during_startup_persists_cancelled_reason() { + let settings = FabroSettings { + setup: Some(fabro_config::run::SetupSettings { + commands: vec!["sleep 5".to_string()], + timeout_ms: Some(30_000), + }), + ..Default::default() + }; + let state = create_app_state_with_options(test_db().await, settings, 5); + let app = build_router(Arc::clone(&state), AuthMode::Disabled); + + let req = Request::builder() + .method("POST") + .uri("/runs") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({"dot_source": MINIMAL_DOT})).unwrap(), + )) + .unwrap(); + let response = app.clone().oneshot(req).await.unwrap(); + assert_eq!(response.status(), StatusCode::CREATED); + let body = body_json(response.into_body()).await; + let run_id = body["id"].as_str().unwrap().to_string(); + + let runner = tokio::spawn(execute_run(Arc::clone(&state), run_id.clone())); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + { + let mut runs = state.runs.lock().expect("runs lock poisoned"); + let managed_run = runs.get_mut(&run_id).expect("run should exist"); + if let Some(token) = &managed_run.cancel_token { + token.store(true, Ordering::SeqCst); + } + if let Some(cancel_tx) = managed_run.cancel_tx.take() { + let _ = cancel_tx.send(()); + } + } + + runner.await.unwrap(); + + let runs = state.runs.lock().expect("runs lock poisoned"); + let managed_run = runs.get(&run_id).expect("run should exist"); + assert_eq!(managed_run.status, RunStatus::Cancelled); + drop(runs); + + let run_store = state + .store + .open_run_reader(&run_id) + .await + .unwrap() + .expect("run store should exist"); + + let mut status_record = None; + for _ in 0..50 { + if let Some(record) = run_store.get_status().await.unwrap() { + if record.status == fabro_workflows::run_status::RunStatus::Failed + && record.reason == Some(fabro_workflows::run_status::StatusReason::Cancelled) + { + status_record = Some(record); + break; + } + } + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + + let status_record = status_record.expect("status record should be persisted"); + assert_eq!( + status_record.status, + fabro_workflows::run_status::RunStatus::Failed + ); + assert_eq!( + status_record.reason, + Some(fabro_workflows::run_status::StatusReason::Cancelled) + ); + } + #[tokio::test] async fn queue_position_reported_for_queued_runs() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(state, AuthMode::Disabled); // Submit two runs (no scheduler, both stay queued) @@ -2440,14 +2503,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn concurrency_limit_respected() { - let state = create_app_state_with_options( - test_db().await, - test_llm_spec, - false, - 1, - GitAuthor::default(), - Vec::new(), - ); + let state = create_app_state_with_options(test_db().await, FabroSettings::default(), 1); let app = test_app_with_scheduler(state); // Submit two runs with max_concurrent_runs=1 @@ -2496,7 +2552,7 @@ mod tests { #[tokio::test] async fn submit_answer_to_queued_run_returns_conflict() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -2528,14 +2584,7 @@ mod tests { #[tokio::test] async fn create_completion_non_streaming_returns_json() { - let state = create_app_state_with_options( - test_db().await, - test_llm_spec, - true, - 5, - GitAuthor::default(), - Vec::new(), - ); + let state = create_app_state_with_options(test_db().await, dry_run_settings(), 5); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -2565,14 +2614,7 @@ mod tests { #[tokio::test] async fn create_completion_streaming_returns_sse() { - let state = create_app_state_with_options( - test_db().await, - test_llm_spec, - true, - 5, - GitAuthor::default(), - Vec::new(), - ); + let state = create_app_state_with_options(test_db().await, dry_run_settings(), 5); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() diff --git a/lib/crates/fabro-server/src/sessions.rs b/lib/crates/fabro-server/src/sessions.rs index 01a56ffb8..4dc3a9dec 100644 --- a/lib/crates/fabro-server/src/sessions.rs +++ b/lib/crates/fabro-server/src/sessions.rs @@ -242,7 +242,7 @@ pub async fn create_session( store.insert(session_id, session); } - spawn_generation(Arc::clone(&state.sessions), session_id, state.dry_run, 1); + spawn_generation(Arc::clone(&state.sessions), session_id, state.dry_run(), 1); ( StatusCode::CREATED, @@ -307,7 +307,7 @@ pub async fn send_message( } }; - spawn_generation(Arc::clone(&state.sessions), id, state.dry_run, seq); + spawn_generation(Arc::clone(&state.sessions), id, state.dry_run(), seq); ( StatusCode::ACCEPTED, @@ -432,18 +432,6 @@ mod tests { use crate::jwt_auth::AuthMode; use crate::server::{build_router, create_app_state_with_options}; - use fabro_workflows::pipeline::LlmSpec; - - fn test_llm_spec() -> LlmSpec { - LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - } - } - async fn test_db() -> sqlx::SqlitePool { let pool = fabro_db::connect_memory().await.unwrap(); fabro_db::initialize_db(&pool).await.unwrap(); @@ -454,11 +442,11 @@ mod tests { let db = test_db().await; let state = create_app_state_with_options( db, - test_llm_spec, - true, + fabro_config::FabroSettings { + dry_run: Some(true), + ..Default::default() + }, 5, - fabro_workflows::git::GitAuthor::default(), - Vec::new(), ); build_router(state, AuthMode::Disabled) } diff --git a/lib/crates/fabro-server/tests/it/api.rs b/lib/crates/fabro-server/tests/it/api.rs index 46fedcab5..f16b3b521 100644 --- a/lib/crates/fabro-server/tests/it/api.rs +++ b/lib/crates/fabro-server/tests/it/api.rs @@ -12,19 +12,8 @@ mod mtls_e2e { use fabro_server::server::{build_router, create_app_state}; use fabro_server::server_config::TlsSettings; use fabro_server::tls::{ClientAuth, build_rustls_config}; - use fabro_workflows::pipeline::LlmSpec; use tokio::net::TcpListener; - fn test_llm_spec() -> LlmSpec { - LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - } - } - async fn test_db() -> sqlx::SqlitePool { let pool = fabro_db::connect_memory().await.unwrap(); fabro_db::initialize_db(&pool).await.unwrap(); @@ -191,7 +180,7 @@ mod mtls_e2e { let rustls_config = build_rustls_config(tls_settings, client_auth); let tls_acceptor = tokio_rustls::TlsAcceptor::from(rustls_config); - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let router = build_router(state, auth_mode); tokio::spawn(async move { @@ -422,7 +411,7 @@ mod mtls_e2e { // =========================================================================== mod server_lifecycle { - use super::super::helpers::{test_db, test_llm_spec}; + use super::super::helpers::test_db; use std::sync::Arc; use std::time::Duration; @@ -469,8 +458,7 @@ mod server_lifecycle { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn full_http_lifecycle_approve_and_complete() { - let state = - create_app_state_with_registry_factory(test_db().await, test_llm_spec, gate_registry); + let state = create_app_state_with_registry_factory(test_db().await, gate_registry); fabro_server::server::spawn_scheduler(Arc::clone(&state)); let app = build_router( Arc::clone(&state), @@ -569,8 +557,7 @@ mod server_lifecycle { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn full_http_lifecycle_cancel() { - let state = - create_app_state_with_registry_factory(test_db().await, test_llm_spec, gate_registry); + let state = create_app_state_with_registry_factory(test_db().await, gate_registry); fabro_server::server::spawn_scheduler(Arc::clone(&state)); let app = build_router( Arc::clone(&state), @@ -621,7 +608,7 @@ mod server_lifecycle { // =========================================================================== mod sse_events { - use super::super::helpers::{test_db, test_llm_spec}; + use super::super::helpers::test_db; use std::sync::Arc; use std::time::Duration; @@ -641,7 +628,7 @@ mod sse_events { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn sse_stream_contains_expected_event_types() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); fabro_server::server::spawn_scheduler(Arc::clone(&state)); let app = build_router( Arc::clone(&state), @@ -768,7 +755,7 @@ mod sse_events { // =========================================================================== mod serve_dry_run { - use super::super::helpers::{test_db, test_llm_spec}; + use super::super::helpers::test_db; use std::sync::Arc; use std::time::Duration; @@ -786,7 +773,7 @@ mod serve_dry_run { /// Build the router exactly as `serve_command` does in dry-run mode. async fn dry_run_app() -> axum::Router { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); fabro_server::server::spawn_scheduler(Arc::clone(&state)); build_router(state, fabro_server::jwt_auth::AuthMode::Disabled) } diff --git a/lib/crates/fabro-server/tests/it/helpers.rs b/lib/crates/fabro-server/tests/it/helpers.rs index f586dd540..4d4f72dd0 100644 --- a/lib/crates/fabro-server/tests/it/helpers.rs +++ b/lib/crates/fabro-server/tests/it/helpers.rs @@ -1,15 +1,3 @@ -use fabro_workflows::pipeline::LlmSpec; - -pub(crate) fn test_llm_spec() -> LlmSpec { - LlmSpec { - model: "test-model".to_string(), - provider: fabro_llm::Provider::Anthropic, - fallback_chain: Vec::new(), - mcp_servers: Vec::new(), - dry_run: true, - } -} - pub(crate) async fn test_db() -> sqlx::SqlitePool { let pool = fabro_db::connect_memory().await.unwrap(); fabro_db::initialize_db(&pool).await.unwrap(); diff --git a/lib/crates/fabro-server/tests/it/openapi_conformance.rs b/lib/crates/fabro-server/tests/it/openapi_conformance.rs index 4c156d60b..a9028b198 100644 --- a/lib/crates/fabro-server/tests/it/openapi_conformance.rs +++ b/lib/crates/fabro-server/tests/it/openapi_conformance.rs @@ -1,6 +1,6 @@ //! Conformance tests: spec ↔ router ↔ Rust struct consistency. -use super::helpers::{test_db, test_llm_spec}; +use super::helpers::test_db; use std::collections::BTreeSet; use axum::body::Body; @@ -58,7 +58,7 @@ fn methods_for_path_item(item: &openapiv3::PathItem) -> Vec { #[tokio::test] async fn all_spec_routes_are_routable() { let spec = load_spec(); - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(state, AuthMode::Disabled); let mut checked = 0; diff --git a/lib/crates/fabro-server/tests/it/pagination.rs b/lib/crates/fabro-server/tests/it/pagination.rs index 30987f043..a87bd1f2c 100644 --- a/lib/crates/fabro-server/tests/it/pagination.rs +++ b/lib/crates/fabro-server/tests/it/pagination.rs @@ -1,6 +1,6 @@ //! Tests that paginated list endpoints return `{ data, meta: { has_more } }`. -use super::helpers::{test_db, test_llm_spec}; +use super::helpers::test_db; use axum::body::Body; use axum::http::{Request, StatusCode}; use fabro_server::jwt_auth::AuthMode; @@ -91,7 +91,7 @@ const ENDPOINTS: &[PaginatedEndpoint] = &[ #[tokio::test] async fn paginated_endpoints_return_correct_shape() { - let state = create_app_state(test_db().await, test_llm_spec); + let state = create_app_state(test_db().await); let app = build_router(state, AuthMode::Disabled); for ep in ENDPOINTS { diff --git a/lib/crates/fabro-workflows/src/git.rs b/lib/crates/fabro-workflows/src/git.rs index c2f7b6ac6..cbf6fc5b1 100644 --- a/lib/crates/fabro-workflows/src/git.rs +++ b/lib/crates/fabro-workflows/src/git.rs @@ -2,6 +2,8 @@ use std::fmt::Write; use std::path::Path; use std::process::Command; +use fabro_config::FabroSettings; +use fabro_config::server::GitAuthorSettings; use fabro_git_storage::branchstore::BranchStore; use fabro_git_storage::gitobj::Store; use git2::{Repository, Signature}; @@ -64,6 +66,19 @@ impl GitAuthor { } } +impl From<&GitAuthorSettings> for GitAuthor { + fn from(value: &GitAuthorSettings) -> Self { + Self::from_options(value.name.clone(), value.email.clone()) + } +} + +pub fn git_author_from_settings(settings: &FabroSettings) -> GitAuthor { + settings + .git_author() + .map(GitAuthor::from) + .unwrap_or_default() +} + fn git_error(msg: impl Into) -> FabroError { FabroError::engine(msg.into()) } diff --git a/lib/crates/fabro-workflows/src/handler/manager_loop.rs b/lib/crates/fabro-workflows/src/handler/manager_loop.rs index 7e6817b41..4b3edf589 100644 --- a/lib/crates/fabro-workflows/src/handler/manager_loop.rs +++ b/lib/crates/fabro-workflows/src/handler/manager_loop.rs @@ -162,17 +162,12 @@ impl Handler for SubWorkflowHandler { let cancel_token = Arc::new(AtomicBool::new(false)); let child_cancel = Arc::clone(&cancel_token); - let git_state = services.git_state(); let child_run_options = RunOptions { settings: fabro_config::FabroSettings::default(), run_dir: child_logs, cancel_token: Some(cancel_token), run_id: format!("{parent_run_id}_child_{}", node.id), labels: HashMap::new(), - git_author: git_state - .as_ref() - .map(|gs| gs.git_author.clone()) - .unwrap_or_default(), workflow_slug: None, github_app: None, base_branch: None, diff --git a/lib/crates/fabro-workflows/src/lifecycle/git.rs b/lib/crates/fabro-workflows/src/lifecycle/git.rs index f59723bc3..5fb87a146 100644 --- a/lib/crates/fabro-workflows/src/lifecycle/git.rs +++ b/lib/crates/fabro-workflows/src/lifecycle/git.rs @@ -61,7 +61,8 @@ impl RunLifecycle for GitLifecycle { .and_then(|g| g.meta_branch.as_ref()), self.run_options.host_repo_path.as_ref(), ) { - let store = MetadataStore::new(repo_path, &self.run_options.git_author); + let git_author = self.run_options.git_author(); + let store = MetadataStore::new(repo_path, &git_author); let run_json = self .run_store .get_run() @@ -131,7 +132,8 @@ impl RunLifecycle for GitLifecycle { .and_then(|g| g.meta_branch.as_ref()), self.run_options.host_repo_path.as_ref(), ) { - let store = MetadataStore::new(repo_path, &self.run_options.git_author); + let git_author = self.run_options.git_author(); + let store = MetadataStore::new(repo_path, &git_author); // Build checkpoint JSON for shadow branch self.run_store .get_checkpoint() @@ -178,6 +180,7 @@ impl RunLifecycle for GitLifecycle { // Run branch commit via sandbox let completed_count = state.completed_nodes.len(); + let git_author = self.run_options.git_author(); let commit_result = git_checkpoint( &*self.sandbox, &self.run_id, @@ -186,7 +189,7 @@ impl RunLifecycle for GitLifecycle { completed_count, shadow_sha, self.run_options.checkpoint_exclude_globs(), - &self.run_options.git_author, + &git_author, ) .await; diff --git a/lib/crates/fabro-workflows/src/operations/start.rs b/lib/crates/fabro-workflows/src/operations/start.rs index 28f53cdd6..faa73f03b 100644 --- a/lib/crates/fabro-workflows/src/operations/start.rs +++ b/lib/crates/fabro-workflows/src/operations/start.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; @@ -20,7 +20,6 @@ use crate::event::{ EventEmitter, RunNoticeLevel, StoreProgressLogger, WorkflowRunEvent, append_progress_event, build_redacted_event_payload, }; -use crate::git::GitAuthor; use crate::handler::HandlerRegistry; use crate::outcome::{Outcome, StageStatus}; use crate::pipeline::{ @@ -51,7 +50,6 @@ struct RunSession { devcontainer: Option, seed_context: Option, run_store: Arc, - git_author: GitAuthor, git: Option, github_app: Option, worktree_mode: Option, @@ -69,7 +67,6 @@ pub struct StartServices { pub emitter: Arc, pub interviewer: Arc, pub run_store: Arc, - pub git_author: GitAuthor, pub github_app: Option, pub on_node: crate::OnNodeCallback, pub registry_override: Option>, @@ -77,6 +74,7 @@ pub struct StartServices { pub struct Started { pub finalized: Finalized, + pub final_context: Option, pub retro: Option, pub retro_duration: Duration, } @@ -117,6 +115,7 @@ pub(super) async fn execute_persisted_run( checkpoint: Option, mut services: StartServices, ) -> Result { + let cancel_token = services.cancel_token.clone(); let inner_store = Arc::clone(&services.run_store); let projection_run_dir = run_dir.to_path_buf(); services.run_store = Arc::new( @@ -169,7 +168,8 @@ pub(super) async fn execute_persisted_run( return Err(error); } - let mut bootstrap_guard = DetachedRunBootstrapGuard::arm(run_dir, Arc::clone(&run_store)); + let mut bootstrap_guard = + DetachedRunBootstrapGuard::arm(run_dir, Arc::clone(&run_store), cancel_token.clone()); let persisted = match Persisted::load_from_store(services.run_store.as_ref(), run_dir).await { Ok(persisted) => persisted, @@ -204,7 +204,8 @@ pub(super) async fn execute_persisted_run( }; bootstrap_guard.defuse(); - let mut completion_guard = DetachedRunCompletionGuard::arm(run_dir, Arc::clone(&run_store)); + let mut completion_guard = + DetachedRunCompletionGuard::arm(run_dir, Arc::clone(&run_store), cancel_token); let run_start = Instant::now(); let started = Box::pin(session.run(persisted, checkpoint)).await; @@ -394,7 +395,6 @@ impl RunSession { devcontainer, seed_context: None, run_store: services.run_store, - git_author: services.git_author, git: None, github_app: services.github_app.clone(), worktree_mode: Some(resolve_worktree_mode(&settings)), @@ -480,7 +480,6 @@ impl RunSession { cancel_token: self.cancel_token, run_id: record.run_id.clone(), labels: record.labels.clone(), - git_author: self.git_author, workflow_slug: record.workflow_slug.clone(), github_app: self.github_app.clone(), host_repo_path: record.host_repo_path.as_deref().map(PathBuf::from), @@ -548,6 +547,7 @@ impl RunSession { let executed = pipeline::execute(initialized).await; store_progress_logger.flush().await; + let final_context = Some(executed.final_context.clone()); let failed = !matches!( executed.outcome.as_ref().map(|outcome| &outcome.status), Ok(StageStatus::Success | StageStatus::PartialSuccess) @@ -600,6 +600,7 @@ impl RunSession { Ok(Started { finalized, + final_context, retro, retro_duration, }) @@ -609,11 +610,16 @@ impl RunSession { struct DetachedRunBootstrapGuard { run_dir: PathBuf, run_store: Arc, + cancel_token: Option>, active: bool, } impl DetachedRunBootstrapGuard { - fn arm(run_dir: &Path, run_store: Arc) -> Self { + fn arm( + run_dir: &Path, + run_store: Arc, + cancel_token: Option>, + ) -> Self { run_status::write_run_status( run_dir, RunStatus::Starting, @@ -622,6 +628,7 @@ impl DetachedRunBootstrapGuard { Self { run_dir: run_dir.to_path_buf(), run_store, + cancel_token, active: true, } } @@ -634,18 +641,23 @@ impl DetachedRunBootstrapGuard { impl Drop for DetachedRunBootstrapGuard { fn drop(&mut self) { if self.active { - run_status::write_run_status( - &self.run_dir, - RunStatus::Failed, - Some(StatusReason::SandboxInitFailed), - ); + let cancelled = self + .cancel_token + .as_ref() + .is_some_and(|token| token.load(Ordering::SeqCst)); + let reason = if cancelled { + StatusReason::Cancelled + } else { + StatusReason::SandboxInitFailed + }; + run_status::write_run_status(&self.run_dir, RunStatus::Failed, Some(reason)); let run_store = Arc::clone(&self.run_store); if let Ok(handle) = Handle::try_current() { handle.spawn(async move { let _ = run_store .put_status(&run_status::RunStatusRecord::new( RunStatus::Failed, - Some(StatusReason::SandboxInitFailed), + Some(reason), )) .await; }); @@ -655,20 +667,27 @@ impl Drop for DetachedRunBootstrapGuard { } const POSTRUN_ABORTED_MESSAGE: &str = "Run aborted before post-run finalization completed."; +const POSTRUN_CANCELLED_MESSAGE: &str = "Run cancelled before post-run finalization completed."; struct DetachedRunCompletionGuard { run_dir: PathBuf, run_store: Arc, run_id: Option, + cancel_token: Option>, active: bool, } impl DetachedRunCompletionGuard { - fn arm(run_dir: &Path, run_store: Arc) -> Self { + fn arm( + run_dir: &Path, + run_store: Arc, + cancel_token: Option>, + ) -> Self { Self { run_dir: run_dir.to_path_buf(), run_store, run_id: load_run_id(run_dir), + cancel_token, active: true, } } @@ -684,17 +703,29 @@ impl Drop for DetachedRunCompletionGuard { return; } - run_status::write_run_status( - &self.run_dir, - RunStatus::Failed, - Some(StatusReason::WorkflowError), - ); + let cancelled = self + .cancel_token + .as_ref() + .is_some_and(|token| token.load(Ordering::SeqCst)); + let reason = if cancelled { + StatusReason::Cancelled + } else { + StatusReason::WorkflowError + }; + let message = if cancelled { + POSTRUN_CANCELLED_MESSAGE + } else { + POSTRUN_ABORTED_MESSAGE + }; + let code = if cancelled { + "postrun_cancelled" + } else { + "postrun_aborted" + }; + + run_status::write_run_status(&self.run_dir, RunStatus::Failed, Some(reason)); if !self.run_dir.join("conclusion.json").exists() { - let _ = write_failure_conclusion( - &self.run_dir, - POSTRUN_ABORTED_MESSAGE, - Some(StatusReason::WorkflowError), - ); + let _ = write_failure_conclusion(&self.run_dir, message, Some(reason)); } if let Some(run_id) = load_run_id(&self.run_dir) { let _ = append_progress_event( @@ -702,8 +733,8 @@ impl Drop for DetachedRunCompletionGuard { &run_id, &WorkflowRunEvent::RunNotice { level: RunNoticeLevel::Error, - code: "postrun_aborted".to_string(), - message: POSTRUN_ABORTED_MESSAGE.to_string(), + code: code.to_string(), + message: message.to_string(), }, ); } @@ -714,11 +745,11 @@ impl Drop for DetachedRunCompletionGuard { let _ = run_store .put_status(&run_status::RunStatusRecord::new( RunStatus::Failed, - Some(StatusReason::WorkflowError), + Some(reason), )) .await; if let Err(err) = run_store - .put_conclusion(&build_failure_conclusion(POSTRUN_ABORTED_MESSAGE)) + .put_conclusion(&build_failure_conclusion(message)) .await { tracing::warn!( @@ -729,8 +760,8 @@ impl Drop for DetachedRunCompletionGuard { if let Some(run_id) = run_id { let event = WorkflowRunEvent::RunNotice { level: RunNoticeLevel::Error, - code: "postrun_aborted".to_string(), - message: POSTRUN_ABORTED_MESSAGE.to_string(), + code: code.to_string(), + message: message.to_string(), }; match build_redacted_event_payload(&event, &run_id) { Ok(payload) => { @@ -938,7 +969,6 @@ mod tests { run_store: crate::operations::open_or_hydrate_run(&InMemoryStore::default(), run_dir) .await .unwrap(), - git_author: crate::git::GitAuthor::default(), github_app: None, on_node: None, registry_override: Some(registry), diff --git a/lib/crates/fabro-workflows/src/pipeline/execute.rs b/lib/crates/fabro-workflows/src/pipeline/execute.rs index 4c35874ac..67496f92a 100644 --- a/lib/crates/fabro-workflows/src/pipeline/execute.rs +++ b/lib/crates/fabro-workflows/src/pipeline/execute.rs @@ -65,7 +65,7 @@ pub async fn execute(init: Initialized) -> Executed { run_branch: git.run_branch.clone(), meta_branch: git.meta_branch.clone(), checkpoint_exclude_globs: run_options.checkpoint_exclude_globs().to_vec(), - git_author: run_options.git_author.clone(), + git_author: run_options.git_author(), })) }); diff --git a/lib/crates/fabro-workflows/src/pipeline/execute/tests.rs b/lib/crates/fabro-workflows/src/pipeline/execute/tests.rs index 1c7b935f4..069ade696 100644 --- a/lib/crates/fabro-workflows/src/pipeline/execute/tests.rs +++ b/lib/crates/fabro-workflows/src/pipeline/execute/tests.rs @@ -77,7 +77,6 @@ fn test_run_options(run_dir: &Path, run_id: &str) -> RunOptions { host_repo_path: None, labels: HashMap::new(), github_app: None, - git_author: crate::git::GitAuthor::default(), base_branch: None, display_base_sha: None, workflow_slug: None, diff --git a/lib/crates/fabro-workflows/src/pipeline/finalize.rs b/lib/crates/fabro-workflows/src/pipeline/finalize.rs index d6edb30b3..a38fa620e 100644 --- a/lib/crates/fabro-workflows/src/pipeline/finalize.rs +++ b/lib/crates/fabro-workflows/src/pipeline/finalize.rs @@ -277,7 +277,8 @@ pub async fn write_finalize_commit( return; }; - let store = MetadataStore::new(repo_path, &run_options.git_author); + let git_author = run_options.git_author(); + let store = MetadataStore::new(repo_path, &git_author); let mut entries = scan_node_files(run_dir); let retro_bytes = match run_store.get_retro().await { Ok(Some(retro)) => serde_json::to_vec_pretty(&retro).ok(), @@ -452,7 +453,6 @@ mod tests { cancel_token: None, run_id: "run-test".to_string(), labels: HashMap::new(), - git_author: crate::git::GitAuthor::default(), workflow_slug: None, github_app: None, host_repo_path: None, diff --git a/lib/crates/fabro-workflows/src/pipeline/initialize.rs b/lib/crates/fabro-workflows/src/pipeline/initialize.rs index a1e5fb06b..1b8df4317 100644 --- a/lib/crates/fabro-workflows/src/pipeline/initialize.rs +++ b/lib/crates/fabro-workflows/src/pipeline/initialize.rs @@ -690,7 +690,6 @@ mod tests { cancel_token: None, run_id: "run-test".to_string(), labels: HashMap::new(), - git_author: crate::git::GitAuthor::default(), workflow_slug: None, github_app: None, host_repo_path: None, diff --git a/lib/crates/fabro-workflows/src/pipeline/retro.rs b/lib/crates/fabro-workflows/src/pipeline/retro.rs index 482ff7bed..7b3745e32 100644 --- a/lib/crates/fabro-workflows/src/pipeline/retro.rs +++ b/lib/crates/fabro-workflows/src/pipeline/retro.rs @@ -228,7 +228,6 @@ mod tests { cancel_token: None, run_id: "run-test".to_string(), labels: HashMap::new(), - git_author: crate::git::GitAuthor::default(), workflow_slug: None, github_app: None, host_repo_path: None, diff --git a/lib/crates/fabro-workflows/src/run_options.rs b/lib/crates/fabro-workflows/src/run_options.rs index 01ac7eab4..eb541526c 100644 --- a/lib/crates/fabro-workflows/src/run_options.rs +++ b/lib/crates/fabro-workflows/src/run_options.rs @@ -6,8 +6,6 @@ use std::sync::atomic::AtomicBool; use fabro_config::FabroSettings; use fabro_config::run::PullRequestSettings; -use crate::git::GitAuthor; - /// Git checkpoint options for a workflow run. #[derive(Clone)] pub struct GitCheckpointOptions { @@ -26,8 +24,6 @@ pub struct RunOptions { pub run_id: String, /// User-defined key-value labels for this run. pub labels: HashMap, - /// Git author identity for checkpoint commits. - pub git_author: GitAuthor, /// Workflow directory slug (e.g. "smoke" from `fabro/workflows/smoke/`). pub workflow_slug: Option, /// GitHub App credentials for pushing metadata branches to origin. @@ -51,6 +47,10 @@ impl RunOptions { &self.settings.checkpoint.exclude_globs } + pub fn git_author(&self) -> crate::git::GitAuthor { + crate::git::git_author_from_settings(&self.settings) + } + /// PR config (already normalized — disabled entries stripped at construction). pub fn pull_request(&self) -> Option<&PullRequestSettings> { self.settings.pull_request.as_ref() diff --git a/lib/crates/fabro-workflows/tests/it/daytona_integration.rs b/lib/crates/fabro-workflows/tests/it/daytona_integration.rs index 306ed2980..8be1f2341 100644 --- a/lib/crates/fabro-workflows/tests/it/daytona_integration.rs +++ b/lib/crates/fabro-workflows/tests/it/daytona_integration.rs @@ -396,7 +396,6 @@ async fn daytona_pipeline_artifact_offload_and_sync() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -587,7 +586,6 @@ async fn daytona_git_checkpoint_remote_emits_events() { cancel_token: None, run_id: "git-cp-test".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -773,7 +771,6 @@ async fn daytona_parallel_git_branching_e2e() { cancel_token: None, run_id: run_id.clone(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1149,7 +1146,6 @@ async fn daytona_git_checkpoint_with_shadow_branch() { cancel_token: None, run_id: run_id.clone(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1294,7 +1290,6 @@ async fn daytona_asset_collection() { cancel_token: None, run_id: "asset-test-daytona".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1540,7 +1535,6 @@ async fn daytona_git_push_run_branch_to_origin() { cancel_token: None, run_id: run_id.clone(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, diff --git a/lib/crates/fabro-workflows/tests/it/integration.rs b/lib/crates/fabro-workflows/tests/it/integration.rs index 326d1e67d..6696f6667 100644 --- a/lib/crates/fabro-workflows/tests/it/integration.rs +++ b/lib/crates/fabro-workflows/tests/it/integration.rs @@ -200,7 +200,6 @@ async fn end_to_end_linear_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -341,7 +340,6 @@ async fn end_to_end_branching_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -461,7 +459,6 @@ async fn end_to_end_human_gate_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -557,7 +554,6 @@ async fn human_gate_aborted_input_fails_closed_without_fail_route() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -668,7 +664,6 @@ async fn human_gate_aborted_input_routes_via_outcome_fail_condition() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -781,7 +776,6 @@ async fn goal_gate_routes_to_retry_target_on_failure() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -902,7 +896,6 @@ async fn goal_gate_routes_to_retry_target_when_present() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1214,7 +1207,6 @@ async fn retry_on_failure_then_succeed() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1289,7 +1281,6 @@ async fn pipeline_with_many_nodes() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1611,7 +1602,6 @@ async fn smoke_test_with_mock_codergen_backend() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1712,7 +1702,6 @@ async fn end_to_end_parallel_fan_out_fan_in() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1825,7 +1814,6 @@ async fn resume_from_checkpoint_completes_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1924,7 +1912,6 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -1967,7 +1954,6 @@ async fn graph_goal_in_context() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2003,7 +1989,6 @@ async fn event_streaming_lifecycle() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2097,7 +2082,6 @@ async fn context_flow_between_stages() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2150,7 +2134,6 @@ async fn tool_handler_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2222,7 +2205,6 @@ async fn auto_approve_interviewer_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2259,7 +2241,6 @@ async fn codergen_without_backend_simulated() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2364,7 +2345,6 @@ async fn branching_loop_back_on_failure() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2447,7 +2427,6 @@ async fn human_gate_loops_back() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2505,7 +2484,6 @@ async fn scenario_ship_a_feature() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2595,7 +2573,6 @@ async fn scenario_parallel_expert_review() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2679,7 +2656,6 @@ async fn scenario_node_retries_on_retry_status() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2741,7 +2717,6 @@ async fn scenario_loop_restart_resets_context() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2809,7 +2784,6 @@ async fn scenario_bug_triage_router() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2868,7 +2842,6 @@ async fn scenario_crash_recovery() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -2977,7 +2950,6 @@ async fn manager_loop_stop_condition_satisfied_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3056,7 +3028,6 @@ async fn manager_loop_max_cycles_exceeded_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3194,7 +3165,6 @@ async fn conditional_branching_success_fail_paths() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3247,7 +3217,6 @@ async fn edge_selection_condition_match_wins_over_weight() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3294,7 +3263,6 @@ async fn edge_selection_weight_breaks_ties() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3333,7 +3301,6 @@ async fn edge_selection_lexical_tiebreak() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3391,7 +3358,6 @@ async fn context_updates_visible_across_nodes() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3435,7 +3401,6 @@ async fn stylesheet_applies_model_override() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3491,7 +3456,6 @@ async fn custom_handler_registration_and_execution() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3562,7 +3526,6 @@ async fn integration_smoke_plan_implement_review_done() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3672,7 +3635,6 @@ async fn manager_loop_runs_child_engine_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3806,7 +3768,6 @@ async fn manager_loop_context_flows_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3879,7 +3840,6 @@ async fn manager_loop_child_dotfile_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -3981,7 +3941,6 @@ async fn import_e2e_through_engine() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4135,7 +4094,6 @@ async fn fidelity_default_is_compact() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4192,7 +4150,6 @@ async fn fidelity_graph_default_applied() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4245,7 +4202,6 @@ async fn fidelity_node_overrides_graph_default() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4304,7 +4260,6 @@ async fn fidelity_edge_overrides_node_and_graph() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4353,7 +4308,6 @@ async fn fidelity_full_produces_empty_preamble() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4412,7 +4366,6 @@ async fn fidelity_truncate_preamble_minimal() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4484,7 +4437,6 @@ async fn fidelity_summary_low_mode() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4551,7 +4503,6 @@ async fn fidelity_summary_medium_mode() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4618,7 +4569,6 @@ async fn fidelity_summary_high_mode() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4678,7 +4628,6 @@ async fn fidelity_full_sets_thread_id_in_context() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4749,7 +4698,6 @@ async fn fidelity_full_nodes_share_thread_id() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4830,7 +4778,6 @@ async fn fidelity_resume_degrades_full_to_summary_high() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -4927,7 +4874,6 @@ async fn fidelity_resume_degrade_only_affects_first_hop() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5011,7 +4957,6 @@ async fn fidelity_resume_no_degrade_when_not_full() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5053,7 +4998,6 @@ async fn fidelity_stored_in_checkpoint_context() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5139,7 +5083,6 @@ async fn fidelity_precedence_multi_node_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5207,7 +5150,6 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5282,7 +5224,6 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5349,7 +5290,6 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5420,7 +5360,6 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5474,7 +5413,6 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5531,7 +5469,6 @@ async fn fidelity_edge_thread_id_override_in_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5589,7 +5526,6 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5657,7 +5593,6 @@ async fn fidelity_from_parsed_dot_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5705,7 +5640,6 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5775,7 +5709,6 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -5862,7 +5795,6 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6077,7 +6009,6 @@ mod real_llm { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6191,7 +6122,6 @@ mod real_llm { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6330,7 +6260,6 @@ mod real_llm { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6437,7 +6366,6 @@ mod real_llm { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6533,7 +6461,6 @@ async fn human_gate_freeform_only_routes_text() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6663,7 +6590,6 @@ async fn human_gate_freeform_with_fixed_choice_match() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6778,7 +6704,6 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -6906,7 +6831,6 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -7014,7 +6938,6 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -7299,7 +7222,6 @@ fn make_run_options(dir: &std::path::Path) -> RunOptions { cancel_token: None, run_id: "hook-test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -8405,7 +8327,6 @@ async fn arc_e2e_with_real_llm() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -8533,7 +8454,6 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -8732,7 +8652,6 @@ async fn large_context_values_are_offloaded_to_artifact_store() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -8947,7 +8866,6 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -9077,7 +8995,6 @@ async fn node_dir_uses_visit_count_on_revisit() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -10051,7 +9968,6 @@ async fn full_pipeline_with_cli_backend_node() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -10182,7 +10098,6 @@ async fn stylesheet_backend_property_routes_to_cli() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -10461,7 +10376,6 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { cancel_token: None, run_id: "test-docker".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -10664,7 +10578,6 @@ async fn git_checkpoint_host_writes_shadow_branch() { cancel_token: None, run_id: run_id.into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -10862,7 +10775,6 @@ async fn parallel_git_branching_host_e2e() { cancel_token: None, run_id: run_id.into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11125,7 +11037,6 @@ async fn git_checkpoint_host_skips_empty_diff_patch() { cancel_token: None, run_id: "empty-diff".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11507,7 +11418,6 @@ async fn e2e_circuit_breaker_deterministic_self_loop() { cancel_token: None, run_id: "e2e-circuit-breaker".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11554,7 +11464,6 @@ async fn e2e_circuit_breaker_custom_limit() { cancel_token: None, run_id: "e2e-custom-limit".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11594,7 +11503,6 @@ async fn e2e_circuit_breaker_ignores_transient_failures() { cancel_token: None, run_id: "e2e-transient-no-breaker".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11641,7 +11549,6 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() { cancel_token: None, run_id: "e2e-varying-reasons".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11681,7 +11588,6 @@ async fn e2e_circuit_breaker_loop_restart() { cancel_token: None, run_id: "e2e-restart-breaker".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11743,7 +11649,6 @@ async fn e2e_failure_signature_persisted_in_context() { cancel_token: None, run_id: "e2e-sig-context".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11807,7 +11712,6 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() { cancel_token: None, run_id: "e2e-sig-hint".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11863,7 +11767,6 @@ async fn e2e_signature_maps_persist_in_checkpoint() { cancel_token: None, run_id: "e2e-sig-persist".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -11990,7 +11893,6 @@ async fn e2e_circuit_breaker_emits_events_before_abort() { cancel_token: None, run_id: "e2e-events".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12057,7 +11959,6 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() { cancel_token: None, run_id: "e2e-below-limit".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12153,7 +12054,6 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { cancel_token: None, run_id: "e2e-impl-verify-cycle".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12250,7 +12150,6 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() { cancel_token: None, run_id: "e2e-restart-blocked-det".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12290,7 +12189,6 @@ async fn e2e_loop_restart_blocked_for_structural_failure() { cancel_token: None, run_id: "e2e-restart-blocked-struct".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12330,7 +12228,6 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() { cancel_token: None, run_id: "e2e-restart-blocked-budget".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12370,7 +12267,6 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() { cancel_token: None, run_id: "e2e-restart-blocked-canceled".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12407,7 +12303,6 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() { cancel_token: None, run_id: "e2e-restart-blocked-comploop".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12448,7 +12343,6 @@ async fn e2e_loop_restart_allowed_for_transient_infra() { cancel_token: None, run_id: "e2e-restart-allowed-transient".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12552,7 +12446,6 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() { cancel_token: None, run_id: "stall-e2e".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12608,7 +12501,6 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() { cancel_token: None, run_id: "stall-alive-e2e".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12654,7 +12546,6 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() { cancel_token: None, run_id: "stall-disabled-e2e".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12719,7 +12610,6 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { cancel_token: None, run_id: "stall-override-e2e".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12855,7 +12745,6 @@ async fn asset_collection_local_sandbox_success() { cancel_token: None, run_id: "asset-test-local".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -12981,7 +12870,6 @@ async fn asset_collection_local_sandbox_on_failure() { cancel_token: None, run_id: "asset-test-fail".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -13073,7 +12961,6 @@ async fn asset_collection_docker_sandbox() { cancel_token: None, run_id: "asset-test-docker".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None, @@ -13137,7 +13024,6 @@ async fn wait_timer_e2e() { cancel_token: None, run_id: "test-run".into(), labels: std::collections::HashMap::new(), - git_author: fabro_workflows::git::GitAuthor::default(), workflow_slug: None, github_app: None, base_branch: None,