@ -2197,3 +2197,205 @@ fn version_flag_exits_zero_with_semver() {
"--version should contain '+' separator: {stdout}"
) ;
}
// ── Scoped next / ready tests ─────────────────────────────────────────────────
/// `nbd next <id>` returns the highest-priority ready dep within the subtree,
/// not the scoping ticket itself and not any unrelated ticket.
///
/// Graph: P → [A, B], A → [C]. C is done. So:
/// - A is ready (C is done)
/// - B is ready (no deps)
/// - P is blocked (A and B not done)
/// `nbd next P` with A at priority 8 and B at priority 3 should return A.
#[ test ]
fn test_next_scoped_by_id ( ) {
let env = TestEnv ::new ( ) ;
// Create leaf ticket C (done).
let c_id = env . create ( & [ "--title" , "C" , "--priority" , "5" , "--type" , "task" ] ) ;
env . run ( & [ "update" , & c_id , "--status" , "done" ] ) ;
// Create A (depends on C, priority 8).
let a_id = env . create ( & [
"--title" ,
"A" ,
"--priority" ,
"8" ,
"--type" ,
"task" ,
"--deps" ,
& c_id ,
] ) ;
// Create B (no deps, priority 3).
let b_id = env . create ( & [ "--title" , "B" , "--priority" , "3" , "--type" , "task" ] ) ;
// Create unrelated ticket U that should never appear.
env . create ( & [ "--title" , "Unrelated" , "--priority" , "10" , "--type" , "task" ] ) ;
// Create P (project, depends on A and B).
let deps = format! ( "{a_id},{b_id}" ) ;
let p_id = env . create ( & [
"--title" ,
"P" ,
"--priority" ,
"5" ,
"--type" ,
"project" ,
"--deps" ,
& deps ,
] ) ;
// `nbd next P --json` should return A (highest-priority ready dep of P).
let output = env . run ( & [ "next" , & p_id , "--json" ] ) ;
assert! (
output . status . success ( ) ,
"next scoped failed: {}" ,
String ::from_utf8_lossy ( & output . stderr )
) ;
let stdout = String ::from_utf8 ( output . stdout ) . unwrap ( ) ;
let parsed : serde_json ::Value =
serde_json ::from_str ( & stdout ) . expect ( "--json output should be valid JSON" ) ;
let next_id = parsed [ "next" ] [ "id" ]
. as_str ( )
. expect ( "next should be non-null" ) ;
assert_eq! (
next_id , a_id ,
"next scoped to P should return A, got {next_id}"
) ;
// P itself and Unrelated must not appear.
assert_ne! ( next_id , p_id , "scoping ticket must not be returned" ) ;
}
/// `nbd ready <id>` returns all ready deps within the subtree of `<id>`.
///
/// Graph: P → [A, B], A → [C]. C is done. B has no deps.
/// - A is ready (C done)
/// - B is ready (no deps)
/// `nbd ready P` should return exactly [A, B].
#[ test ]
fn test_ready_scoped_by_id ( ) {
let env = TestEnv ::new ( ) ;
let c_id = env . create ( & [ "--title" , "C" , "--priority" , "5" ] ) ;
env . run ( & [ "update" , & c_id , "--status" , "done" ] ) ;
let a_id = env . create ( & [ "--title" , "A" , "--priority" , "7" , "--deps" , & c_id ] ) ;
let b_id = env . create ( & [ "--title" , "B" , "--priority" , "4" ] ) ;
// Unrelated ticket with high priority — must not appear.
env . create ( & [ "--title" , "Unrelated" , "--priority" , "10" ] ) ;
let deps = format! ( "{a_id},{b_id}" ) ;
let p_id = env . create ( & [ "--title" , "P" , "--type" , "project" , "--deps" , & deps ] ) ;
let output = env . run ( & [ "ready" , & p_id , "--json" ] ) ;
assert! (
output . status . success ( ) ,
"ready scoped failed: {}" ,
String ::from_utf8_lossy ( & output . stderr )
) ;
let stdout = String ::from_utf8 ( output . stdout ) . unwrap ( ) ;
let parsed : serde_json ::Value =
serde_json ::from_str ( & stdout ) . expect ( "--json output should be valid JSON" ) ;
let arr = parsed
. as_array ( )
. expect ( "ready --json should return an array" ) ;
let ids : Vec < & str > = arr . iter ( ) . map ( | v | v [ "id" ] . as_str ( ) . unwrap ( ) ) . collect ( ) ;
assert! (
ids . contains ( & a_id . as_str ( ) ) ,
"A should be in scoped ready list"
) ;
assert! (
ids . contains ( & b_id . as_str ( ) ) ,
"B should be in scoped ready list"
) ;
assert! (
! ids . contains ( & p_id . as_str ( ) ) ,
"scoping ticket P must not appear in results"
) ;
// Unrelated should not appear.
assert_eq! (
ids . len ( ) ,
2 ,
"exactly A and B should be ready in subtree of P, got: {ids:?}"
) ;
}
/// `nbd next <id>` returns `null` when all deps of the scoping ticket are done.
#[ test ]
fn test_next_scoped_no_ready ( ) {
let env = TestEnv ::new ( ) ;
let a_id = env . create ( & [ "--title" , "A" , "--priority" , "8" ] ) ;
let b_id = env . create ( & [ "--title" , "B" , "--priority" , "5" ] ) ;
env . run ( & [ "update" , & a_id , "--status" , "done" ] ) ;
env . run ( & [ "update" , & b_id , "--status" , "done" ] ) ;
let deps = format! ( "{a_id},{b_id}" ) ;
let p_id = env . create ( & [ "--title" , "P" , "--type" , "project" , "--deps" , & deps ] ) ;
let output = env . run ( & [ "next" , & p_id , "--json" ] ) ;
assert! (
output . status . success ( ) ,
"next scoped (no ready) failed: {}" ,
String ::from_utf8_lossy ( & output . stderr )
) ;
let stdout = String ::from_utf8 ( output . stdout ) . unwrap ( ) ;
let parsed : serde_json ::Value =
serde_json ::from_str ( & stdout ) . expect ( "--json output should be valid JSON" ) ;
assert! (
parsed [ "next" ] . is_null ( ) ,
"next should be null when all deps are done, got: {}" ,
parsed [ "next" ]
) ;
}
/// `nbd ready <id> --filter` narrows within the scoped subtree.
///
/// Graph: P → [A (bug), B (task)]. Both ready.
/// `nbd ready P --filter type=bug` should return only A.
#[ test ]
fn test_ready_scoped_with_filter ( ) {
let env = TestEnv ::new ( ) ;
let a_id = env . create ( & [ "--title" , "A" , "--priority" , "6" , "--type" , "bug" ] ) ;
let b_id = env . create ( & [ "--title" , "B" , "--priority" , "6" , "--type" , "task" ] ) ;
// Unrelated bug — must not appear even though it matches the filter.
env . create ( & [
"--title" ,
"Unrelated bug" ,
"--priority" ,
"9" ,
"--type" ,
"bug" ,
] ) ;
let deps = format! ( "{a_id},{b_id}" ) ;
let p_id = env . create ( & [ "--title" , "P" , "--type" , "project" , "--deps" , & deps ] ) ;
let output = env . run ( & [ "ready" , & p_id , "--filter" , "type=bug" , "--json" ] ) ;
assert! (
output . status . success ( ) ,
"ready scoped+filtered failed: {}" ,
String ::from_utf8_lossy ( & output . stderr )
) ;
let stdout = String ::from_utf8 ( output . stdout ) . unwrap ( ) ;
let parsed : serde_json ::Value =
serde_json ::from_str ( & stdout ) . expect ( "--json output should be valid JSON" ) ;
let arr = parsed
. as_array ( )
. expect ( "ready --json should return an array" ) ;
let ids : Vec < & str > = arr . iter ( ) . map ( | v | v [ "id" ] . as_str ( ) . unwrap ( ) ) . collect ( ) ;
assert_eq! ( ids . len ( ) , 1 , "only A (bug) should match; got: {ids:?}" ) ;
assert_eq! ( ids [ 0 ] , a_id , "matched ticket should be A" ) ;
}