fabro/lib/crates/fabro-cli/tests/it/cmd/start.rs
Bryan Helmkamp 5fc9157017
refactor(workflow): remove retro stage (#230)
## 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.

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 (unknown context, reasoning unspecified) via
[Codex](https://openai.com/codex)
2026-05-09 10:18:20 -04:00

243 lines
6.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"]);
}