mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-03 02:24:33 +00:00
The CLI's integration tests seeded runs by appending legacy run events and waited on legacy event names. Now every seeded run is a real dry run: the fixtures start the run through the CLI, read the run id from its output and wait for the stream's terminal lifecycle record. Waits, assertions and snapshots read `RunStreamItem`s (`run.finished`, the platform `run.lifecycle` record, `derived.parsed.kind == "question"`). Test changes: - support.rs: `run_completed_dry_run`, `wait_for_run_finished`, `wait_for_lifecycle`, `wait_for_stream_item`; the `append_seeded_*` writers, `wait_for_event_names` and the git-backed seeded fixtures are gone (the checkpoint patch is not in the projection yet). - diff.rs keeps only the help test; inspect.rs drops the git-backed checkpoint test; events.rs, dump.rs, create.rs, attach.rs and dry_run_examples.rs snapshots are re-recorded over Petri's rendering with redactions for epoch millis, digests and commit shas. - run.rs: the remote foreground mock serves stream pages and a run state with a conclusion and a `report` stage response; the event history test checks `run.finished` and the terminal lifecycle item. - runner.rs / attach.rs: question ids containing `#` are percent-encoded in answer URLs. Production fixes the ports surfaced: - petri_worker.rs: a cancelled run exits without reporting a failure. - runner.rs: resuming a run that already finished fails its precondition instead of starting a worker. Left failing on purpose, each bound to a Petri-side gap reported to the lead rather than to the port: sandbox_cp (4), sandbox_preview and sandbox_ssh (the projection carries no sandbox instance), the artifact collection tests in workflow::artifacts and run.rs (no artifact collection for Petri runs yet), and the two dump blob-ref tests (blob refs are not visible in the inspect output). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
241 lines
7.3 KiB
Rust
241 lines
7.3 KiB
Rust
use fabro_test::test_context;
|
|
use serde_json::Value;
|
|
|
|
use super::support::{fixture, output_stderr, output_stdout, setup_seeded_completed_dry_run};
|
|
|
|
#[test]
|
|
fn completion_rejects_json() {
|
|
let context = test_context!();
|
|
let output = context
|
|
.command()
|
|
.args(["--json", "completion", "zsh"])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(!output.status.success());
|
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
|
assert!(stderr.contains("--json is not supported for this command"));
|
|
}
|
|
|
|
#[test]
|
|
fn settings_json_outputs_parseable_json() {
|
|
let context = test_context!();
|
|
let output = context
|
|
.settings()
|
|
.arg("--json")
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let value: Value =
|
|
serde_json::from_slice(&output.stdout).expect("settings --json should parse");
|
|
assert!(value.is_object());
|
|
}
|
|
|
|
#[test]
|
|
fn settings_uses_json_output_format_from_home_config() {
|
|
let context = test_context!();
|
|
context.write_home(
|
|
".fabro/settings.toml",
|
|
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
|
);
|
|
|
|
let output = context.settings().output().expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let value: Value =
|
|
serde_json::from_slice(&output.stdout).expect("settings config JSON should parse");
|
|
assert!(value.is_object());
|
|
}
|
|
|
|
#[test]
|
|
fn auth_status_ignores_json_output_format_from_home_config() {
|
|
let context = test_context!();
|
|
context.write_home(
|
|
".fabro/settings.toml",
|
|
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
|
);
|
|
|
|
let output = context
|
|
.command()
|
|
.args(["auth", "status"])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
assert!(
|
|
output.stdout.is_empty(),
|
|
"stdout should stay empty in text mode"
|
|
);
|
|
let stderr = output_stderr(&output);
|
|
assert!(stderr.contains("Not logged in to any servers."));
|
|
assert!(!stderr.contains("FABRO_DEV_TOKEN:"));
|
|
}
|
|
|
|
#[test]
|
|
fn secret_list_uses_json_output_format_from_home_config() {
|
|
let context = test_context!();
|
|
context.write_home(
|
|
".fabro/settings.toml",
|
|
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
|
);
|
|
|
|
let output = context
|
|
.command()
|
|
.args(["secret", "list"])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let value: Value =
|
|
serde_json::from_slice(&output.stdout).expect("secret list config JSON should parse");
|
|
assert!(value.is_array(), "secret list JSON should be an array");
|
|
}
|
|
|
|
#[test]
|
|
fn variable_list_uses_json_output_format_from_home_config() {
|
|
let context = test_context!();
|
|
context.write_home(
|
|
".fabro/settings.toml",
|
|
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
|
);
|
|
|
|
let output = context
|
|
.command()
|
|
.args(["variable", "list"])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let value: Value =
|
|
serde_json::from_slice(&output.stdout).expect("variable list config JSON should parse");
|
|
assert!(value.is_array(), "variable list JSON should be an array");
|
|
}
|
|
|
|
#[test]
|
|
fn completion_succeeds_with_json_output_format_from_home_config() {
|
|
let context = test_context!();
|
|
context.write_home(
|
|
".fabro/settings.toml",
|
|
"_version = 1\n\n[cli.output]\nformat = \"json\"\n",
|
|
);
|
|
|
|
let output = context
|
|
.command()
|
|
.args(["completion", "zsh"])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let stdout = output_stdout(&output);
|
|
assert!(stdout.contains("#compdef"));
|
|
assert!(serde_json::from_slice::<Value>(&output.stdout).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn ps_supports_global_flag_and_env_var() {
|
|
let context = test_context!();
|
|
setup_seeded_completed_dry_run(&context);
|
|
let test_case_label = context.test_case_label();
|
|
|
|
let global_output = context
|
|
.command()
|
|
.args(["--json", "ps", "-a", "--label", &test_case_label])
|
|
.output()
|
|
.expect("command should run");
|
|
assert!(global_output.status.success());
|
|
let global_runs: Value =
|
|
serde_json::from_slice(&global_output.stdout).expect("global --json should parse");
|
|
assert!(global_runs.as_array().is_some_and(|runs| !runs.is_empty()));
|
|
|
|
let env_output = context
|
|
.command()
|
|
.env("FABRO_JSON", "1")
|
|
.args(["ps", "-a", "--label", &test_case_label])
|
|
.output()
|
|
.expect("command should run");
|
|
assert!(env_output.status.success());
|
|
let env_runs: Value =
|
|
serde_json::from_slice(&env_output.stdout).expect("FABRO_JSON output should parse");
|
|
|
|
let normalize = |runs: &Value| {
|
|
let mut rows = runs
|
|
.as_array()
|
|
.expect("ps output should be an array")
|
|
.iter()
|
|
.map(|run| {
|
|
let nullable_string = |field: &str| {
|
|
run.get(field)
|
|
.unwrap_or_else(|| panic!("{field} should be present"))
|
|
.as_str()
|
|
.map(str::to_string)
|
|
};
|
|
(
|
|
run["run_id"]
|
|
.as_str()
|
|
.expect("run_id should be present")
|
|
.to_string(),
|
|
nullable_string("workflow_name"),
|
|
nullable_string("workflow_graph_name"),
|
|
nullable_string("workflow_slug"),
|
|
run["goal"]
|
|
.as_str()
|
|
.expect("goal should be present")
|
|
.to_string(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
rows.sort_unstable();
|
|
rows
|
|
};
|
|
|
|
assert_eq!(normalize(&global_runs), normalize(&env_runs));
|
|
}
|
|
|
|
#[test]
|
|
fn events_json_wins_over_pretty() {
|
|
let context = test_context!();
|
|
let run = setup_seeded_completed_dry_run(&context);
|
|
|
|
let output = context
|
|
.command()
|
|
.args(["--json", "events", "--pretty", &run.run_id])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let stdout = String::from_utf8(output.stdout).unwrap();
|
|
let first_line = stdout.lines().find(|line| !line.is_empty()).unwrap();
|
|
let value: Value = serde_json::from_str(first_line).expect("events output should remain JSONL");
|
|
assert!(value.get("stream_seq").is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn graph_json_without_output_is_rejected() {
|
|
let context = test_context!();
|
|
let workflow = fixture("simple.fabro");
|
|
|
|
let output = context
|
|
.command()
|
|
.args(["--json", "graph", workflow.to_str().unwrap()])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(!output.status.success());
|
|
let stderr = output_stderr(&output);
|
|
assert!(stderr.contains("--json is not supported for this command"));
|
|
}
|
|
|
|
#[test]
|
|
fn secret_list_json_missing_env_outputs_json_array() {
|
|
let context = test_context!();
|
|
let output = context
|
|
.command()
|
|
.args(["--json", "secret", "list"])
|
|
.output()
|
|
.expect("command should run");
|
|
|
|
assert!(output.status.success());
|
|
let value: Value = serde_json::from_slice(&output.stdout).expect("secret list should parse");
|
|
assert!(value.is_array(), "secret list JSON should be an array");
|
|
}
|