feat(nbd): add archive command and Closed status [1939a7]

Add Status::Closed variant serialised as "closed", providing a
soft-delete for tickets that should be hidden from normal listings.

- nbd archive <id>: sets status to closed (sugar for update --status closed)
- nbd list: excludes closed tickets by default (same as done)
- nbd list --all: bypasses default status exclusion, shows everything
- nbd list --filter status=closed: shows only closed tickets
- Closed tickets count as resolved for dependency purposes (unblock dependents)
- nbd ready / nbd next: closed tickets excluded from actionable set

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

@ -1,9 +1,8 @@
{ {
"id": "1939a7",
"title": "nbd archive command and Closed status", "title": "nbd archive command and Closed status",
"body": "Add `Status::Closed` (serialised as `\"closed\"`) and a convenience `nbd archive <id>` command that sets it.\n\n## Motivation\n\n`done` tickets clutter `nbd list`. `closed` provides a soft-delete: the ticket is preserved on disk but excluded from normal listings by default.\n\n## Approach\n\n### ticket.rs\n- Add `Closed` variant to `Status` enum (after `Done`).\n- `#[serde(rename_all = \"snake_case\")]` already handles serialisation → `\"closed\"`.\n\n### main.rs\n- Update `parse_status` to accept `\"closed\"`.\n- Update `status_str` in `display.rs` to map `Status::Closed` → `\"closed\"`.\n- Add `Archive` variant to `Commands`:\n ```\n Archive { id: String }\n ```\n- Implement `cmd_archive(id, json)`: read ticket → set status to `Closed` → write → print.\n This is syntactic sugar for `nbd update <id> --status closed`.\n\n### display.rs\n- Add `\"closed\"` to `status_str` match arm.\n\n### list filtering\n- `nbd list` currently shows all tickets. After this change, it should by default **hide** `Closed` tickets.\n- Add a `--all` flag to `nbd list` to show all tickets including closed ones.\n- Update `list_tickets` or filter at the command handler level. Prefer filtering in `cmd_list` to keep `list_tickets` generic.\n\n## Tests\n- Unit test: `Status::Closed` serialises to `\"closed\"` and back.\n- Integration test: `nbd archive <id>` sets status to `closed`.\n- Integration test: `nbd list` does not show closed tickets.\n- Integration test: `nbd list --all` shows closed tickets.\n\n## Files touched\n- `src/ticket.rs` — add `Closed` variant\n- `src/main.rs` — `Archive` command, `parse_status` update, `--all` flag on `list`\n- `src/display.rs` — `status_str` update\n- `src/tests.rs` — unit tests\n- `tests/integration.rs` — integration tests\n- `README.md` — document archive and --all", "body": "Add `Status::Closed` (serialised as `\"closed\"`) and a convenience `nbd archive <id>` command that sets it.\n\n## Motivation\n\n`done` tickets clutter `nbd list`. `closed` provides a soft-delete: the ticket is preserved on disk but excluded from normal listings by default.\n\n## Approach\n\n### ticket.rs\n- Add `Closed` variant to `Status` enum (after `Done`).\n- `#[serde(rename_all = \"snake_case\")]` already handles serialisation → `\"closed\"`.\n\n### main.rs\n- Update `parse_status` to accept `\"closed\"`.\n- Update `status_str` in `display.rs` to map `Status::Closed` → `\"closed\"`.\n- Add `Archive` variant to `Commands`:\n ```\n Archive { id: String }\n ```\n- Implement `cmd_archive(id, json)`: read ticket → set status to `Closed` → write → print.\n This is syntactic sugar for `nbd update <id> --status closed`.\n\n### display.rs\n- Add `\"closed\"` to `status_str` match arm.\n\n### list filtering\n- `nbd list` currently shows all tickets. After this change, it should by default **hide** `Closed` tickets.\n- Add a `--all` flag to `nbd list` to show all tickets including closed ones.\n- Update `list_tickets` or filter at the command handler level. Prefer filtering in `cmd_list` to keep `list_tickets` generic.\n\n## Tests\n- Unit test: `Status::Closed` serialises to `\"closed\"` and back.\n- Integration test: `nbd archive <id>` sets status to `closed`.\n- Integration test: `nbd list` does not show closed tickets.\n- Integration test: `nbd list --all` shows closed tickets.\n\n## Files touched\n- `src/ticket.rs` — add `Closed` variant\n- `src/main.rs` — `Archive` command, `parse_status` update, `--all` flag on `list`\n- `src/display.rs` — `status_str` update\n- `src/tests.rs` — unit tests\n- `tests/integration.rs` — integration tests\n- `README.md` — document archive and --all",
"priority": 6, "priority": 6,
"status": "todo", "status": "done",
"dependencies": [], "dependencies": [],
"ticket_type": "feature" "ticket_type": "feature"
} }

@ -17,7 +17,7 @@ Each ticket is a JSON file named `{id}.json`, where `id` is a unique
| `title` | string | *(required)* | | `title` | string | *(required)* |
| `body` | string | `""` | | `body` | string | `""` |
| `priority` | integer 010 | `5` | | `priority` | integer 010 | `5` |
| `status` | `todo` \| `in_progress` \| `done` | `todo` | | `status` | `todo` \| `in_progress` \| `done` \| `closed` | `todo` |
| `ticket_type` | `project` \| `feature` \| `task` \| `bug` | `task` | | `ticket_type` | `project` \| `feature` \| `task` \| `bug` | `task` |
| `dependencies` | list of ticket IDs | `[]` | | `dependencies` | list of ticket IDs | `[]` |
@ -51,16 +51,32 @@ nbd read a3f9c2 --json
### List all tickets ### List all tickets
By default, `done` tickets are excluded. By default, `done` and `closed` tickets are excluded.
```sh ```sh
nbd list # todo + in_progress only (done excluded) nbd list # todo + in_progress only (done + closed excluded)
nbd list --filter status=* # all tickets including done nbd list --all # all tickets including done and closed
nbd list --filter status=* # all tickets (alias for --all via filter)
nbd list --filter status=done # only completed tickets nbd list --filter status=done # only completed tickets
nbd list --filter type=bug # non-done bug tickets nbd list --filter status=closed # only archived tickets
nbd list --filter type=bug # non-done, non-closed bug tickets
nbd list --json nbd list --json
``` ```
### Archive a ticket
Soft-delete a ticket by setting its status to `closed`. The file is preserved
on disk but hidden from normal listings.
```sh
nbd archive a3f9c2
nbd archive a3f9c2 --json
```
`nbd archive` is shorthand for `nbd update <id> --status closed`. Archived
tickets still count as resolved for dependency purposes — any ticket that
depends on an archived ticket becomes unblocked.
### Update a ticket ### Update a ticket
Only the flags you supply are changed; all other fields retain their current Only the flags you supply are changed; all other fields retain their current
@ -114,9 +130,11 @@ nbd migrate --json # machine-readable summary
cargo run -- init cargo run -- init
cargo run -- create --title "Test ticket" --priority 7 --type bug cargo run -- create --title "Test ticket" --priority 7 --type bug
cargo run -- list cargo run -- list
cargo run -- list --all
cargo run -- ready cargo run -- ready
cargo run -- read <id> cargo run -- read <id>
cargo run -- update <id> --status in_progress cargo run -- update <id> --status in_progress
cargo run -- archive <id>
cargo run -- list --json cargo run -- list --json
``` ```

@ -34,6 +34,7 @@ fn status_str(status: &Status) -> &'static str {
Status::Todo => "todo", Status::Todo => "todo",
Status::InProgress => "in_progress", Status::InProgress => "in_progress",
Status::Done => "done", Status::Done => "done",
Status::Closed => "closed",
} }
} }

@ -23,6 +23,7 @@ fn status_str(status: &Status) -> &'static str {
Status::Todo => "todo", Status::Todo => "todo",
Status::InProgress => "in_progress", Status::InProgress => "in_progress",
Status::Done => "done", Status::Done => "done",
Status::Closed => "closed",
} }
} }

@ -76,9 +76,9 @@ enum Commands {
/// List tickets sorted by priority (highest first). /// List tickets sorted by priority (highest first).
/// ///
/// By default, tickets with status `done` are excluded. Use /// By default, tickets with status `done` or `closed` are excluded. Pass
/// `--filter status=*` to include all tickets, or /// `--all` to include all tickets, or `--filter status=*` to override only
/// `--filter status=done` to show only completed tickets. /// the status exclusion.
List { List {
/// Filter tickets by field: repeatable `key=value` pairs. /// Filter tickets by field: repeatable `key=value` pairs.
/// ///
@ -86,10 +86,18 @@ enum Commands {
/// Different keys are ANDed; the same key with multiple values is ORed. /// Different keys are ANDed; the same key with multiple values is ORed.
/// Values support glob wildcards: `title=*login*`. /// Values support glob wildcards: `title=*login*`.
/// ///
/// If no `status` filter is provided, tickets with status `done` are /// If no `status` filter is provided, tickets with status `done` or
/// excluded automatically. Provide `--filter status=*` to override. /// `closed` are excluded automatically. Provide `--filter status=*`
/// to override, or use `--all`.
#[arg(long = "filter", value_name = "KEY=VALUE")] #[arg(long = "filter", value_name = "KEY=VALUE")]
filter: Vec<String>, filter: Vec<String>,
/// Show all tickets regardless of status, including `done` and `closed`.
///
/// Equivalent to `--filter status=*` but takes precedence over any
/// `--filter` arguments when both are supplied.
#[arg(long)]
all: bool,
}, },
/// Initialise a new `.nbd/tickets/` store in the current directory. /// Initialise a new `.nbd/tickets/` store in the current directory.
@ -143,6 +151,18 @@ enum Commands {
filter: Vec<String>, filter: Vec<String>,
}, },
/// Archive a ticket by setting its status to `closed`.
///
/// The ticket is preserved on disk but excluded from normal `nbd list`
/// output. Use `nbd list --all` or `--filter status=closed` to view
/// archived tickets.
///
/// This is syntactic sugar for `nbd update <id> --status closed`.
Archive {
/// The 6-character hex ticket ID to archive.
id: String,
},
/// Update fields of an existing ticket and print the result. /// Update fields of an existing ticket and print the result.
/// ///
/// Only the flags you supply are changed; all other fields retain their /// Only the flags you supply are changed; all other fields retain their
@ -208,9 +228,11 @@ async fn dispatch(cli: Cli) -> store::Result<()> {
Commands::Migrate { dry_run, filter } => cmd_migrate(filter, dry_run, cli.json).await, Commands::Migrate { dry_run, filter } => cmd_migrate(filter, dry_run, cli.json).await,
Commands::Archive { id } => cmd_archive(id, cli.json).await,
Commands::Read { id } => cmd_read(id, cli.json).await, Commands::Read { id } => cmd_read(id, cli.json).await,
Commands::List { filter } => cmd_list(filter, cli.json).await, Commands::List { filter, all } => cmd_list(filter, all, cli.json).await,
Commands::Update { Commands::Update {
id, id,
@ -240,7 +262,7 @@ async fn dispatch(cli: Cli) -> store::Result<()> {
/// Parse a [`Status`] from its lowercase string representation. /// Parse a [`Status`] from its lowercase string representation.
/// ///
/// Accepts `"todo"`, `"in_progress"`, and `"done"`. /// Accepts `"todo"`, `"in_progress"`, `"done"`, and `"closed"`.
/// ///
/// # Errors /// # Errors
/// ///
@ -250,8 +272,9 @@ fn parse_status(s: &str) -> store::Result<Status> {
"todo" => Ok(Status::Todo), "todo" => Ok(Status::Todo),
"in_progress" => Ok(Status::InProgress), "in_progress" => Ok(Status::InProgress),
"done" => Ok(Status::Done), "done" => Ok(Status::Done),
"closed" => Ok(Status::Closed),
other => Err(format!( other => Err(format!(
"unknown status '{other}'; expected 'todo', 'in_progress', or 'done'" "unknown status '{other}'; expected 'todo', 'in_progress', 'done', or 'closed'"
) )
.into()), .into()),
} }
@ -347,10 +370,13 @@ async fn cmd_ready(filter_args: Vec<String>, json: bool) -> store::Result<()> {
let root = find_nbd_root()?; let root = find_nbd_root()?;
let all = list_tickets(&root).await?; let all = list_tickets(&root).await?;
// Build the set of IDs that are fully done. // Build the set of IDs that are resolved (done or closed).
// Closed tickets count as resolved for dependency purposes.
let done_ids: std::collections::HashSet<&str> = all let done_ids: std::collections::HashSet<&str> = all
.iter() .iter()
.filter(|t| t.status == crate::ticket::Status::Done) .filter(|t| {
t.status == crate::ticket::Status::Done || t.status == crate::ticket::Status::Closed
})
.map(|t| t.id.as_str()) .map(|t| t.id.as_str())
.collect(); .collect();
@ -358,6 +384,7 @@ async fn cmd_ready(filter_args: Vec<String>, json: bool) -> store::Result<()> {
.iter() .iter()
.filter(|t| { .filter(|t| {
t.status != crate::ticket::Status::Done t.status != crate::ticket::Status::Done
&& t.status != crate::ticket::Status::Closed
&& t.dependencies && t.dependencies
.iter() .iter()
.all(|dep| done_ids.contains(dep.as_str())) .all(|dep| done_ids.contains(dep.as_str()))
@ -390,12 +417,15 @@ async fn cmd_next(filter_args: Vec<String>, json: bool) -> store::Result<()> {
let done_ids: std::collections::HashSet<&str> = all let done_ids: std::collections::HashSet<&str> = all
.iter() .iter()
.filter(|t| t.status == crate::ticket::Status::Done) .filter(|t| {
t.status == crate::ticket::Status::Done || t.status == crate::ticket::Status::Closed
})
.map(|t| t.id.as_str()) .map(|t| t.id.as_str())
.collect(); .collect();
let next = all.iter().find(|t| { let next = all.iter().find(|t| {
t.status != crate::ticket::Status::Done t.status != crate::ticket::Status::Done
&& t.status != crate::ticket::Status::Closed
&& t.dependencies && t.dependencies
.iter() .iter()
.all(|dep| done_ids.contains(dep.as_str())) .all(|dep| done_ids.contains(dep.as_str()))
@ -503,24 +533,31 @@ async fn cmd_read(id: String, json: bool) -> store::Result<()> {
/// List tickets sorted by priority and print them. /// List tickets sorted by priority and print them.
/// ///
/// `filter_args` are optional `key=value` expressions that narrow the output. /// `filter_args` are optional `key=value` expressions that narrow the output.
/// `all` bypasses the default done/closed exclusion.
/// ///
/// **Default behaviour:** tickets with status [`Status::Done`] are excluded /// **Default behaviour:** tickets with status [`Status::Done`] or
/// unless the caller provides at least one `status=…` filter argument. /// [`Status::Closed`] are excluded unless the caller provides at least one
/// Pass `--filter status=*` to see all tickets including done ones. /// `status=…` filter argument or passes `--all`.
async fn cmd_list(filter_args: Vec<String>, json: bool) -> store::Result<()> { /// Pass `--filter status=*` or `--all` to see every ticket.
async fn cmd_list(filter_args: Vec<String>, all: bool, json: bool) -> store::Result<()> {
let filter = crate::filter::parse_filters(&filter_args)?; let filter = crate::filter::parse_filters(&filter_args)?;
let root = find_nbd_root()?; let root = find_nbd_root()?;
let tickets: Vec<Ticket> = list_tickets(&root) let tickets: Vec<Ticket> = list_tickets(&root)
.await? .await?
.into_iter() .into_iter()
.filter(|t| { .filter(|t| {
// If no status filter was provided, exclude done tickets by default. if all {
let status_ok = if filter.has_status_filter() { // --all: skip status exclusions; apply every other filter.
filter.matches_status(t) filter.matches(t)
} else { } else {
t.status != Status::Done // If no status filter was provided, exclude done and closed tickets by default.
}; let status_ok = if filter.has_status_filter() {
status_ok && filter.matches_except_status(t) filter.matches_status(t)
} else {
t.status != Status::Done && t.status != Status::Closed
};
status_ok && filter.matches_except_status(t)
}
}) })
.collect(); .collect();
@ -533,6 +570,26 @@ async fn cmd_list(filter_args: Vec<String>, json: bool) -> store::Result<()> {
Ok(()) Ok(())
} }
/// Archive a ticket by setting its status to [`Status::Closed`] and printing it.
///
/// The ticket is preserved on disk but excluded from normal `nbd list` output.
/// This is syntactic sugar for `nbd update <id> --status closed`.
async fn cmd_archive(id: String, json: bool) -> store::Result<()> {
let root = find_nbd_root()?;
let id = resolve_id(&root, &id).await?;
let mut ticket = read_ticket(&root, &id).await?;
ticket.status = Status::Closed;
write_ticket(&root, &ticket).await?;
if json {
display::print_ticket_json(&ticket);
} else {
display::print_ticket(&ticket);
}
Ok(())
}
/// Update the specified fields of an existing ticket, persist it, and print it. /// Update the specified fields of an existing ticket, persist it, and print it.
/// ///
/// Only the flags explicitly passed on the command line are applied; all other /// Only the flags explicitly passed on the command line are applied; all other

@ -72,6 +72,10 @@ mod ticket {
"\"in_progress\"" "\"in_progress\""
); );
assert_eq!(serde_json::to_string(&Status::Done).unwrap(), "\"done\""); assert_eq!(serde_json::to_string(&Status::Done).unwrap(), "\"done\"");
assert_eq!(
serde_json::to_string(&Status::Closed).unwrap(),
"\"closed\""
);
} }
/// `Status` deserialises correctly from lowercase snake_case strings. /// `Status` deserialises correctly from lowercase snake_case strings.
@ -89,6 +93,10 @@ mod ticket {
serde_json::from_str::<Status>("\"done\"").unwrap(), serde_json::from_str::<Status>("\"done\"").unwrap(),
Status::Done Status::Done
); );
assert_eq!(
serde_json::from_str::<Status>("\"closed\"").unwrap(),
Status::Closed
);
} }
/// `TicketType` variants serialise to the expected lowercase strings. /// `TicketType` variants serialise to the expected lowercase strings.
@ -811,7 +819,7 @@ mod filter {
assert!(!filter.matches_status(&ticket)); assert!(!filter.matches_status(&ticket));
} }
/// `matches_status` with `status=*` matches any status. /// `matches_status` with `status=*` matches any status including `closed`.
#[test] #[test]
fn matches_status_wildcard_matches_all() { fn matches_status_wildcard_matches_all() {
let args = vec!["status=*".to_string()]; let args = vec!["status=*".to_string()];
@ -819,6 +827,18 @@ mod filter {
assert!(filter.matches_status(&make_ticket(Status::Todo, TicketType::Task, 5, "T"))); assert!(filter.matches_status(&make_ticket(Status::Todo, TicketType::Task, 5, "T")));
assert!(filter.matches_status(&make_ticket(Status::InProgress, TicketType::Task, 5, "T"))); assert!(filter.matches_status(&make_ticket(Status::InProgress, TicketType::Task, 5, "T")));
assert!(filter.matches_status(&make_ticket(Status::Done, TicketType::Task, 5, "T"))); assert!(filter.matches_status(&make_ticket(Status::Done, TicketType::Task, 5, "T")));
assert!(filter.matches_status(&make_ticket(Status::Closed, TicketType::Task, 5, "T")));
}
/// `matches_status` with `status=closed` matches only closed tickets.
#[test]
fn matches_status_closed_pattern() {
let args = vec!["status=closed".to_string()];
let filter = parse_filters(&args).unwrap();
assert!(!filter.matches_status(&make_ticket(Status::Todo, TicketType::Task, 5, "T")));
assert!(!filter.matches_status(&make_ticket(Status::InProgress, TicketType::Task, 5, "T")));
assert!(!filter.matches_status(&make_ticket(Status::Done, TicketType::Task, 5, "T")));
assert!(filter.matches_status(&make_ticket(Status::Closed, TicketType::Task, 5, "T")));
} }
/// `matches_status` ORs multiple status patterns. /// `matches_status` ORs multiple status patterns.

@ -14,7 +14,7 @@ use serde::{Deserialize, Serialize};
/// The lifecycle status of a ticket. /// The lifecycle status of a ticket.
/// ///
/// Serializes to/from lowercase snake_case strings so that JSON files are /// Serializes to/from lowercase snake_case strings so that JSON files are
/// human-readable: `"todo"`, `"in_progress"`, `"done"`. /// human-readable: `"todo"`, `"in_progress"`, `"done"`, `"closed"`.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Status { pub enum Status {
@ -25,6 +25,12 @@ pub enum Status {
InProgress, InProgress,
/// The ticket has been completed. /// The ticket has been completed.
Done, Done,
/// The ticket has been archived (soft-deleted).
///
/// Closed tickets are preserved on disk but excluded from normal listings.
/// Use `nbd archive <id>` to close a ticket, or pass `--filter status=closed`
/// (or `--all`) to make them visible again.
Closed,
} }
/// The category of a ticket. /// The category of a ticket.

@ -964,6 +964,146 @@ fn list_filter_type_and_status_wildcard_includes_done_bugs() {
); );
} }
// ── nbd archive tests ─────────────────────────────────────────────────────────
/// `nbd archive <id>` sets the ticket status to `closed`.
#[test]
fn archive_sets_status_closed() {
let env = TestEnv::new();
let id = env.create(&["--title", "Archive me"]);
let output = env.run(&["archive", &id, "--json"]);
assert!(
output.status.success(),
"archive 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("should be valid JSON");
assert_eq!(
parsed["status"], "closed",
"archive should set status to closed"
);
}
/// `nbd list` does not show closed tickets by default.
#[test]
fn list_excludes_closed_by_default() {
let env = TestEnv::new();
let todo_id = env.create(&["--title", "Active ticket"]);
let closed_id = env.create(&["--title", "Closed ticket"]);
env.run(&["archive", &closed_id]);
let output = env.run(&["list", "--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 arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1, "only the active ticket should appear");
assert_eq!(arr[0]["id"], todo_id);
}
/// `nbd list --all` includes closed tickets.
#[test]
fn list_all_includes_closed() {
let env = TestEnv::new();
env.create(&["--title", "Active ticket"]);
let closed_id = env.create(&["--title", "Closed ticket"]);
env.run(&["archive", &closed_id]);
let output = env.run(&["list", "--all", "--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 arr = parsed.as_array().unwrap();
assert_eq!(
arr.len(),
2,
"--all should show all tickets including closed"
);
}
/// `nbd list --filter status=closed` shows only closed tickets.
#[test]
fn list_filter_status_closed_shows_only_closed() {
let env = TestEnv::new();
env.create(&["--title", "Active ticket"]);
let closed_id = env.create(&["--title", "Closed ticket"]);
env.run(&["archive", &closed_id]);
let output = env.run(&["list", "--filter", "status=closed", "--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 arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 1, "only the closed ticket should appear");
assert_eq!(arr[0]["id"], closed_id);
}
/// `nbd archive` with a prefix ID resolves and archives the ticket.
#[test]
fn archive_accepts_prefix() {
let env = TestEnv::new();
let id = env.create(&["--title", "Prefix archive"]);
let prefix = &id[..3];
let output = env.run(&["archive", prefix, "--json"]);
assert!(
output.status.success(),
"archive with prefix 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).unwrap();
assert_eq!(parsed["status"], "closed");
}
/// A closed dependency unblocks dependent tickets (closed counts as resolved).
#[test]
fn closed_dep_unblocks_dependent() {
let env = TestEnv::new();
let dep = env.create(&["--title", "Dep ticket"]);
env.run(&["create", "--title", "Dependent", "--deps", &dep, "--json"]);
// Archive (close) the dependency.
env.run(&["archive", &dep]);
// The dependent should now be ready.
let output = env.run(&["ready", "--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 arr = parsed.as_array().unwrap();
assert_eq!(
arr.len(),
1,
"dependent should be ready after dep is closed"
);
assert_eq!(arr[0]["title"], "Dependent");
}
/// `nbd ready` does not include closed tickets.
#[test]
fn ready_excludes_closed_tickets() {
let env = TestEnv::new();
let id = env.create(&["--title", "Will be closed"]);
env.run(&["archive", &id]);
let output = env.run(&["ready", "--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 arr = parsed.as_array().unwrap();
assert_eq!(arr.len(), 0, "closed tickets should not appear in ready");
}
// ── nbd next tests ──────────────────────────────────────────────────────────── // ── nbd next tests ────────────────────────────────────────────────────────────
/// `nbd next --json` returns the highest-priority ready ticket. /// `nbd next --json` returns the highest-priority ready ticket.

Loading…
Cancel
Save