fabro/lib/apps/fabro-cli/tests/it/cmd/archive.rs
2026-07-23 17:59:34 -04:00

262 lines
8.1 KiB
Rust

use fabro_test::{fabro_snapshot, test_context};
use httpmock::MockServer;
use serde_json::Value;
use super::support::{
remote_run_summary_json, setup_seeded_completed_dry_run, setup_seeded_created_dry_run,
ulid_filter,
};
use crate::support::unique_run_id;
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["archive", "--help"]);
fabro_snapshot!(context.filters(), cmd, @r"
success: true
exit_code: 0
----- stdout -----
Mark terminal runs as archived (reviewed, no further action needed). Archived runs are hidden from default listings
Usage: fabro archive [OPTIONS] <RUNS>...
Arguments:
<RUNS>... Run IDs or workflow names to archive
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 archive_requires_at_least_one_id() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["archive"]);
fabro_snapshot!(context.filters(), cmd, @"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: the following required arguments were not provided:
<RUNS>...
Usage: fabro archive --no-upgrade-check <RUNS>...
For more information, try '--help'.
");
}
#[test]
fn archive_succeeded_run_hides_it_from_default_ps() {
let context = test_context!();
let run = setup_seeded_completed_dry_run(&context);
let mut filters = context.filters();
filters.push(ulid_filter());
let mut cmd = context.command();
cmd.args(["archive", &run.run_id]);
fabro_snapshot!(filters, cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[ULID]
");
// Default `ps` filters out archived runs.
let mut ps = context.ps();
ps.args(["--json", "--label", &context.test_case_label()]);
fabro_snapshot!(context.filters(), ps, @r#"
success: true
exit_code: 0
----- stdout -----
[]
----- stderr -----
"#);
// `ps -a` surfaces it with status `archived`.
let output = context
.ps()
.args(["-a", "--json", "--label", &context.test_case_label()])
.output()
.expect("ps -a should execute");
assert!(output.status.success());
let runs: Vec<Value> = serde_json::from_slice(&output.stdout).expect("ps JSON should parse");
assert_eq!(runs.len(), 1, "ps -a should show the archived run");
assert_eq!(runs[0]["status"]["kind"], "succeeded");
assert_eq!(runs[0]["status"]["reason"], "completed");
assert_eq!(runs[0]["run_id"], run.run_id);
}
#[test]
fn archive_running_run_rejects_with_must_be_terminal_message() {
// A `create`d run is in `submitted` — not yet terminal.
let context = test_context!();
let run = setup_seeded_created_dry_run(&context);
let output = context
.command()
.args(["archive", &run.run_id])
.output()
.expect("archive should execute");
assert!(!output.status.success(), "archive on submitted must fail");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("must be terminal"),
"expected 'must be terminal' in stderr, got: {stderr}"
);
}
#[test]
fn archive_already_archived_is_idempotent() {
let context = test_context!();
let run = setup_seeded_completed_dry_run(&context);
let first = context
.command()
.args(["archive", &run.run_id])
.output()
.expect("archive should execute");
assert!(first.status.success(), "first archive should succeed");
let second = context
.command()
.args(["archive", &run.run_id])
.output()
.expect("archive should execute");
assert!(
second.status.success(),
"second archive on already-archived should succeed\nstderr:\n{}",
String::from_utf8_lossy(&second.stderr)
);
}
#[test]
fn archive_unknown_id_renders_clean_error() {
let context = test_context!();
let fake_id = unique_run_id();
let output = context
.command()
.args(["archive", &fake_id])
.output()
.expect("archive should execute");
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains(&fake_id) || stderr.contains("No run found"),
"expected unknown-id error in stderr, got: {stderr}"
);
}
#[test]
fn archive_json_output_shape() {
let context = test_context!();
let run = setup_seeded_completed_dry_run(&context);
let output = context
.command()
.args(["--json", "archive", &run.run_id])
.output()
.expect("archive --json should execute");
assert!(output.status.success());
let value: Value = serde_json::from_slice(&output.stdout).expect("archive JSON should parse");
assert_eq!(
value["archived"],
Value::Array(vec![Value::String(run.run_id.clone())])
);
assert_eq!(value["errors"], Value::Array(vec![]));
}
#[test]
fn archive_mixed_batch_aggregates_errors() {
let context = test_context!();
let good = setup_seeded_completed_dry_run(&context);
let bad = unique_run_id();
let output = context
.command()
.args(["--json", "archive", &good.run_id, &bad])
.output()
.expect("archive should execute");
assert!(!output.status.success(), "mixed batch should exit non-zero");
let value: Value = serde_json::from_slice(&output.stdout).expect("archive JSON should parse");
assert_eq!(
value["archived"],
Value::Array(vec![Value::String(good.run_id.clone())])
);
let errors = value["errors"].as_array().expect("errors should be array");
assert_eq!(errors.len(), 1);
assert_eq!(errors[0]["identifier"], bad);
}
#[test]
fn archive_resolves_selector_via_server_endpoint() {
let context = test_context!();
let server = MockServer::start();
let run_id = unique_run_id();
let resolve_mock = server.mock(|when, then| {
when.method("GET")
.path("/api/v1/runs/resolve")
.query_param("selector", "nightly-build");
then.status(200)
.header("Content-Type", "application/json")
.body(
remote_run_summary_json(
&run_id,
"Nightly Build",
"nightly-build",
"Nightly run",
&serde_json::json!({
"kind": "succeeded",
"reason": "completed"
}),
"2026-04-05T12:00:00Z",
)
.to_string(),
);
});
let archive_mock = server.mock(|when, then| {
when.method("POST")
.path(format!("/api/v1/runs/{run_id}/archive"));
then.status(200)
.header("Content-Type", "application/json")
.body(
remote_run_summary_json(
&run_id,
"Nightly Build",
"nightly-build",
"Nightly run",
&serde_json::json!({
"kind": "succeeded",
"reason": "completed"
}),
"2026-04-05T12:00:00Z",
)
.to_string(),
);
});
context.set_http_target(&server.base_url());
let mut filters = context.filters();
filters.push(ulid_filter());
let mut cmd = context.command();
cmd.args(["archive", "nightly-build"]);
fabro_snapshot!(filters, cmd, @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
[ULID]
");
resolve_mock.assert();
archive_mock.assert();
}