fabro/lib/crates/fabro-cli/tests/it/cmd/parent.rs
Bryan Helmkamp 0f1cf4da5c
feat(cli): wire run parent commands (#288)
## 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`

---

[![Compound
Engineering](https://img.shields.io/badge/Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)
Generated with GPT-5 via [Codex](https://openai.com/codex)
2026-05-16 15:15:28 -04:00

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();
}