fabro/lib/crates/fabro-cli/tests/it/scenario/lifecycle.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

351 lines
10 KiB
Rust

#![allow(
clippy::absolute_paths,
reason = "This test module prefers explicit type paths over extra imports."
)]
#![expect(
clippy::disallowed_methods,
reason = "Integration tests stage fixtures with sync std::fs calls."
)]
use fabro_test::{fabro_json_snapshot, test_context};
use serde_json::Value;
use super::{fixture, run_state, timeout_for};
use crate::support::unique_run_id;
#[fabro_macros::e2e_test()]
fn local_run_lifecycle() {
let context = test_context!();
let cmd = |args: &[&str]| -> assert_cmd::assert::Assert {
context
.command()
.args(args)
.timeout(timeout_for("local"))
.assert()
};
// 1. Run a workflow
cmd(&[
"run",
"--auto-approve",
"--sandbox",
"local",
fixture("command_pipeline.fabro").to_str().unwrap(),
])
.success();
// 2. ps -a --json — should list exactly one run
let label = context.test_case_label();
let ps_out = cmd(&["ps", "-a", "--json", "--label", &label]).success();
let ps_stdout = String::from_utf8(ps_out.get_output().stdout.clone()).unwrap();
let runs: Vec<Value> =
serde_json::from_str(&ps_stdout).expect("ps --json should produce a JSON array");
assert_eq!(runs.len(), 1, "should have exactly one run: {ps_stdout}");
let run_id = runs[0]["run_id"]
.as_str()
.expect("run should have run_id")
.to_string();
assert_eq!(
runs[0]["workflow_name"].as_str(),
Some("CommandPipeline"),
"workflow_name should be CommandPipeline"
);
// 3. inspect <run_id> — JSON array with run_spec and conclusion
let inspect_out = cmd(&["inspect", &run_id]).success();
let inspect_stdout = String::from_utf8(inspect_out.get_output().stdout.clone()).unwrap();
let items: Vec<Value> =
serde_json::from_str(&inspect_stdout).expect("inspect should produce a JSON array");
assert!(!items.is_empty(), "inspect should return at least one item");
assert!(
items[0]["run_spec"].is_object(),
"inspect should include run_spec"
);
assert!(
items[0]["conclusion"].is_object(),
"inspect should include conclusion"
);
// 4. events <run_id> — non-empty, first line is valid JSONL with event field
let events_out = cmd(&["events", &run_id]).success();
let events_stdout = String::from_utf8(events_out.get_output().stdout.clone()).unwrap();
assert!(!events_stdout.is_empty(), "events should not be empty");
let first_line = events_stdout.lines().next().unwrap();
let log_entry: Value =
serde_json::from_str(first_line).expect("first event line should be valid JSON");
assert!(
log_entry["event"].is_string(),
"first log line should have an event field"
);
// 5. artifact list — no assets yet, should succeed with empty message
let artifact_list_out = cmd(&["artifact", "list", &run_id]).success();
let artifact_list_stdout =
String::from_utf8(artifact_list_out.get_output().stdout.clone()).unwrap();
assert!(
artifact_list_stdout.contains("No artifacts found"),
"artifact list should report no artifacts: {artifact_list_stdout}"
);
// 6. system df — mentions "Runs"
let df_out = cmd(&["system", "df"]).success();
let df_stdout = String::from_utf8(df_out.get_output().stdout.clone()).unwrap();
assert!(
df_stdout.contains("Runs"),
"system df should mention Runs: {df_stdout}"
);
// 7. rm <run_id> — remove the run
cmd(&["rm", &run_id]).success();
// 8. ps -a --json — should be empty
let ps_out2 = cmd(&["ps", "-a", "--json", "--label", &label]).success();
let ps_stdout2 = String::from_utf8(ps_out2.get_output().stdout.clone()).unwrap();
let runs2: Vec<Value> =
serde_json::from_str(&ps_stdout2).expect("ps --json should produce a JSON array");
assert!(
runs2.is_empty(),
"runs should be empty after rm: {ps_stdout2}"
);
}
#[test]
fn dry_run_create_start_attach_works_with_default_run_lookup() {
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(["attach", &run_id])
.timeout(std::time::Duration::from_secs(10))
.assert()
.success();
let run_dir = context.find_run_dir(&run_id);
let state = run_state(&run_dir);
fabro_json_snapshot!(
context,
serde_json::json!({
"status": state.status.map(|status| match status {
fabro_types::RunStatus::Submitted => "submitted",
fabro_types::RunStatus::Queued => "queued",
fabro_types::RunStatus::Starting => "starting",
fabro_types::RunStatus::Running => "running",
fabro_types::RunStatus::Blocked { .. } => "blocked",
fabro_types::RunStatus::Paused { .. } => "paused",
fabro_types::RunStatus::Removing => "removing",
fabro_types::RunStatus::Succeeded { .. } => "succeeded",
fabro_types::RunStatus::Failed { .. } => "failed",
fabro_types::RunStatus::Dead => "dead",
fabro_types::RunStatus::Archived { .. } => "archived",
}),
"has_conclusion": state.conclusion.is_some(),
}),
@r#"
{
"status": "succeeded",
"has_conclusion": true
}
"#
);
}
#[test]
fn dry_run_detach_attach_works_with_default_run_lookup() {
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([
"run",
"--detach",
"--dry-run",
"--auto-approve",
"--run-id",
run_id.as_str(),
workflow.to_str().unwrap(),
])
.assert()
.success();
context
.command()
.args(["attach", &run_id])
.timeout(std::time::Duration::from_secs(10))
.assert()
.success();
let run_dir = context.find_run_dir(&run_id);
let state = run_state(&run_dir);
fabro_json_snapshot!(
context,
serde_json::json!({
"run_dir": run_dir,
"has_conclusion": state.conclusion.is_some(),
}),
@r#"
{
"run_dir": "[RUN_DIR]",
"has_conclusion": true
}
"#
);
}
#[test]
fn completed_run_can_be_attached_by_workflow_slug() {
let context = test_context!();
context.ensure_home_server_auth_methods();
let project = tempfile::tempdir().unwrap();
let workflow_dir = project.path().join("workflows").join("sluggy");
let workflow_path = workflow_dir.join("workflow.fabro");
let run_id = unique_run_id();
std::fs::create_dir_all(&workflow_dir).unwrap();
std::fs::write(
&workflow_path,
"\
digraph BarBaz {
start [shape=Mdiamond, label=\"Start\"]
exit [shape=Msquare, label=\"Exit\"]
start -> exit
}
",
)
.unwrap();
context
.command()
.current_dir(project.path())
.args([
"create",
"--dry-run",
"--auto-approve",
"--run-id",
run_id.as_str(),
workflow_path.to_str().unwrap(),
])
.assert()
.success();
context
.command()
.current_dir(project.path())
.args(["start", "sluggy"])
.assert()
.success();
context
.command()
.current_dir(project.path())
.args(["attach", &run_id])
.timeout(std::time::Duration::from_secs(10))
.assert()
.success();
context
.command()
.current_dir(project.path())
.args(["attach", "sluggy"])
.timeout(std::time::Duration::from_secs(10))
.assert()
.success();
let run_dir = context.find_run_dir(&run_id);
let state = run_state(&run_dir);
fabro_json_snapshot!(
context,
serde_json::json!({
"run_dir_exists": run_dir.exists(),
"has_conclusion": state.conclusion.is_some(),
}),
@r#"
{
"run_dir_exists": true,
"has_conclusion": true
}
"#
);
}
#[test]
fn completed_run_can_be_attached_by_file_stem() {
let context = test_context!();
context.ensure_home_server_auth_methods();
let workflow_dir = tempfile::tempdir().unwrap();
let workflow_path = workflow_dir.path().join("alpha.fabro");
let run_id = unique_run_id();
std::fs::write(
&workflow_path,
"\
digraph FooWorkflow {
start [shape=Mdiamond, label=\"Start\"]
exit [shape=Msquare, label=\"Exit\"]
start -> exit
}
",
)
.unwrap();
context
.command()
.args([
"create",
"--dry-run",
"--auto-approve",
"--run-id",
run_id.as_str(),
workflow_path.to_str().unwrap(),
])
.assert()
.success();
context
.command()
.args(["start", "alpha"])
.assert()
.success();
context
.command()
.args(["attach", "alpha"])
.timeout(std::time::Duration::from_secs(10))
.assert()
.success();
let run_spec = run_state(&context.find_run_dir(&run_id))
.spec
.expect("run spec should exist");
fabro_json_snapshot!(
context,
serde_json::json!({
"graph_name": run_spec.graph.name,
"workflow_slug": run_spec.workflow_slug,
}),
@r#"
{
"graph_name": "FooWorkflow",
"workflow_slug": "alpha"
}
"#
);
}