#![allow( clippy::absolute_paths, reason = "This test module prefers explicit type paths over extra imports." )] mod acp; mod agent_linear; mod artifacts; mod command_agent_mixed; mod command_pipeline; mod command_routing; mod conditional_branching; mod dry_run_examples; mod full_stack; mod hooks; mod human_gate; use std::path::{Path, PathBuf}; use std::time::Duration; use fabro_store::EventEnvelope; use fabro_test::{TestContext, expect_reqwest_status}; use serde_json::Value; use crate::cmd::support::{RunProjection, server_endpoint}; pub(super) fn fixture(name: &str) -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests/it/workflow/fixtures") .join(name) } pub(super) fn read_conclusion(run_dir: &Path) -> Value { serde_json::to_value( run_state(run_dir) .conclusion .expect("run store conclusion should exist"), ) .expect("conclusion should serialize") } pub(super) fn read_run_spec(run_dir: &Path) -> Value { serde_json::to_value(run_state(run_dir).spec).expect("run spec should serialize") } pub(super) fn completed_nodes(run_dir: &Path) -> Vec { let state = run_state(run_dir); let cp = state .current_checkpoint() .expect("run store checkpoint should exist"); cp.completed_nodes.clone() } pub(super) fn has_event(run_dir: &Path, event_name: &str) -> bool { run_events(run_dir) .into_iter() .any(|event| event.event.event_name() == event_name) } pub(super) fn dump_export(context: &TestContext, run_id: &str) -> PathBuf { let output_dir = context.temp_dir.join(format!("dump-{run_id}")); context .command() .args([ "dump", "--output", output_dir .to_str() .expect("dump output path should be valid UTF-8"), run_id, ]) .assert() .success(); output_dir } #[expect( clippy::disallowed_methods, reason = "integration test helpers inspect exported files synchronously" )] pub(super) fn stage_dump_dir(export_dir: &Path, stage_id: &str) -> PathBuf { let stages_dir = export_dir.join("stages"); let mut matches: Vec<_> = std::fs::read_dir(&stages_dir) .unwrap_or_else(|err| panic!("reading {} should succeed: {err}", stages_dir.display())) .filter_map(|entry| entry.ok().map(|entry| entry.path())) .filter(|path| { path.file_name() .and_then(|name| name.to_str()) .is_some_and(|name| { name == stage_id || name.split_once('-').is_some_and(|(_, id)| id == stage_id) }) }) .collect(); matches.sort(); match matches.as_slice() { [path] => path.clone(), [] => panic!( "stage dump dir for {stage_id} not found in {}", stages_dir.display() ), _ => panic!("stage dump dir for {stage_id} was ambiguous: {matches:?}"), } } /// Find the single run directory for this test context. pub(super) fn find_run_dir(context: &TestContext) -> PathBuf { context.single_run_dir() } pub(super) fn run_id_for(run_dir: &Path) -> String { infer_run_id(run_dir) } fn infer_run_id(run_dir: &Path) -> String { run_dir .file_name() .map(|name| name.to_string_lossy().to_string()) .and_then(|name| name.rsplit('-').next().map(ToOwned::to_owned)) .filter(|value| !value.is_empty()) .expect("run directory name should contain run id suffix") } fn block_on(future: impl std::future::Future) -> T { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("test runtime should build") .block_on(future) } async fn get_server_json_for_storage( storage_dir: &Path, path: &str, ) -> T { let (client, base_url) = server_endpoint(storage_dir).expect("server endpoint should exist"); let response = client .get(format!("{base_url}{path}")) .send() .await .expect("server request should succeed"); let response = expect_reqwest_status(response, fabro_http::StatusCode::OK, format!("GET {path}")).await; response .json::() .await .expect("server response should parse") } fn run_state(run_dir: &Path) -> RunProjection { let run_id = infer_run_id(run_dir); let runs_dir = run_dir.parent().expect("run dir should have parent"); let storage_dir = runs_dir.parent().expect("runs dir should have parent"); block_on(get_server_json_for_storage( storage_dir, &format!("/api/v1/runs/{run_id}/state"), )) } fn run_events(run_dir: &Path) -> Vec { let run_id = infer_run_id(run_dir); let runs_dir = run_dir.parent().expect("run dir should have parent"); let storage_dir = runs_dir.parent().expect("runs dir should have parent"); let response: serde_json::Value = block_on(get_server_json_for_storage( storage_dir, &format!("/api/v1/runs/{run_id}/events"), )); crate::support::parse_event_envelopes(&response) } macro_rules! sandbox_tests { ($name:ident) => { sandbox_tests!($name, keys = []); }; ($name:ident, keys = [$($key:expr),* $(,)?]) => { paste::paste! { #[fabro_macros::e2e_test($(live($key)),*)] fn []() { []("local"); } #[fabro_macros::e2e_test(live("DAYTONA_API_KEY") $(, live($key))*)] fn []() { []("daytona"); } } }; } pub(super) use sandbox_tests; pub(super) fn timeout_for(sandbox: &str) -> Duration { match sandbox { "daytona" => Duration::from_mins(10), _ => Duration::from_mins(3), } }