mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
## Summary Removes Fabro's automatic retro generation stage so workflow runs go directly from execution to finalization and optional PR creation. This drops the retro-specific crate, events, projection fields, config/API knobs, and user-facing docs in favor of the existing durable run observability surfaces. ## What Changed - Deleted the `fabro-retro` crate and the workflow `retro` pipeline phase, with finalization now consuming `Executed` state directly. - Removed retro configuration and API surface area, including `--no-retro`, `[run.execution].retros`, manifest `no_retro`, `features.retros`, and run projection `retro*` fields. - Retired typed `retro.*` events while keeping historical event logs readable by deserializing retired retro event names as `Unknown`. - Stopped appending retro sections to generated PR bodies and updated docs, marketing copy, screenshots, and navigation to point users toward observability/event-stream inspection. ## Testing Not run during PR creation; this branch already contained the implementation commit. --- [](https://github.com/EveryInc/compound-engineering-plugin) 🤖 Generated with GPT-5 (unknown context, reasoning unspecified) via [Codex](https://openai.com/codex)
243 lines
6.8 KiB
Rust
243 lines
6.8 KiB
Rust
use fabro_test::{fabro_json_snapshot, fabro_snapshot, test_context};
|
||
|
||
use super::support::{output_stdout, resolve_run, wait_for_status, write_gated_workflow};
|
||
use crate::support::unique_run_id;
|
||
|
||
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] <RUN>
|
||
|
||
Arguments:
|
||
<RUN> Run ID prefix or workflow name
|
||
|
||
Options:
|
||
--json Output as JSON [env: FABRO_JSON=]
|
||
--server <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 run_id = unique_run_id();
|
||
let workflow = context.install_fixture("simple.fabro");
|
||
|
||
context
|
||
.command()
|
||
.args([
|
||
"create",
|
||
"--dry-run",
|
||
"--auto-approve",
|
||
"--run-id",
|
||
run_id.as_str(),
|
||
workflow.to_str().unwrap(),
|
||
])
|
||
.assert()
|
||
.success();
|
||
|
||
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 run_id = unique_run_id();
|
||
let workflow = context.install_fixture("simple.fabro");
|
||
|
||
context
|
||
.command()
|
||
.args([
|
||
"create",
|
||
"--dry-run",
|
||
"--auto-approve",
|
||
"--run-id",
|
||
run_id.as_str(),
|
||
workflow.to_str().unwrap(),
|
||
])
|
||
.assert()
|
||
.success();
|
||
|
||
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",
|
||
"--sandbox",
|
||
"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",
|
||
"--sandbox",
|
||
"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"]);
|
||
}
|