use fabro_test::{fabro_json_snapshot, fabro_snapshot, test_context}; use super::support::{ created_run_id, output_stdout, resolve_run, wait_for_status, write_gated_workflow, }; const SHARED_DAEMON_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); #[test] fn help() { let context = test_context!(); let mut cmd = context.command(); cmd.args(["start", "--help"]); fabro_snapshot!(context.filters(), cmd, @" success: true exit_code: 0 ----- stdout ----- Start a created workflow run on the server Usage: fabro start [OPTIONS] Arguments: Run ID prefix or workflow name Options: --json Output as JSON [env: FABRO_JSON=] --server Fabro server target: http(s) URL or absolute Unix socket path [env: FABRO_SERVER=] --debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=] --no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true] --quiet Suppress non-essential output [env: FABRO_QUIET=] --verbose Enable verbose output [env: FABRO_VERBOSE=] -h, --help Print help ----- stderr ----- "); } #[test] fn start_by_run_id_starts_created_run() { let context = test_context!(); context.ensure_home_server_auth_methods(); let workflow = context.install_fixture("simple.fabro"); let create = context .command() .args([ "create", "--dry-run", "--auto-approve", "--environment", "local", workflow.to_str().unwrap(), ]) .assert() .success(); let run_id = created_run_id(create.get_output()); context .command() .args(["start", &run_id]) .assert() .success(); context .command() .args(["wait", &run_id]) .timeout(SHARED_DAEMON_TIMEOUT) .assert() .success(); let output = context .command() .args(["wait", "--json", &run_id]) .output() .expect("wait should execute"); assert!(output.status.success(), "wait should succeed"); let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("wait JSON"); fabro_json_snapshot!( context, serde_json::json!({ "status": value["status"], }), @r#" { "status": "succeeded" } "# ); } #[test] fn start_by_run_id_starts_created_run_without_run_json_or_status_json() { let context = test_context!(); context.ensure_home_server_auth_methods(); let workflow = context.install_fixture("simple.fabro"); let create = context .command() .args([ "create", "--dry-run", "--auto-approve", "--environment", "local", workflow.to_str().unwrap(), ]) .assert() .success(); let run_id = created_run_id(create.get_output()); context .command() .args(["start", &run_id]) .assert() .success(); let output = context .command() .args(["wait", "--json", &run_id]) .timeout(SHARED_DAEMON_TIMEOUT) .output() .expect("wait should execute"); assert!(output.status.success(), "wait should succeed"); let value: serde_json::Value = serde_json::from_slice(&output.stdout).expect("wait JSON"); fabro_json_snapshot!( context, serde_json::json!({ "status": value["status"], }), @r#" { "status": "succeeded" } "# ); } #[test] fn start_rejects_already_active_or_completed_run() { let context = test_context!(); context.ensure_home_server_auth_methods(); let gate = write_gated_workflow(&context.temp_dir.join("slow.fabro"), "slow", "Run slowly"); let mut create_cmd = context.command(); create_cmd.env("OPENAI_API_KEY", "test"); create_cmd.args([ "create", "--provider", "openai", "--environment", "local", "slow.fabro", ]); let create_output = create_cmd.output().expect("command should execute"); assert!( create_output.status.success(), "create failed:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&create_output.stdout), String::from_utf8_lossy(&create_output.stderr) ); let run_id = output_stdout(&create_output).trim().to_string(); let run = resolve_run(&context, &run_id); let mut start_cmd = context.command(); start_cmd.env("OPENAI_API_KEY", "test"); start_cmd.args(["start", &run_id]); start_cmd.assert().success(); wait_for_status(&run.run_dir, &["running"]); let mut active_cmd = context.command(); active_cmd.args(["start", &run_id]); fabro_snapshot!(context.filters(), active_cmd, @" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × an engine process is still running for this run — cannot start "); gate.release(); wait_for_status(&run.run_dir, &["succeeded"]); let mut completed_cmd = context.command(); completed_cmd.args(["start", &run_id]); fabro_snapshot!(context.filters(), completed_cmd, @" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- × cannot start run: status is succeeded(completed), expected submitted "); } #[test] fn start_runs_under_server_ownership_without_launcher_record() { let context = test_context!(); context.ensure_home_server_auth_methods(); let gate = write_gated_workflow( &context.temp_dir.join("owned-by-server.fabro"), "owned-by-server", "Run under daemon ownership", ); let output = context .command() .args([ "create", "--provider", "openai", "--environment", "local", "owned-by-server.fabro", ]) .env("OPENAI_API_KEY", "test") .output() .expect("create should execute"); assert!( output.status.success(), "create failed:\nstdout:\n{}\nstderr:\n{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr) ); let run_id = output_stdout(&output).trim().to_string(); let run = resolve_run(&context, &run_id); context .command() .args(["start", &run_id]) .env("OPENAI_API_KEY", "test") .assert() .success(); wait_for_status(&run.run_dir, &["running"]); assert!( !context .storage_dir .join("launchers") .join(format!("{run_id}.json")) .exists(), "server-owned execution should not create a launcher record" ); gate.release(); wait_for_status(&run.run_dir, &["succeeded"]); }