Format the Petri test fixtures after the engine key removal

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-09-18 10:54:45 -04:00
parent 1f0dbd86ae
commit 3e157d3356
No known key found for this signature in database
13 changed files with 2 additions and 748 deletions

View file

@ -1,118 +0,0 @@
use anyhow::Result;
use cli_table::format::{Border, Separator};
use cli_table::{Cell, CellStruct, Color, Style, Table};
use fabro_api::types::TimelineEntryResponse;
use fabro_types::RunId;
use fabro_util::printer::Printer;
use fabro_util::terminal::Styles;
use git2::Repository;
use serde::Serialize;
use crate::server_client::Client;
use crate::shared::color_if;
use crate::shared::repo::ensure_matching_repo_origin;
#[derive(Serialize)]
pub(crate) struct TimelineEntryJson {
ordinal: usize,
node_name: String,
visit: usize,
run_commit_sha: Option<String>,
}
pub(crate) async fn ensure_origin_if_local(
client: &Client,
run_id: &RunId,
verb: &str,
) -> Result<()> {
if Repository::discover(".").is_err() {
return Ok(());
}
let state = client.get_run_state(run_id).await?;
ensure_matching_repo_origin(state.spec.repo_origin_url(), verb)?;
Ok(())
}
pub(crate) fn timeline_entries_json(entries: &[TimelineEntryResponse]) -> Vec<TimelineEntryJson> {
entries
.iter()
.map(|entry| TimelineEntryJson {
ordinal: usize::try_from(entry.ordinal.get())
.expect("timeline ordinal should fit in usize"),
node_name: entry.node_name.clone(),
visit: usize::try_from(entry.visit.get())
.expect("timeline visit should fit in usize"),
run_commit_sha: entry.run_commit_sha.clone(),
})
.collect()
}
pub(crate) fn short_id(run_id: &str) -> &str {
&run_id[..8.min(run_id.len())]
}
pub(crate) fn print_timeline(entries: &[TimelineEntryJson], styles: &Styles, printer: Printer) {
if entries.is_empty() {
fabro_util::printerr!(printer, "No checkpoints found.");
return;
}
let use_color = styles.use_color;
let title = vec![
"@".cell().bold(use_color),
"Node".cell().bold(use_color),
"Details".cell().bold(use_color),
];
let rows: Vec<Vec<CellStruct>> = entries
.iter()
.map(|entry| {
let ordinal_str = format!("@{}", entry.ordinal);
let mut details = Vec::new();
if entry.visit > 1 {
details.push(format!("visit {}, loop", entry.visit));
}
if entry.run_commit_sha.is_none() {
details.push("no run commit".to_string());
}
let detail_str = if details.is_empty() {
String::new()
} else {
format!("({})", details.join(", "))
};
vec![
ordinal_str
.cell()
.foreground_color(color_if(use_color, Color::Cyan)),
entry.node_name.clone().cell(),
detail_str
.cell()
.foreground_color(color_if(use_color, Color::Ansi256(8))),
]
})
.collect();
let color_choice = if use_color {
cli_table::ColorChoice::Auto
} else {
cli_table::ColorChoice::Never
};
let table = rows
.table()
.title(title)
.color_choice(color_choice)
.border(Border::builder().build())
.separator(Separator::builder().build());
#[allow(
clippy::print_stderr,
reason = "The checkpoint timeline table is operator feedback, not command output."
)]
if let Ok(display) = table.display() {
for line in display.to_string().lines() {
eprintln!("{}", line.trim_end());
}
}
}

View file

@ -1,50 +0,0 @@
use anyhow::Result;
use fabro_api::types::ForkRequest;
use fabro_util::terminal::Styles;
use crate::args::ForkArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(crate) async fn run(args: &ForkArgs, styles: &Styles, base_ctx: &CommandContext) -> Result<()> {
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run_id).await?.id;
super::checkpoints::ensure_origin_if_local(client.as_ref(), &run_id, "fork").await?;
if args.list {
let timeline = client.run_timeline(&run_id).await?;
if ctx.json_output() {
print_json_pretty(&super::checkpoints::timeline_entries_json(&timeline))?;
return Ok(());
}
let entries = super::checkpoints::timeline_entries_json(&timeline);
super::checkpoints::print_timeline(&entries, styles, printer);
return Ok(());
}
let response = client
.fork_run(&run_id, ForkRequest {
target: args.target.clone(),
})
.await?;
if ctx.json_output() {
print_json_pretty(&response)?;
} else {
fabro_util::printerr!(
printer,
"\nForked run {} -> {}",
super::checkpoints::short_id(&response.source_run_id),
super::checkpoints::short_id(&response.new_run_id)
);
fabro_util::printerr!(
printer,
"To resume: fabro resume {}",
super::checkpoints::short_id(&response.new_run_id)
);
}
Ok(())
}

View file

@ -1,74 +0,0 @@
use anyhow::Result;
use fabro_api::types::RewindRequest;
use fabro_util::terminal::Styles;
use super::checkpoints::{ensure_origin_if_local, print_timeline, short_id, timeline_entries_json};
use crate::args::RewindArgs;
use crate::command_context::CommandContext;
use crate::shared::print_json_pretty;
pub(crate) async fn run(
args: &RewindArgs,
styles: &Styles,
base_ctx: &CommandContext,
) -> Result<()> {
let printer = base_ctx.printer();
let ctx = base_ctx.with_target(&args.server)?;
let client = ctx.server().await?;
let run_id = client.resolve_run(&args.run_id).await?.id;
ensure_origin_if_local(client.as_ref(), &run_id, "rewind").await?;
if args.list || args.target.is_none() {
let timeline = client.run_timeline(&run_id).await?;
if ctx.json_output() {
print_json_pretty(&timeline_entries_json(&timeline))?;
return Ok(());
}
print_timeline(&timeline_entries_json(&timeline), styles, printer);
return Ok(());
}
let target = args
.target
.clone()
.expect("rewind target should be present unless listing");
let result = client
.rewind_run(&run_id, RewindRequest {
target: Some(target),
})
.await?;
let response = result.response;
if ctx.json_output() {
print_json_pretty(&serde_json::json!({
"source_run_id": response.source_run_id,
"new_run_id": response.new_run_id,
"target": response.target,
"archived": response.archived,
"archive_error": response.archive_error,
"status": result.status,
}))?;
} else {
fabro_util::printerr!(
printer,
"\nRewound {}; new run {}",
short_id(&response.source_run_id),
short_id(&response.new_run_id)
);
fabro_util::printerr!(
printer,
"To resume: fabro resume {}",
short_id(&response.new_run_id)
);
if !response.archived {
let archive_error = response.archive_error.as_deref().unwrap_or("unknown error");
fabro_util::printerr!(
printer,
"Warning: source not archived: {archive_error}. Run `fabro archive {}` to finish.",
short_id(&response.source_run_id)
);
}
}
Ok(())
}

View file

@ -1,92 +0,0 @@
use std::path::Path;
use anyhow::{Context as _, Result, bail};
/// Detect the git remote URL and current branch from a local repository.
///
/// Uses `git2` to discover the repo at `path`, reads the `origin` remote URL
/// and the HEAD branch name.
pub(crate) fn detect_repo_info(path: &Path) -> Result<(String, Option<String>)> {
let repo = git2::Repository::discover(path)
.with_context(|| format!("Failed to discover git repo at {}", path.display()))?;
let url = repo
.find_remote("origin")
.context("Failed to find 'origin' remote")?
.url()
.context("origin remote URL is not valid UTF-8")?
.to_string();
let branch = repo
.head()
.ok()
.and_then(|head| head.shorthand().map(String::from));
Ok((url, branch))
}
pub(crate) fn ensure_matching_repo_origin(
expected_origin_url: Option<&str>,
action: &str,
) -> Result<()> {
let Some(expected_origin_url) = expected_origin_url else {
return Ok(());
};
let cwd = std::env::current_dir()?;
let (origin_url, _) = detect_repo_info(&cwd).map_err(|_| {
anyhow::anyhow!(
"Current directory is not a git repository with an origin remote; refusing to {action} run from repository '{expected_origin_url}'"
)
})?;
let current_origin_url = fabro_github::normalize_repo_origin_url(&origin_url);
if current_origin_url != expected_origin_url {
bail!(
"Current repository origin '{current_origin_url}' does not match run repository '{expected_origin_url}'; refusing to {action} this run from the wrong checkout"
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{detect_repo_info, ensure_matching_repo_origin};
#[test]
fn missing_expected_origin_skips_guard() {
ensure_matching_repo_origin(None, "fork").unwrap();
}
#[test]
fn detect_git_remote_from_repo() {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
repo.remote("origin", "https://github.com/org/repo.git")
.unwrap();
let (url, _branch) = detect_repo_info(dir.path()).unwrap();
assert_eq!(url, "https://github.com/org/repo.git");
}
#[test]
fn detect_repo_info_returns_worktree_branch() {
let dir = tempfile::tempdir().unwrap();
let repo = git2::Repository::init(dir.path()).unwrap();
let sig = git2::Signature::now("Test", "test@test.com").unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
let commit = repo
.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
.unwrap();
repo.remote("origin", "https://github.com/org/repo.git")
.unwrap();
let commit_obj = repo.find_commit(commit).unwrap();
repo.branch("fabro/run/ABC", &commit_obj, false).unwrap();
repo.set_head("refs/heads/fabro/run/ABC").unwrap();
let (_, branch) = detect_repo_info(dir.path()).unwrap();
assert_eq!(branch, Some("fabro/run/ABC".into()));
}
}

View file

@ -1,126 +0,0 @@
use fabro_test::{fabro_snapshot, run_and_format, test_context};
use insta::assert_snapshot;
use super::support::{
git_filters, output_stdout, run_state_by_id, setup_seeded_git_backed_changed_run,
};
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["fork", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Fork a workflow run from an earlier checkpoint into a new run
Usage: fabro fork [OPTIONS] <RUN_ID> [TARGET]
Arguments:
<RUN_ID> Run ID (or unambiguous prefix)
[TARGET] Target checkpoint: node name, node@visit, or @ordinal (omit to fork from latest)
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=]
--list Show the checkpoint timeline instead of forking
--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 fork_outside_git_repo_errors() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["fork", "01ARZ3NDEKTSV4RRFFQ69G5FAW"]);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No run found matching '[ULID]' (tried run ID prefix and workflow name)
");
}
#[test]
fn fork_latest_prints_new_run_and_resume_hint() {
let context = test_context!();
let setup = setup_seeded_git_backed_changed_run(&context);
let mut cmd = context.command();
cmd.args(["fork", &setup.run.run_id]);
let (snapshot, output) = run_and_format(&mut cmd, &git_filters(&context));
assert_snapshot!(snapshot, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Forked run [RUN_PREFIX] -> [RUN_PREFIX]
To resume: fabro resume [RUN_PREFIX]
");
assert!(output.status.success(), "fork should succeed");
}
#[test]
fn fork_from_earlier_checkpoint_uses_expected_sha() {
let context = test_context!();
let setup = setup_seeded_git_backed_changed_run(&context);
let output = context
.command()
.args(["fork", &setup.run.run_id, "@2", "--json"])
.output()
.expect("fork should execute");
assert!(
output.status.success(),
"fork should succeed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let fork_response: serde_json::Value =
serde_json::from_str(&output_stdout(&output)).expect("fork json should parse");
let new_run_id = fork_response["new_run_id"]
.as_str()
.expect("fork json should include new_run_id");
let run_snapshot = run_state_by_id(&context, new_run_id);
assert_eq!(
run_snapshot
.current_checkpoint()
.map(|checkpoint| checkpoint.current_node.as_str()),
Some("step_one")
);
assert_eq!(
run_snapshot
.current_checkpoint()
.and_then(|checkpoint| checkpoint.git_commit_sha.as_deref()),
Some(setup.step_one_sha.as_str())
);
assert_eq!(
run_snapshot
.spec
.fork_source_ref
.as_ref()
.map(|source| source.checkpoint_sha.as_str()),
Some(setup.step_one_sha.as_str())
);
assert_eq!(
run_snapshot
.spec
.fork_source_ref
.as_ref()
.map(|source| source.source_run_id.to_string()),
Some(setup.run.run_id.clone())
);
}

View file

@ -1,158 +0,0 @@
use fabro_test::{fabro_snapshot, run_and_format, test_context};
use insta::assert_snapshot;
use super::support::{
git_filters, output_stderr as support_stderr, run_events, run_state, run_state_by_id,
setup_seeded_git_backed_changed_run,
};
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["rewind", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Rewind a workflow run to an earlier checkpoint
Usage: fabro rewind [OPTIONS] <RUN_ID> [TARGET]
Arguments:
<RUN_ID> Run ID (or unambiguous prefix)
[TARGET] Target checkpoint: node name, node@visit, or @ordinal (omit with --list)
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=]
--list Show the checkpoint timeline instead of rewinding
--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 rewind_outside_git_repo_errors() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["rewind", "01ARZ3NDEKTSV4RRFFQ69G5FAW", "--list"]);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No run found matching '[ULID]' (tried run ID prefix and workflow name)
");
}
#[test]
fn rewind_list_prints_timeline_for_completed_git_run() {
let context = test_context!();
let setup = setup_seeded_git_backed_changed_run(&context);
let mut cmd = context.command();
cmd.args(["rewind", &setup.run.run_id, "--list"]);
fabro_snapshot!(git_filters(&context), cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
@ Node Details
@1 start (no run commit)
@2 step_one
@3 step_two
");
}
#[test]
fn rewind_target_updates_metadata_and_resume_hint() {
let context = test_context!();
let setup = setup_seeded_git_backed_changed_run(&context);
let mut cmd = context.command();
cmd.args(["rewind", &setup.run.run_id, "@2"]);
let (snapshot, output) = run_and_format(&mut cmd, &git_filters(&context));
assert_snapshot!(snapshot, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Rewound [RUN_PREFIX]; new run [RUN_PREFIX]
To resume: fabro resume [RUN_PREFIX]
");
assert!(output.status.success(), "rewind should succeed");
let state = run_state(&setup.run.run_dir);
assert!(state.archived_at.is_some());
let new_run_id = state
.superseded_by
.expect("rewind should record replacement run");
let replacement = run_state_by_id(&context, &new_run_id.to_string());
assert_eq!(
replacement
.current_checkpoint()
.and_then(|checkpoint| checkpoint.git_commit_sha.clone()),
Some(setup.step_one_sha)
);
}
#[test]
fn rewind_archives_source_and_records_superseded_by() {
let context = test_context!();
let setup = setup_seeded_git_backed_changed_run(&context);
let before_events = run_events(&setup.run.run_dir);
assert!(
before_events
.iter()
.any(|event| event.event.event_name() == "run.completed"),
"setup run should be completed before rewind"
);
let mut cmd = context.command();
cmd.args(["rewind", &setup.run.run_id, "@2"]);
let output = cmd.output().expect("rewind should execute");
assert!(
output.status.success(),
"rewind should succeed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
support_stderr(&output),
);
let after_events = run_events(&setup.run.run_dir);
assert_eq!(
after_events.len(),
before_events.len() + 2,
"rewind should append run.archived and run.superseded_by"
);
assert_eq!(
after_events[..before_events.len()]
.iter()
.map(|event| event.event.event_name())
.collect::<Vec<_>>(),
before_events
.iter()
.map(|event| event.event.event_name())
.collect::<Vec<_>>(),
"rewind should preserve the prior event prefix"
);
assert_eq!(
after_events[before_events.len()].event.event_name(),
"run.archived"
);
assert_eq!(
after_events[before_events.len() + 1].event.event_name(),
"run.superseded_by"
);
let state = run_state(&setup.run.run_dir);
assert!(state.archived_at.is_some());
assert!(state.superseded_by.is_some());
}

View file

@ -58,8 +58,7 @@ const CHILD_DOT: &str = r#"digraph Child {
say [shape=parallelogram, script="echo hello from the child", max_retries=0]
start -> say -> exit
}"#;
const CHILD_SETTINGS: &str =
"_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n";
const CHILD_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n";
/// A `[[run.hooks]]` entry that blocks every `fabro_run_search` call.
const BLOCKING_HOOK: &str = r#"

View file

@ -189,8 +189,7 @@ mod tests {
start -> exit
}"#;
const PETRI_SETTINGS: &str =
"_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n";
const PETRI_SETTINGS: &str = "_version = 1\n\n[workflow]\ngraph = \"workflow.fabro\"\n";
/// A worker runtime whose one worker runs until the test ends it, so
/// the test can act while the server waits on the worker. It keeps the

View file

@ -1,25 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Request body for creating a new run from a source run checkpoint.
*/
export interface ForkRequest {
/**
* Optional checkpoint target such as `@2`, `build`, or `build@1`. Defaults to the latest checkpoint.
*/
'target'?: string | null;
}

View file

@ -1,24 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Response returned after creating a forked run.
*/
export interface ForkResponse {
'source_run_id': string;
'new_run_id': string;
'target': string;
}

View file

@ -1,25 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Request body for creating a replacement run from a source run checkpoint.
*/
export interface RewindRequest {
/**
* Optional checkpoint target such as `@2`, `build`, or `build@1`. Defaults to the latest checkpoint.
*/
'target'?: string | null;
}

View file

@ -1,26 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Response returned after rewind creates a new run.
*/
export interface RewindResponse {
'source_run_id': string;
'new_run_id': string;
'target': string;
'archived': boolean;
'archive_error'?: string | null;
}

View file

@ -1,26 +0,0 @@
/* tslint:disable */
/* eslint-disable */
/**
* Fabro Run API
* HTTP API for managing Fabro workflow run executions.
*
* The version of the OpenAPI document: 0.2.0
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/
/**
* Checkpoint timeline entry for a run.
*/
export interface TimelineEntryResponse {
'ordinal': number;
'node_name': string;
'visit': number;
'checkpoint_seq': number;
'run_commit_sha'?: string | null;
}