fabro/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs
Bryan Helmkamp cf80fe567a
test(harness): scrub ambient creds from spawned fabro CLI
CLI integration tests spawned the real fabro binary while letting the
parent process's env pass through. The pr_view "no credentials" snapshot
failed in CI because the Nightly workflow's minted GITHUB_TOKEN was
inherited by the child and turned the expected "credentials required"
error into a real GitHub API call (404 / 401). On developer laptops the
same leak occurs whenever gh auth login is active.

Introduce apply_test_isolation(cmd, home) in fabro-test: env_clear() +
re-populate PATH, HOME, NO_COLOR, and the FABRO_* test overrides. Route
TestContext::command(), the internal server bootstrap, and the four
ad-hoc spawners in tests/it/cmd/{attach,render_graph,runner,server_start}
through the same helper so the isolation is systemic instead of
per-callsite. Tests that deliberately need a credential (OPENAI_API_KEY,
GITHUB_APP_PRIVATE_KEY, etc.) continue to set it explicitly on the
returned Command; those survive the clear.

Add a regression test that sets sentinel GITHUB_TOKEN and
ANTHROPIC_API_KEY in the parent, spawns /usr/bin/env through the helper,
and asserts the child sees neither credential while still seeing PATH
and the harness's FABRO_NO_UPGRADE_CHECK override.

Verified: the full workspace (4022 tests) passes with GITHUB_TOKEN and
ANTHROPIC_API_KEY set in the parent, which previously broke the
pr_view_reads_pull_request_from_store_without_pull_request_json
snapshot. cargo fmt and nightly clippy are clean.
2026-04-18 01:55:01 -04:00

107 lines
3.3 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."
)]
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_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}"
);
}