Snapshot run settings for execution

This commit is contained in:
Bryan Helmkamp 2026-03-29 22:42:13 -04:00
parent bafab58442
commit 947db0713c
23 changed files with 478 additions and 607 deletions

View file

@ -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,

View file

@ -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<GitHubAppCredentials> {
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)
}

View file

@ -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

View file

@ -53,6 +53,24 @@ pub struct GitHubAppCredentials {
pub private_key_pem: String,
}
impl GitHubAppCredentials {
pub fn from_env(app_id: Option<&str>) -> Option<Self> {
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 {

View file

@ -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<PathBuf>,
}
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<FabroSettings>,
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::<Provider>().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::<Provider>().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<String> {
let raw = std::env::var("GITHUB_APP_PRIVATE_KEY").ok()?;

View file

@ -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<String, ModelUsageTotals>,
}
type LlmSpecFactory = dyn Fn() -> LlmSpec + Send + Sync;
type RegistryFactoryOverride = dyn Fn(Arc<dyn Interviewer>) -> HandlerRegistry + Send + Sync;
/// Shared application state for the server.
@ -126,16 +120,19 @@ pub struct AppState {
runs: Mutex<HashMap<String, ManagedRun>>,
aggregate_usage: Mutex<AggregateUsageTotals>,
store: Arc<dyn Store>,
llm_spec_factory: Box<LlmSpecFactory>,
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
pub dry_run: bool,
pub db: sqlx::SqlitePool,
max_concurrent_runs: usize,
scheduler_notify: Notify,
pub hooks: Vec<fabro_hooks::HookDefinition>,
git_author: GitAuthor,
pub sessions: SessionStore,
llm_client: OnceCell<LlmClient>,
settings: Arc<RwLock<FabroSettings>>,
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
}
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<AppState> {
create_app_state_with_options(
db,
llm_spec_factory,
false,
5,
GitAuthor::default(),
Vec::new(),
)
pub fn create_app_state(db: sqlx::SqlitePool) -> Arc<AppState> {
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<dyn Interviewer>) -> HandlerRegistry + Send + Sync + 'static,
) -> Arc<AppState> {
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<fabro_hooks::HookDefinition>,
) -> Arc<AppState> {
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<RwLock<FabroSettings>>,
max_concurrent_runs: usize,
git_author: GitAuthor,
hooks: Vec<fabro_hooks::HookDefinition>,
store: Arc<dyn Store>,
) -> Arc<AppState> {
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<LlmSpecFactory>,
settings: Arc<RwLock<FabroSettings>>,
registry_factory_override: Option<Box<RegistryFactoryOverride>>,
dry_run: bool,
max_concurrent_runs: usize,
git_author: GitAuthor,
hooks: Vec<fabro_hooks::HookDefinition>,
store: Arc<dyn Store>,
) -> Arc<AppState> {
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<AppState>, 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<AppState>, 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<AppState>, 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<dyn Interviewer>,
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<dyn Interviewer>;
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<AppState>, 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<AppState>, 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()

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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();

View file

@ -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<Method> {
#[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;

View file

@ -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 {

View file

@ -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<String>) -> FabroError {
FabroError::engine(msg.into())
}

View file

@ -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,

View file

@ -61,7 +61,8 @@ impl RunLifecycle<WorkflowGraph> 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<WorkflowGraph> 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<WorkflowGraph> 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<WorkflowGraph> for GitLifecycle {
completed_count,
shadow_sha,
self.run_options.checkpoint_exclude_globs(),
&self.run_options.git_author,
&git_author,
)
.await;

View file

@ -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<DevcontainerSpec>,
seed_context: Option<Context>,
run_store: Arc<dyn RunStore>,
git_author: GitAuthor,
git: Option<GitCheckpointOptions>,
github_app: Option<fabro_github::GitHubAppCredentials>,
worktree_mode: Option<WorktreeMode>,
@ -69,7 +67,6 @@ pub struct StartServices {
pub emitter: Arc<EventEmitter>,
pub interviewer: Arc<dyn Interviewer>,
pub run_store: Arc<dyn RunStore>,
pub git_author: GitAuthor,
pub github_app: Option<fabro_github::GitHubAppCredentials>,
pub on_node: crate::OnNodeCallback,
pub registry_override: Option<Arc<HandlerRegistry>>,
@ -77,6 +74,7 @@ pub struct StartServices {
pub struct Started {
pub finalized: Finalized,
pub final_context: Option<Context>,
pub retro: Option<Retro>,
pub retro_duration: Duration,
}
@ -117,6 +115,7 @@ pub(super) async fn execute_persisted_run(
checkpoint: Option<Checkpoint>,
mut services: StartServices,
) -> Result<Started, FabroError> {
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<dyn RunStore>,
cancel_token: Option<Arc<AtomicBool>>,
active: bool,
}
impl DetachedRunBootstrapGuard {
fn arm(run_dir: &Path, run_store: Arc<dyn RunStore>) -> Self {
fn arm(
run_dir: &Path,
run_store: Arc<dyn RunStore>,
cancel_token: Option<Arc<AtomicBool>>,
) -> 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<dyn RunStore>,
run_id: Option<String>,
cancel_token: Option<Arc<AtomicBool>>,
active: bool,
}
impl DetachedRunCompletionGuard {
fn arm(run_dir: &Path, run_store: Arc<dyn RunStore>) -> Self {
fn arm(
run_dir: &Path,
run_store: Arc<dyn RunStore>,
cancel_token: Option<Arc<AtomicBool>>,
) -> 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),

View file

@ -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(),
}))
});

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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<String, String>,
/// Git author identity for checkpoint commits.
pub git_author: GitAuthor,
/// Workflow directory slug (e.g. "smoke" from `fabro/workflows/smoke/`).
pub workflow_slug: Option<String>,
/// 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()

View file

@ -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,

View file

@ -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,