mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
## Summary Add CLI support for run parent relationships now that the server API can store them. This lets users create child runs, filter children, inspect parent metadata, and link or unlink parents without dropping to raw API calls. ## What Changed - Added top-level `fabro parent link` and `fabro parent unlink` commands with selector resolution, text output, and JSON summaries. - Added `--parent` to `fabro run`, `fabro create`, and `fabro ps`; create/run send `parent_id` in manifests and `ps` uses server-side parent filtering. - Surfaced `parent_id` in `ps --json` and `inspect`, with a conditional `PARENT` column for unfiltered tables. - Extended `fabro-client` parent-link APIs and `list_store_runs(parent_id)`. ## Test Plan - `cargo nextest run -p fabro-cli` - `cargo nextest run -p fabro-client` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-cli -p fabro-client --all-targets -- -D warnings` - `cargo insta pending-snapshots` - `git diff --check` --- [](https://github.com/EveryInc/compound-engineering-plugin) Generated with GPT-5 via [Codex](https://openai.com/codex)
198 lines
5.9 KiB
Rust
198 lines
5.9 KiB
Rust
use fabro_test::{fabro_snapshot, test_context};
|
|
use httpmock::MockServer;
|
|
use serde_json::Value;
|
|
|
|
use super::support::{mock_resolved_run, remote_run_summary_json};
|
|
use crate::support::unique_run_id;
|
|
|
|
#[test]
|
|
fn help() {
|
|
let context = test_context!();
|
|
let mut cmd = context.command();
|
|
cmd.args(["parent", "--help"]);
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
Manage run parent links
|
|
|
|
Usage: fabro parent [OPTIONS] <COMMAND>
|
|
|
|
Commands:
|
|
link Link or replace a run's orchestration parent
|
|
unlink Unlink a run from its orchestration parent
|
|
help Print this message or the help of the given subcommand(s)
|
|
|
|
Options:
|
|
--json Output as JSON [env: FABRO_JSON=]
|
|
--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 parent_link_resolves_selectors_calls_endpoint_and_prints_link() {
|
|
let context = test_context!();
|
|
let server = MockServer::start();
|
|
let child_id = unique_run_id();
|
|
let parent_id = unique_run_id();
|
|
|
|
let child_resolve = mock_resolved_run(&server, "child-build", &child_id);
|
|
let parent_resolve = mock_resolved_run(&server, "parent-build", &parent_id);
|
|
let link_mock = server.mock(|when, then| {
|
|
when.method("PUT")
|
|
.path(format!("/api/v1/runs/{child_id}/parent"))
|
|
.header("content-type", "application/json")
|
|
.json_body(serde_json::json!({
|
|
"parent_id": parent_id
|
|
}));
|
|
let mut summary = remote_run_summary_json(
|
|
&child_id,
|
|
"Nightly Build",
|
|
"nightly-build",
|
|
"Nightly run",
|
|
&serde_json::json!({
|
|
"kind": "succeeded",
|
|
"reason": "completed"
|
|
}),
|
|
"2026-04-05T12:00:00Z",
|
|
);
|
|
summary["parent_id"] = serde_json::json!(parent_id);
|
|
then.status(200)
|
|
.header("Content-Type", "application/json")
|
|
.json_body(summary);
|
|
});
|
|
|
|
let mut cmd = context.command();
|
|
cmd.args([
|
|
"parent",
|
|
"link",
|
|
"--server",
|
|
&server.base_url(),
|
|
"child-build",
|
|
"parent-build",
|
|
]);
|
|
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
Linked parent: [ULID] -> [ULID]
|
|
----- stderr -----
|
|
");
|
|
|
|
child_resolve.assert();
|
|
parent_resolve.assert();
|
|
link_mock.assert();
|
|
}
|
|
|
|
#[test]
|
|
fn parent_link_json_prints_updated_run_summary() {
|
|
let context = test_context!();
|
|
let server = MockServer::start();
|
|
let child_id = unique_run_id();
|
|
let parent_id = unique_run_id();
|
|
|
|
let child_resolve = mock_resolved_run(&server, "child-build", &child_id);
|
|
let parent_resolve = mock_resolved_run(&server, "parent-build", &parent_id);
|
|
let link_mock = server.mock(|when, then| {
|
|
when.method("PUT")
|
|
.path(format!("/api/v1/runs/{child_id}/parent"))
|
|
.json_body(serde_json::json!({
|
|
"parent_id": parent_id
|
|
}));
|
|
let mut summary = remote_run_summary_json(
|
|
&child_id,
|
|
"Nightly Build",
|
|
"nightly-build",
|
|
"Nightly run",
|
|
&serde_json::json!({
|
|
"kind": "succeeded",
|
|
"reason": "completed"
|
|
}),
|
|
"2026-04-05T12:00:00Z",
|
|
);
|
|
summary["parent_id"] = serde_json::json!(parent_id);
|
|
then.status(200)
|
|
.header("Content-Type", "application/json")
|
|
.json_body(summary);
|
|
});
|
|
|
|
let output = context
|
|
.command()
|
|
.args([
|
|
"--json",
|
|
"parent",
|
|
"link",
|
|
"--server",
|
|
&server.base_url(),
|
|
"child-build",
|
|
"parent-build",
|
|
])
|
|
.output()
|
|
.expect("parent link should execute");
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"parent link failed:\nstdout:\n{}\nstderr:\n{}",
|
|
String::from_utf8_lossy(&output.stdout),
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let summary: Value = serde_json::from_slice(&output.stdout).expect("JSON should parse");
|
|
assert_eq!(summary["id"], child_id);
|
|
assert_eq!(summary["parent_id"], parent_id);
|
|
|
|
child_resolve.assert();
|
|
parent_resolve.assert();
|
|
link_mock.assert();
|
|
}
|
|
|
|
#[test]
|
|
fn parent_unlink_resolves_selector_calls_endpoint_and_prints_unlinked_child() {
|
|
let context = test_context!();
|
|
let server = MockServer::start();
|
|
let child_id = unique_run_id();
|
|
|
|
let child_resolve = mock_resolved_run(&server, "child-build", &child_id);
|
|
let unlink_mock = server.mock(|when, then| {
|
|
when.method("DELETE")
|
|
.path(format!("/api/v1/runs/{child_id}/parent"));
|
|
then.status(200)
|
|
.header("Content-Type", "application/json")
|
|
.json_body(remote_run_summary_json(
|
|
&child_id,
|
|
"Nightly Build",
|
|
"nightly-build",
|
|
"Nightly run",
|
|
&serde_json::json!({
|
|
"kind": "succeeded",
|
|
"reason": "completed"
|
|
}),
|
|
"2026-04-05T12:00:00Z",
|
|
));
|
|
});
|
|
|
|
let mut cmd = context.command();
|
|
cmd.args([
|
|
"parent",
|
|
"unlink",
|
|
"--server",
|
|
&server.base_url(),
|
|
"child-build",
|
|
]);
|
|
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
Unlinked parent: [ULID]
|
|
----- stderr -----
|
|
");
|
|
|
|
child_resolve.assert();
|
|
unlink_mock.assert();
|
|
}
|