diff --git a/nbd/.claude/settings.json b/nbd/.claude/settings.json index 0b7ed38..c97511a 100644 --- a/nbd/.claude/settings.json +++ b/nbd/.claude/settings.json @@ -3,7 +3,9 @@ "allow": [ "Bash(cargo:*)", "Bash(git:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Edit", + "Write" ] } } diff --git a/nbd/.nbd/tickets/5f1495.md b/nbd/.nbd/tickets/5f1495.md index f70a163..af2fcab 100644 --- a/nbd/.nbd/tickets/5f1495.md +++ b/nbd/.nbd/tickets/5f1495.md @@ -1,7 +1,7 @@ +++ title = "nbd update diff output" priority = 5 -status = "todo" +status = "done" ticket_type = "feature" dependencies = [] +++ diff --git a/nbd/src/display.rs b/nbd/src/display.rs index 1f64097..3ab2208 100644 --- a/nbd/src/display.rs +++ b/nbd/src/display.rs @@ -266,6 +266,69 @@ pub fn print_migrate_report_json(report: &MigrateReport) { println!("{}", format_migrate_report_json(report)); } +/// Format a git-diff-style `- old / + new` summary of what changed between +/// two [`Ticket`] snapshots. +/// +/// Each field that differs produces two lines: +/// +/// ```text +/// - status: todo +/// + status: in_progress +/// ``` +/// +/// Fields compared: `title`, `body`, `priority`, `status`, `ticket_type`, and +/// `dependencies`. The `id` field is never shown because it cannot change. +/// +/// Returns `"(no changes)"` when every compared field is identical. +pub fn format_diff(old: &Ticket, new: &Ticket) -> String { + let mut lines: Vec = Vec::new(); + + macro_rules! diff_field { + ($label:expr, $old_val:expr, $new_val:expr) => {{ + let old_s: String = $old_val; + let new_s: String = $new_val; + if old_s != new_s { + lines.push(format!("- {:` for ticket A shows A and its dependents but not unrelated @@ -1714,7 +1717,10 @@ fn graph_single_ticket_subtree() { 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}"); + assert!( + !stdout.contains(&id_c), + "unrelated ticket should be absent: {stdout}" + ); } /// `nbd graph --json` produces valid JSON with `nodes` and `edges` arrays. @@ -1730,12 +1736,22 @@ fn graph_json_output() { 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 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!( + 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); @@ -1776,7 +1792,10 @@ fn graph_filter() { 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}"); + assert!( + !stdout.contains(&id_task), + "task ticket should be excluded: {stdout}" + ); } /// `nbd graph <3-char-prefix>` resolves to the correct ticket using prefix @@ -1794,5 +1813,80 @@ fn graph_partial_id() { 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"); + assert!( + stdout.contains(&id_a), + "resolved ticket should appear in output" + ); +} + +// ── update diff output tests ────────────────────────────────────────────────── + +/// `nbd update --status in_progress` (no `--json`) prints `- status:` and +/// `+ status:` lines showing what changed. +#[test] +fn update_no_json_prints_diff() { + let env = TestEnv::new(); + let id = env.create(&["--title", "Diff test"]); + + let output = env.run(&["update", &id, "--status", "in_progress"]); + assert!( + output.status.success(), + "update failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("- status:"), + "should show old status line: {stdout}" + ); + assert!( + stdout.contains("+ status:"), + "should show new status line: {stdout}" + ); + assert!(stdout.contains("todo"), "should show old value: {stdout}"); + assert!( + stdout.contains("in_progress"), + "should show new value: {stdout}" + ); +} + +/// `nbd update --json` still prints full JSON (no diff). +#[test] +fn update_with_json_flag_prints_full_ticket() { + let env = TestEnv::new(); + let id = env.create(&["--title", "JSON update test"]); + + let output = env.run(&["update", &id, "--status", "in_progress", "--json"]); + assert!( + output.status.success(), + "update --json failed: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + // Must be valid JSON containing the updated ticket. + let parsed: serde_json::Value = + serde_json::from_str(&stdout).expect("--json output should be valid JSON"); + assert_eq!(parsed["status"], "in_progress"); + assert_eq!(parsed["title"], "JSON update test"); + // Should NOT contain diff markers. + assert!( + !stdout.contains("- status:"), + "JSON output should not contain diff markers: {stdout}" + ); +} + +/// `nbd update` with no changes prints `(no changes)`. +#[test] +fn update_no_changes_prints_no_changes() { + let env = TestEnv::new(); + let id = env.create(&["--title", "Unchanged"]); + + // Update with no field changes (supply same status). + let output = env.run(&["update", &id, "--status", "todo"]); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains("(no changes)"), + "should print '(no changes)' when nothing changed: {stdout}" + ); }