mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-20 00:11:34 +00:00
## Summary Graph rendering now accepts documented Fabro dotted DOT attributes end to end while keeping raw Graphviz calls behind `fabro-graphviz`. The new `RenderableDot` boundary applies Fabro render styling and normalization before raw SVG rendering, and both the CLI subprocess and server path now route through that typed boundary instead of calling `graphviz_sys` directly outside the graphviz crate. The branch also adds a small curated DOT compatibility corpus covering ACP agent attributes, human default choices, and subworkflow manager attributes. Those fixtures are exercised by both render and validation tests, and the run overview now shows graph render errors directly instead of falling through to the empty graph state. ## Verification - `cargo nextest run -p fabro-graphviz` - `cargo nextest run -p fabro-validate` - `cargo nextest run -p fabro-cli render_graph` - `cargo nextest run -p fabro-server render_graph_from_manifest_accepts_fabro_dotted_attributes` - `cargo +nightly-2026-04-14 fmt --check --all` - `cargo +nightly-2026-04-14 clippy -p fabro-graphviz -p fabro-cli -p fabro-server -p fabro-validate --all-targets -- -D warnings` - `rg -n "graphviz_sys" lib/crates/fabro-cli lib/crates/fabro-server` - `cd apps/fabro-web && bun test` - `cd apps/fabro-web && bun run typecheck` --- [](https://github.com/EveryInc/compound-engineering-plugin) Generated with GPT-5 via [Codex](https://openai.com/codex) --------- Co-authored-by: Jess Martin <27258+jessmartin@users.noreply.github.com>
152 lines
4.5 KiB
Rust
152 lines
4.5 KiB
Rust
#![expect(
|
|
clippy::disallowed_methods,
|
|
reason = "These CLI integration tests intentionally spawn the real fabro binary and stream DOT over stdio to verify the internal render subprocess contract."
|
|
)]
|
|
#![expect(
|
|
clippy::disallowed_types,
|
|
reason = "integration tests write DOT to the spawned child's stdin via std::io::Write"
|
|
)]
|
|
|
|
use std::io::Write;
|
|
use std::process::{Command, Stdio};
|
|
|
|
use fabro_test::{fabro_snapshot, test_context};
|
|
|
|
fn render_graph_command(context: &fabro_test::TestContext) -> Command {
|
|
let mut cmd = Command::new(env!("CARGO_BIN_EXE_fabro"));
|
|
fabro_test::apply_test_isolation(&mut cmd, &context.home_dir);
|
|
cmd.current_dir(&context.temp_dir);
|
|
cmd
|
|
}
|
|
|
|
#[test]
|
|
fn help() {
|
|
let context = test_context!();
|
|
let mut cmd = context.command();
|
|
cmd.args(["__render-graph", "--help"]);
|
|
fabro_snapshot!(context.filters(), cmd, @"
|
|
success: true
|
|
exit_code: 0
|
|
----- stdout -----
|
|
Render a DOT graph to SVG (internal)
|
|
|
|
Usage: fabro __render-graph [OPTIONS]
|
|
|
|
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 render_graph_outputs_svg() {
|
|
let context = test_context!();
|
|
let mut cmd = render_graph_command(&context);
|
|
cmd.args(["__render-graph"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut child = cmd.spawn().expect("render-graph subprocess should spawn");
|
|
child
|
|
.stdin
|
|
.as_mut()
|
|
.expect("stdin should be piped")
|
|
.write_all(b"digraph { a -> b }")
|
|
.expect("stdin write should succeed");
|
|
|
|
let output = child
|
|
.wait_with_output()
|
|
.expect("render-graph subprocess should exit");
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stderr: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be valid UTF-8");
|
|
assert!(
|
|
stdout.contains("<svg"),
|
|
"expected SVG output, got: {}",
|
|
&stdout[..stdout.len().min(200)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn render_graph_accepts_fabro_dotted_attributes() {
|
|
let context = test_context!();
|
|
let mut cmd = render_graph_command(&context);
|
|
cmd.args(["__render-graph"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut child = cmd.spawn().expect("render-graph subprocess should spawn");
|
|
child
|
|
.stdin
|
|
.as_mut()
|
|
.expect("stdin should be piped")
|
|
.write_all(
|
|
br#"digraph X {
|
|
start [shape=Mdiamond]
|
|
exit [shape=Msquare]
|
|
a [label="A", acp.command="codex"]
|
|
start -> a -> exit
|
|
}"#,
|
|
)
|
|
.expect("stdin write should succeed");
|
|
|
|
let output = child
|
|
.wait_with_output()
|
|
.expect("render-graph subprocess should exit");
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stderr: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be valid UTF-8");
|
|
assert!(
|
|
stdout.contains("<svg"),
|
|
"expected SVG output, got: {}",
|
|
&stdout[..stdout.len().min(200)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn render_graph_bad_input_uses_render_error_protocol() {
|
|
let context = test_context!();
|
|
let mut cmd = render_graph_command(&context);
|
|
cmd.args(["__render-graph"])
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut child = cmd.spawn().expect("render-graph subprocess should spawn");
|
|
child
|
|
.stdin
|
|
.as_mut()
|
|
.expect("stdin should be piped")
|
|
.write_all(b"not valid dot")
|
|
.expect("stdin write should succeed");
|
|
|
|
let output = child
|
|
.wait_with_output()
|
|
.expect("render-graph subprocess should exit");
|
|
|
|
assert!(
|
|
output.status.success(),
|
|
"stderr: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be valid UTF-8");
|
|
assert!(
|
|
stdout.starts_with("RENDER_ERROR:"),
|
|
"expected render error protocol, got: {stdout}"
|
|
);
|
|
}
|