test(nbd): add integration tests for nbd graph command [9c1f2c]

8 integration tests covering: empty store, independent tickets (no
indentation), chain rendering (└──), single-ticket subtree (excludes
unrelated), --json output (nodes/edges arrays), --json subtree scoping,
--filter narrowing, and 3-char prefix resolution.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
quotesdb
Elijah Voigt 3 months ago
parent b0359c4392
commit 84a75f4531

@ -0,0 +1,90 @@
+++
title = "Tests for nbd graph command"
priority = 5
status = "done"
ticket_type = "task"
dependencies = ["9ad11f"]
+++
Add comprehensive unit and integration tests for the `nbd graph` command introduced across tickets `9c9ebe`, `e14172`, and `9ad11f`.
## Unit tests (src/tests.rs)
These test `graph.rs` and the rendering helpers in `display.rs` in isolation using in-memory `Ticket` values (no temp files needed).
### graph.rs tests
```
test_graph_build_empty
TicketGraph::build(&[]) has no nodes.
test_graph_roots_no_deps
Two tickets with no dependencies → both appear in roots().
test_graph_roots_with_chain
Ticket B depends on A → only A is a root.
test_graph_subtree_linear
A → B → C; subtree("a") returns ["a", "b", "c"] (DFS order).
test_graph_subtree_single
subtree of a leaf returns just that ID.
test_graph_subtree_cycle
A.deps = [B], B.deps = [A] → subtree("a") returns both without panic/infinite loop.
test_graph_to_json_nodes_and_edges
Three tickets with two edges → JSON "nodes" has 3 entries, "edges" has 2.
```
### display.rs tests
```
test_format_graph_single_ticket
One ticket, no deps → output contains the ID, status, and title on one line; no connector chars.
test_format_graph_two_ticket_chain
A → B; output has A at col 0 and B indented with "└──".
test_format_graph_branching
A has two dependents B and C; B's line uses "├──" and C's uses "└──".
test_format_subtree_scope
A → B, C (unrelated root); format_subtree(graph, "a") does not contain C's ID.
test_format_graph_cycle_label
Cycle present → output contains "[cycle]" and does not repeat infinitely.
```
## Integration tests (tests/integration.rs)
Use a real temp directory set up with `cargo run -- init` and tickets created via `cmd_create`.
```
test_graph_empty_store
`nbd graph` on an empty store produces an empty line (or at least exits 0).
test_graph_all_no_deps
Create two tickets without deps; `nbd graph` output contains both IDs with no indentation.
test_graph_chain
Create A (no deps) and B (--deps A); `nbd graph` output shows A at top level and B indented.
test_graph_single_ticket
`nbd graph <id>` for A returns only A and its subtree, not unrelated tickets.
test_graph_json_output
`nbd graph --json` parses as valid JSON with "nodes" and "edges" arrays.
test_graph_json_subtree
`nbd graph <id> --json` returns JSON whose "nodes" array contains only reachable tickets.
test_graph_filter
`nbd graph --filter type=bug` only shows bug tickets and their reachable deps.
test_graph_partial_id
`nbd graph <3-char-prefix>` resolves to the correct ticket (prefix resolution).
```
## Files touched
- `src/tests.rs` — unit tests for `TicketGraph` and `format_graph` / `format_subtree`
- `tests/integration.rs` — CLI-level integration tests

@ -1634,3 +1634,165 @@ fn next_filter_no_match_returns_null() {
"no ticket has priority 99 so next should be null" "no ticket has priority 99 so next should be null"
); );
} }
// ── graph tests ───────────────────────────────────────────────────────────────
/// `nbd graph` on an empty store exits 0 and produces an empty (or
/// whitespace-only) line.
#[test]
fn graph_empty_store() {
let env = TestEnv::new();
let output = env.run(&["graph"]);
assert!(
output.status.success(),
"graph on empty store should exit 0: {}",
String::from_utf8_lossy(&output.stderr)
);
}
/// `nbd graph` with two independent tickets (no deps) shows both IDs with no
/// box-drawing indentation on either line.
#[test]
fn graph_all_no_deps() {
let env = TestEnv::new();
let id_a = env.create(&["--title", "Alpha"]);
let id_b = env.create(&["--title", "Beta"]);
let output = env.run(&["graph"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains(&id_a), "output should contain id_a");
assert!(stdout.contains(&id_b), "output should contain id_b");
// Neither ticket should have a connector (they are both roots).
let line_a = stdout.lines().find(|l| l.contains(&id_a)).unwrap();
let line_b = stdout.lines().find(|l| l.contains(&id_b)).unwrap();
assert!(
!line_a.contains("├──") && !line_a.contains("└──"),
"root ticket should have no connector: {line_a}"
);
assert!(
!line_b.contains("├──") && !line_b.contains("└──"),
"root ticket should have no connector: {line_b}"
);
}
/// When B depends on A, `nbd graph` shows A at the top level and B indented
/// below it with `└──`.
#[test]
fn graph_chain() {
let env = TestEnv::new();
let id_a = env.create(&["--title", "Alpha"]);
let id_b = env.create(&["--title", "Beta", "--deps", &id_a]);
let output = env.run(&["graph"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
// A should appear before B.
let pos_a = stdout.find(&id_a).expect("id_a not found");
let pos_b = stdout.find(&id_b).expect("id_b not found");
assert!(pos_a < pos_b, "root (A) should appear before dependent (B)");
// B's line should use `└──`.
let line_b = stdout.lines().find(|l| l.contains(&id_b)).unwrap();
assert!(line_b.contains("└──"), "child should use └── connector: {line_b}");
}
/// `nbd graph <id>` for ticket A shows A and its dependents but not unrelated
/// tickets.
#[test]
fn graph_single_ticket_subtree() {
let env = TestEnv::new();
let id_a = env.create(&["--title", "Alpha"]);
let id_b = env.create(&["--title", "Beta", "--deps", &id_a]);
let id_c = env.create(&["--title", "Gamma"]); // unrelated
let output = env.run(&["graph", &id_a]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains(&id_a), "root should be in subtree");
assert!(stdout.contains(&id_b), "dependent should be in subtree");
assert!(!stdout.contains(&id_c), "unrelated ticket should be absent: {stdout}");
}
/// `nbd graph --json` produces valid JSON with `nodes` and `edges` arrays.
#[test]
fn graph_json_output() {
let env = TestEnv::new();
let id_a = env.create(&["--title", "Alpha"]);
let id_b = env.create(&["--title", "Beta", "--deps", &id_a]);
let output = env.run(&["graph", "--json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("--json should produce valid JSON");
let nodes = parsed["nodes"].as_array().expect("nodes should be an array");
let edges = parsed["edges"].as_array().expect("edges should be an array");
let node_ids: Vec<&str> = nodes.iter().map(|n| n["id"].as_str().unwrap()).collect();
assert!(node_ids.contains(&id_a.as_str()), "nodes should include id_a");
assert!(node_ids.contains(&id_b.as_str()), "nodes should include id_b");
assert_eq!(edges.len(), 1, "should have exactly one edge (A blocks B)");
assert_eq!(edges[0]["from"].as_str().unwrap(), id_a);
assert_eq!(edges[0]["to"].as_str().unwrap(), id_b);
}
/// `nbd graph <id> --json` returns only nodes and edges reachable from <id>.
#[test]
fn graph_json_subtree() {
let env = TestEnv::new();
let id_a = env.create(&["--title", "Alpha"]);
let id_b = env.create(&["--title", "Beta", "--deps", &id_a]);
let id_c = env.create(&["--title", "Gamma"]); // unrelated
let output = env.run(&["graph", &id_a, "--json"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
let nodes = parsed["nodes"].as_array().unwrap();
let node_ids: Vec<&str> = nodes.iter().map(|n| n["id"].as_str().unwrap()).collect();
assert!(node_ids.contains(&id_a.as_str()));
assert!(node_ids.contains(&id_b.as_str()));
assert!(
!node_ids.contains(&id_c.as_str()),
"unrelated ticket should be excluded from JSON subtree"
);
}
/// `nbd graph --filter type=bug` only includes bug tickets and their dependents.
#[test]
fn graph_filter() {
let env = TestEnv::new();
let id_bug = env.create(&["--title", "A bug", "--type", "bug"]);
let id_task = env.create(&["--title", "A task", "--type", "task"]); // unrelated
let output = env.run(&["graph", "--filter", "type=bug"]);
assert!(output.status.success());
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains(&id_bug), "bug ticket should be visible");
assert!(!stdout.contains(&id_task), "task ticket should be excluded: {stdout}");
}
/// `nbd graph <3-char-prefix>` resolves to the correct ticket using prefix
/// matching.
#[test]
fn graph_partial_id() {
let env = TestEnv::new();
let id_a = env.create(&["--title", "Alpha"]);
let prefix = &id_a[..3];
let output = env.run(&["graph", prefix]);
assert!(
output.status.success(),
"graph with prefix should succeed: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains(&id_a), "resolved ticket should appear in output");
}

Loading…
Cancel
Save