fabro/lib/crates/fabro-cli/tests/it/cmd/rewind.rs
Bryan Helmkamp 5eeacd7864 fmt
2026-04-11 11:27:46 -04:00

200 lines
6.6 KiB
Rust

use fabro_test::{fabro_snapshot, run_and_format, test_context};
use insta::assert_snapshot;
use super::support::{
git_filters, git_stdout, output_stderr as support_stderr, run_branch_commits_since_base,
run_events, run_state, setup_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-push Skip force-pushing rewound refs to the remote
--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 -----
error: not in a git repository
> could not find repository at '.'; class=Repository (6); code=NotFound (-3)
");
}
#[test]
fn rewind_list_prints_timeline_for_completed_git_run() {
let context = test_context!();
let setup = setup_git_backed_changed_run(&context);
let mut cmd = context.command();
cmd.current_dir(&setup.repo_dir);
cmd.args(["rewind", &setup.run.run_id, "--list"]);
fabro_snapshot!(git_filters(&context), cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
@ Node Details
@1 step_one
@2 step_two
");
}
#[test]
fn rewind_target_updates_metadata_and_resume_hint() {
let context = test_context!();
let setup = setup_git_backed_changed_run(&context);
let expected_run_head =
run_branch_commits_since_base(&setup.repo_dir, &setup.run.run_id, &setup.base_sha)
.into_iter()
.next()
.expect("source run should have a first run commit");
let mut cmd = context.command();
cmd.current_dir(&setup.repo_dir);
cmd.args(["rewind", &setup.run.run_id, "@1", "--no-push"]);
let (snapshot, output) = run_and_format(&mut cmd, &git_filters(&context));
assert_snapshot!(snapshot, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Rewound metadata branch to @1 (step_one)
Rewound run branch fabro/run/[ULID] to [SHA]
To resume: fabro resume [RUN_PREFIX]
");
assert!(output.status.success(), "rewind should succeed");
let run_head = git_stdout(&setup.repo_dir, &[
"rev-parse",
&format!("fabro/run/{}", setup.run.run_id),
]);
assert_eq!(run_head.trim(), expected_run_head);
let mut list_cmd = context.command();
list_cmd.current_dir(&setup.repo_dir);
list_cmd.args(["rewind", &setup.run.run_id, "--list"]);
let list_output = list_cmd.output().expect("rewind --list should execute");
assert!(list_output.status.success(), "rewind --list should succeed");
let list = support_stderr(&list_output);
assert!(
list.contains("@1"),
"rewound timeline should keep @1: {list}"
);
assert!(
!list.contains("@2"),
"rewound timeline should drop @2: {list}"
);
}
#[test]
fn rewind_preserves_event_history_and_clears_terminal_snapshot_state() {
let context = test_context!();
let setup = setup_git_backed_changed_run(&context);
let before_events = run_events(&setup.run.run_dir);
assert!(
before_events
.iter()
.any(|event| event.payload.as_value()["event"] == "run.completed"),
"setup run should be completed before rewind"
);
let mut cmd = context.command();
cmd.current_dir(&setup.repo_dir);
cmd.args(["rewind", &setup.run.run_id, "@1", "--no-push"]);
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() + 3,
"rewind should append run.rewound, checkpoint.completed, and run.submitted"
);
assert_eq!(
after_events[..before_events.len()]
.iter()
.map(|event| event.payload.as_value()["event"].as_str().unwrap())
.collect::<Vec<_>>(),
before_events
.iter()
.map(|event| event.payload.as_value()["event"].as_str().unwrap())
.collect::<Vec<_>>(),
"rewind should preserve the prior event prefix"
);
assert_eq!(
after_events[before_events.len()].payload.as_value()["event"],
"run.rewound"
);
assert_eq!(
after_events[before_events.len() + 1].payload.as_value()["event"],
"checkpoint.completed"
);
assert_eq!(
after_events[before_events.len() + 2].payload.as_value()["event"],
"run.submitted"
);
assert!(
after_events[before_events.len() + 2].payload.as_value()["properties"]["definition_blob"]
.is_string(),
"rewind should re-emit run.submitted with the definition_blob"
);
let state = run_state(&setup.run.run_dir);
assert_eq!(
state.status.as_ref().map(|status| &status.status),
Some(&fabro_types::RunStatus::Submitted)
);
assert!(state.conclusion.is_none(), "rewind should clear conclusion");
assert!(
state.final_patch.is_none(),
"rewind should clear final patch"
);
assert!(
state.pull_request.is_none(),
"rewind should clear pull request"
);
assert!(
state.is_empty(),
"rewind should clear node state that belonged to the prior execution"
);
}