mod common;
use common::{
TestEnv, command, command_with_db, contains_all, contains_none, extract_ref, fail, ok, suffix,
};
#[test]
fn version_flag_prints_package_version() {
let output = ok(command()
.arg("--version")
.output()
.expect("run aven --version"));
assert_eq!(output.trim(), format!("aven {}", env!("CARGO_PKG_VERSION")));
}
#[test]
fn list_json_supports_limit() {
let env = TestEnv::new();
let db = env.db("list-json.sqlite");
ok(env.aven(&db, ["add", "task one", "--project", "app"]));
ok(env.aven(&db, ["add", "task two", "--project", "app"]));
let output = ok(env.aven(&db, ["list", "--json", "--limit", "1"]));
let items: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(items.as_array().unwrap().len(), 1);
assert!(items[0]["ref"].is_string());
assert!(items[0]["title"].is_string());
assert!(items[0]["status"].is_string());
assert!(items[0]["project"].is_string());
}
#[test]
fn availability_preserves_attention_filters_and_explicit_discovery() {
let env = TestEnv::new();
let db = env.db("availability.sqlite");
let open_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"future open needle",
"--project",
"app",
"--available-at",
"2099-01-01T00:00:00Z",
],
)));
let done_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"future terminal needle",
"--project",
"app",
"--available-at",
"2099-01-02T00:00:00Z",
],
)));
ok(env.aven(&db, ["edit", &done_ref, "--status", "done"]));
let deleted_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"future deleted needle",
"--project",
"app",
"--available-at",
"2099-01-03T00:00:00Z",
],
)));
ok(env.aven(&db, ["delete", &deleted_ref]));
let regular = ok(env.aven(&db, ["list"]));
contains_none(
®ular,
&[
"future open needle",
"future terminal needle",
"future deleted needle",
],
);
let upcoming = ok(env.aven(&db, ["list", "--upcoming", "--json"]));
let items: serde_json::Value = serde_json::from_str(&upcoming).unwrap();
assert_eq!(items.as_array().unwrap().len(), 1);
assert_eq!(items[0]["ref"], open_ref);
assert_eq!(items[0]["available_at"], "2099-01-01T00:00:00Z");
let terminal = ok(env.aven(&db, ["list", "--status", "done"]));
contains_all(&terminal, &[&done_ref, "future terminal needle"]);
contains_none(&terminal, &["future open needle", "future deleted needle"]);
let deleted = ok(env.aven(&db, ["list", "--deleted"]));
contains_all(
&deleted,
&[&deleted_ref, "future deleted needle", "deleted=yes"],
);
contains_none(&deleted, &["future open needle", "future terminal needle"]);
let shown = ok(env.aven(&db, ["show", &open_ref]));
contains_all(
&shown,
&[
&open_ref,
"future open needle",
"available_at=2099-01-01T00:00:00Z",
],
);
let searched = ok(env.aven(&db, ["search", "future open needle"]));
contains_all(&searched, &[&open_ref, "future open needle"]);
contains_none(
&searched,
&["future terminal needle", "future deleted needle"],
);
ok(env.aven(&db, ["edit", &open_ref, "--clear-available-at"]));
let regular = ok(env.aven(&db, ["list", "--json"]));
let items: serde_json::Value = serde_json::from_str(®ular).unwrap();
assert_eq!(items.as_array().unwrap().len(), 1);
assert_eq!(items[0]["ref"], open_ref);
assert_eq!(items[0]["available_at"], "");
}
#[test]
fn due_dates_round_trip_filter_and_preserve_defer_semantics() {
let env = TestEnv::new();
let db = env.db("due-dates.sqlite");
let overdue_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"overdue task",
"--project",
"app",
"--due",
"2000-01-01",
],
)));
let newer_overdue_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"newer overdue task",
"--project",
"app",
"--due",
"2010-01-01",
],
)));
let future_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"future deadline",
"--project",
"app",
"--due",
"2099-01-01",
],
)));
let deferred_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"deferred overdue task",
"--project",
"app",
"--available-at",
"2099-02-01T00:00:00Z",
"--due",
"2000-02-01",
],
)));
let shown: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["show", &future_ref, "--json"]))).unwrap();
assert_eq!(shown["due_on"], "2099-01-01");
let overdue: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["list", "--overdue", "--json"]))).unwrap();
assert_eq!(overdue.as_array().unwrap().len(), 2);
assert_eq!(overdue[0]["ref"], overdue_ref);
assert_eq!(overdue[1]["ref"], newer_overdue_ref);
let mismatch: serde_json::Value = serde_json::from_str(&ok(
env.aven(&db, ["list", "--upcoming", "--overdue", "--json"])
))
.unwrap();
assert_eq!(mismatch.as_array().unwrap().len(), 1);
assert_eq!(mismatch[0]["ref"], deferred_ref);
assert_eq!(mismatch[0]["due_on"], "2000-02-01");
ok(env.aven(&db, ["edit", &future_ref, "--due", "2099-03-01"]));
let edited: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["show", &future_ref, "--json"]))).unwrap();
assert_eq!(edited["due_on"], "2099-03-01");
ok(env.aven(&db, ["edit", &future_ref, "--clear-due"]));
let cleared: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["show", &future_ref, "--json"]))).unwrap();
assert_eq!(cleared["due_on"], "");
}
#[test]
fn due_dates_accept_calendar_expressions_and_reject_times() {
let env = TestEnv::new();
let db = env.db("due-date-input.sqlite");
let task_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"fuzzy deadline",
"--project",
"app",
"--due",
"in 2 weeks",
],
)));
let shown: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["show", &task_ref, "--json"]))).unwrap();
assert_eq!(shown["due_on"].as_str().unwrap().len(), 10);
let error = fail(env.aven(&db, ["edit", &task_ref, "--due", "2099-01-01T09:00:00Z"]));
contains_all(&error, &["invalid-due", "ISO date"]);
}
#[test]
fn fuzzy_availability_is_shared_by_cli_add_and_edit() {
let env = TestEnv::new();
let db = env.db("fuzzy-availability.sqlite");
let task_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"fuzzy deferred task",
"--project",
"app",
"--available-at",
"in 2 weeks",
],
)));
let created: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["show", &task_ref, "--json"]))).unwrap();
let created_at = created["available_at"].as_str().unwrap();
assert_eq!(created_at.len(), 20);
assert!(created_at.ends_with('Z'));
ok(env.aven(
&db,
["edit", &task_ref, "--available-at", "next monday at 9am"],
));
let edited: serde_json::Value =
serde_json::from_str(&ok(env.aven(&db, ["show", &task_ref, "--json"]))).unwrap();
let edited_at = edited["available_at"].as_str().unwrap();
assert_eq!(edited_at.len(), 20);
assert!(edited_at.ends_with('Z'));
assert_ne!(edited_at, created_at);
let error = fail(env.aven(&db, ["edit", &task_ref, "--available-at", "monday"]));
contains_all(
&error,
&["ambiguous-available-at-weekday", "use next monday"],
);
}
#[test]
fn local_calendar_dates_use_offsets_across_daylight_saving_boundaries() {
let env = TestEnv::new();
let db = env.db("availability-timezones.sqlite");
let cases = [
(
"spring transition",
"2026-03-08",
"America/New_York",
"2026-03-08T05:00:00Z",
),
(
"fall transition",
"2026-11-01",
"America/New_York",
"2026-11-01T04:00:00Z",
),
(
"quarter hour offset",
"2026-07-16",
"Asia/Kathmandu",
"2026-07-15T18:15:00Z",
),
];
for (title, date, timezone, expected) in cases {
let created = ok(command_with_db(&db)
.env("TZ", timezone)
.env("XDG_STATE_HOME", env.state_dir())
.env("AVEN_CONFIG_DIR", env.config_dir().join("aven"))
.args(["add", title, "--project", "app", "--available-at", date])
.output()
.expect("run aven in timezone"));
let task_ref = extract_ref(&created);
let shown = ok(command_with_db(&db)
.env("TZ", timezone)
.env("XDG_STATE_HOME", env.state_dir())
.env("AVEN_CONFIG_DIR", env.config_dir().join("aven"))
.args(["show", &task_ref, "--json"])
.output()
.expect("show timezone task"));
let task: serde_json::Value = serde_json::from_str(&shown).unwrap();
assert_eq!(task["available_at"], expected, "timezone={timezone}");
}
}
#[test]
fn show_json_returns_single_task() {
let env = TestEnv::new();
let db = env.db("show-json.sqlite");
let task_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"show json test",
"--project",
"app",
"--priority",
"high",
],
)));
let output = ok(env.aven(&db, ["show", &task_ref, "--json"]));
let item: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(item["ref"], task_ref);
assert_eq!(item["title"], "show json test");
assert_eq!(item["priority"], "high");
assert!(item.get("task").is_none());
assert!(item.get("description").is_none());
}
#[test]
fn show_json_full_includes_description_dependencies_notes_conflicts() {
let env = TestEnv::new();
let db = env.db("show-json-full.sqlite");
let task_ref = extract_ref(&ok(env.aven(
&db,
[
"add",
"full json test",
"--project",
"app",
"--description",
"full json description",
],
)));
let output = ok(env.aven(&db, ["show", &task_ref, "--json", "--full"]));
let item: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(item["task"]["ref"], task_ref);
assert_eq!(item["description"], "full json description");
assert!(item["dependencies"]["depends_on"].is_array());
assert!(item["dependencies"]["blocks"].is_array());
assert!(item["notes"].is_array());
assert!(item["conflicts"].is_array());
}
#[test]
fn project_list_json_supports_limit() {
let env = TestEnv::new();
let db = env.db("project-list-json.sqlite");
ok(env.aven(&db, ["project", "create", "agent-offload"]));
ok(env.aven(&db, ["project", "create", "docs"]));
let output = ok(env.aven(&db, ["project", "list", "--json", "--limit", "1"]));
let items: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(items.as_array().unwrap().len(), 1);
assert!(items[0]["key"].is_string());
assert!(items[0]["prefix"].is_string());
}
#[test]
fn label_list_json_supports_limit() {
let env = TestEnv::new();
let db = env.db("label-list-json.sqlite");
ok(env.aven(&db, ["label", "create", "bug"]));
ok(env.aven(&db, ["label", "create", "sync"]));
let output = ok(env.aven(&db, ["label", "list", "--json", "--limit", "1"]));
let items: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(items.as_array().unwrap().len(), 1);
}
#[test]
fn creates_db_and_captures_task() {
let env = TestEnv::new();
let implicit = env.db("implicit.sqlite");
let output = command()
.env("AVEN_DB", &implicit)
.args(["project", "create", "implicit"])
.output()
.expect("run aven with AVEN_DB");
ok(output);
assert!(implicit.exists(), "implicit database was not created");
let db = env.db("local.sqlite");
ok(env.aven(&db, ["label", "create", "bug"]));
let created = ok(env.aven(
&db,
[
"add",
"fix sync conflict display",
"--project",
"app",
"--label",
"bug",
"--priority",
"high",
],
));
let task_ref = extract_ref(&created);
let bare = suffix(&task_ref);
contains_all(
&created,
&[
"created",
"APP-",
"ref=",
&format!("ref={bare}"),
"project=app",
"status=inbox",
"priority=high",
r#"title="fix sync conflict display""#,
],
);
let list = ok(env.aven(&db, ["list"]));
contains_all(
&list,
&[&task_ref, "status=inbox", "priority=high", "labels=bug"],
);
let shown = ok(env.aven(&db, ["show", &task_ref]));
contains_all(
&shown,
&[&task_ref, "status=inbox", "priority=high", "labels=bug"],
);
}
#[test]
fn updates_task_and_preserves_suffix_on_project_move() {
let env = TestEnv::new();
let db = env.db("move.sqlite");
ok(env.aven(&db, ["label", "create", "bug"]));
ok(env.aven(&db, ["label", "create", "sync"]));
let created = ok(env.aven(
&db,
[
"add",
"fix sync conflict display",
"--project",
"app",
"--label",
"bug",
],
));
let original = extract_ref(&created);
let original_suffix = suffix(&original);
let updated = ok(env.aven(
&db,
[
"edit",
&original,
"--title",
"fix conflict display",
"--status",
"active",
"--priority",
"urgent",
"--label",
"sync",
"--remove-label",
"bug",
"--project",
"homelab",
],
));
let moved = extract_ref(&updated);
contains_all(
&updated,
&[
"updated HML-",
"changed=yes",
"status=active",
"priority=urgent",
],
);
assert_eq!(
original_suffix,
suffix(&moved),
"project move changed suffix"
);
let shown = ok(env.aven(&db, ["show", &moved]));
contains_all(
&shown,
&[
&moved,
"status=active",
"priority=urgent",
"labels=sync",
r#"title="fix conflict display""#,
],
);
contains_none(&shown, &["labels=bug"]);
}
#[test]
fn renames_project_and_display_prefix() {
let env = TestEnv::new();
let db = env.db("rename-project.sqlite");
let created = ok(env.aven(
&db,
["add", "move project metadata", "--project", "agent-offload"],
));
let task_ref = extract_ref(&created);
contains_all(&created, &["project=agent-offload", "AO-"]);
let renamed = ok(env.aven(
&db,
[
"project",
"rename",
"agent-offload",
"sideagent",
"--prefix",
"SIDE",
],
));
contains_all(
&renamed,
&[
"renamed-project sideagent",
"changed=yes",
"old=agent-offload",
"old_prefix=AO",
"prefix=SIDE",
r#"name="sideagent""#,
],
);
let shown = ok(env.aven(&db, ["show", &task_ref]));
contains_all(&shown, &["SIDE-"]);
contains_none(&shown, &["agent-offload"]);
let filtered = ok(env.aven(&db, ["list", "--project", "sideagent"]));
contains_all(&filtered, &[&suffix(&task_ref), "move project metadata"]);
let projects = ok(env.aven(&db, ["project", "list"]));
contains_all(&projects, &["sideagent prefix=SIDE"]);
contains_none(&projects, &["agent-offload"]);
}
#[test]
fn singular_list_commands_list_workspace_values() {
let env = TestEnv::new();
let db = env.db("singular-list.sqlite");
ok(env.aven(&db, ["label", "create", "bug"]));
ok(env.aven(&db, ["project", "create", "agent-offload"]));
let projects = ok(env.aven(&db, ["project", "list", "--search", "agent"]));
contains_all(&projects, &["agent-offload prefix=AO"]);
contains_none(&projects, &["app prefix=APP"]);
let labels = ok(env.aven(&db, ["label", "list", "--search", "bu"]));
contains_all(&labels, &["bug"]);
contains_none(&labels, &["sync"]);
}
#[test]
fn project_rename_updates_managed_path_mapping() {
let env = TestEnv::new();
let db = env.db("rename-project-path.sqlite");
let project_dir = env.path("mapped-project");
std::fs::create_dir_all(&project_dir).unwrap();
ok(env.aven(&db, ["project", "create", "agent-offload"]));
ok(env.aven(
&db,
[
"project",
"path",
"add",
"agent-offload",
project_dir.to_str().unwrap(),
],
));
let renamed = ok(env.aven(
&db,
[
"project",
"rename",
"agent-offload",
"sideagent",
"--prefix",
"SIDE",
],
));
contains_all(&renamed, &["updated-config-project-mapping sideagent"]);
let config = std::fs::read_to_string(env.config_file()).unwrap();
contains_all(&config, &["project: sideagent"]);
contains_none(&config, &["project: agent-offload"]);
}
#[test]
fn project_rename_noop_does_not_claim_config_update() {
let env = TestEnv::new();
let db = env.db("rename-project-noop.sqlite");
let project_dir = env.path("mapped-project");
std::fs::create_dir_all(&project_dir).unwrap();
ok(env.aven(&db, ["project", "create", "agent-offload"]));
ok(env.aven(
&db,
[
"project",
"path",
"add",
"agent-offload",
project_dir.to_str().unwrap(),
],
));
let renamed = ok(env.aven(
&db,
[
"project",
"rename",
"agent-offload",
"agent-offload",
"--prefix",
"AO",
],
));
contains_all(&renamed, &["changed=none"]);
contains_none(&renamed, &["updated-config-project-mapping"]);
}
#[test]
fn delete_restore_and_filters_work() {
let env = TestEnv::new();
let db = env.db("filters.sqlite");
for label in ["bug", "sync", "docs"] {
ok(env.aven(&db, ["label", "create", label]));
}
ok(env.aven(&db, ["project", "create", "app"]));
ok(env.aven(&db, ["project", "create", "ops"]));
let app_bug = extract_ref(&ok(env.aven(
&db,
[
"add",
"app bug",
"--project",
"app",
"--label",
"bug",
"--priority",
"high",
],
)));
let app_docs = extract_ref(&ok(env.aven(
&db,
[
"add",
"app docs",
"--project",
"app",
"--label",
"docs",
"--priority",
"low",
],
)));
let ops_sync = extract_ref(&ok(env.aven(
&db,
[
"add",
"ops sync",
"--project",
"ops",
"--label",
"sync",
"--priority",
"urgent",
],
)));
ok(env.aven(&db, ["edit", &app_docs, "--status", "active"]));
ok(env.aven(&db, ["edit", &ops_sync, "--status", "done"]));
let by_project = ok(env.aven(&db, ["list", "--project", "app"]));
contains_all(&by_project, &["app bug", "app docs"]);
contains_none(&by_project, &["ops sync"]);
let by_status = ok(env.aven(&db, ["list", "--status", "active"]));
contains_all(&by_status, &["app docs"]);
contains_none(&by_status, &["app bug", "ops sync"]);
let by_priority = ok(env.aven(&db, ["list", "--priority", "urgent"]));
contains_all(&by_priority, &["ops sync"]);
contains_none(&by_priority, &["app bug", "app docs"]);
let by_label = ok(env.aven(&db, ["list", "--label", "bug"]));
contains_all(&by_label, &["app bug"]);
contains_none(&by_label, &["app docs", "ops sync"]);
ok(env.aven(&db, ["delete", &app_bug]));
let normal = ok(env.aven(&db, ["list"]));
contains_none(&normal, &[&app_bug, "app bug"]);
let all = ok(env.aven(&db, ["list", "--all"]));
contains_all(&all, &[&app_bug, "deleted=yes", "app bug", "app docs"]);
let deleted = ok(env.aven(&db, ["list", "--deleted"]));
contains_all(&deleted, &[&app_bug, "deleted=yes", "app bug"]);
contains_none(&deleted, &["app docs", "ops sync"]);
let all_project = ok(env.aven(&db, ["list", "--project", "app", "--all"]));
contains_all(&all_project, &[&app_bug, "deleted=yes", "app bug"]);
contains_none(&all_project, &["ops sync"]);
ok(env.aven(&db, ["restore", &app_bug]));
let restored = ok(env.aven(&db, ["list"]));
contains_all(&restored, &[&app_bug, "app bug"]);
}
#[test]
fn search_controls_deleted_visibility() {
let env = TestEnv::new();
let db = env.db("search-deleted.sqlite");
let live = extract_ref(&ok(
env.aven(&db, ["add", "live needle", "--project", "app"])
));
let deleted = extract_ref(&ok(
env.aven(&db, ["add", "deleted needle", "--project", "app"])
));
ok(env.aven(&db, ["delete", &deleted]));
let ordinary = ok(env.aven(&db, ["search", "needle"]));
contains_all(&ordinary, &[&live, "live needle"]);
contains_none(&ordinary, &[&deleted, "deleted needle", "deleted=yes"]);
let all = ok(env.aven(&db, ["search", "needle", "--all"]));
contains_all(&all, &[&live, &deleted, "deleted needle", "deleted=yes"]);
let by_ref = ok(env.aven(&db, ["search", &deleted]));
contains_all(
&by_ref,
&[&deleted, "deleted needle", "match=ref", "deleted=yes"],
);
let by_ref_json = ok(env.aven(&db, ["search", "--json", &deleted]));
let items: serde_json::Value = serde_json::from_str(&by_ref_json).unwrap();
assert_eq!(items[0]["ref"], serde_json::json!(deleted));
assert_eq!(items[0]["deleted"], serde_json::json!(true));
assert_eq!(items[0]["matched_field"], serde_json::json!("ref"));
}
#[test]
fn invalid_filter_values_fail() {
let env = TestEnv::new();
let db = env.db("bad-filter.sqlite");
let error = fail(env.aven(&db, ["list", "--status", "blocked"]));
contains_all(
&error,
&[
"error invalid-status",
"choices=inbox,backlog,todo,active,done,canceled",
],
);
}
#[test]
fn bulk_update_filters_and_removes_label() {
let env = TestEnv::new();
let db = env.db("bulk-update.sqlite");
for label in ["bug", "docs"] {
ok(env.aven(&db, ["label", "create", label]));
}
let bug_one = extract_ref(&ok(env.aven(
&db,
["add", "bug one", "--project", "app", "--label", "bug"],
)));
let bug_two = extract_ref(&ok(env.aven(
&db,
["add", "bug two", "--project", "app", "--label", "bug"],
)));
let docs = extract_ref(&ok(
env.aven(&db, ["add", "docs", "--project", "app", "--label", "docs"])
));
let dry_run = ok(env.aven(
&db,
[
"bulk-update",
"--filter-label",
"bug",
"--remove-label",
"bug",
"--dry-run",
],
));
contains_all(
&dry_run,
&[
"would-update",
"changed=yes",
"bulk-update-summary matched=2 changed=0 would_change=2 unchanged=0 dry_run=yes",
],
);
contains_all(&ok(env.aven(&db, ["show", &bug_one])), &["labels=bug"]);
let updated = ok(env.aven(
&db,
[
"bulk-update",
"--filter-label",
"bug",
"--remove-label",
"bug",
],
));
contains_all(
&updated,
&[
"bulk-updated",
"bulk-update-summary matched=2 changed=2 would_change=2 unchanged=0 dry_run=no",
],
);
contains_none(&ok(env.aven(&db, ["show", &bug_one])), &["labels=bug"]);
contains_none(&ok(env.aven(&db, ["show", &bug_two])), &["labels=bug"]);
contains_all(&ok(env.aven(&db, ["show", &docs])), &["labels=docs"]);
}
#[test]
fn bulk_update_sets_status_by_project_and_status() {
let env = TestEnv::new();
let db = env.db("bulk-update-status.sqlite");
ok(env.aven(&db, ["project", "create", "app"]));
ok(env.aven(&db, ["project", "create", "ops"]));
let app_todo = extract_ref(&ok(env.aven(&db, ["add", "app todo", "--project", "app"])));
let app_inbox = extract_ref(&ok(env.aven(&db, ["add", "app inbox", "--project", "app"])));
let ops_todo = extract_ref(&ok(env.aven(&db, ["add", "ops todo", "--project", "ops"])));
ok(env.aven(&db, ["edit", &app_todo, "--status", "todo"]));
ok(env.aven(&db, ["edit", &ops_todo, "--status", "todo"]));
let updated = ok(env.aven(
&db,
[
"bulk-update",
"--project",
"app",
"--status",
"todo",
"--set-status",
"active",
],
));
contains_all(
&updated,
&["bulk-update-summary matched=1 changed=1 would_change=1 unchanged=0 dry_run=no"],
);
contains_all(&ok(env.aven(&db, ["show", &app_todo])), &["status=active"]);
contains_all(&ok(env.aven(&db, ["show", &app_inbox])), &["status=inbox"]);
contains_all(&ok(env.aven(&db, ["show", &ops_todo])), &["status=todo"]);
}
#[test]
fn bulk_update_requires_selector_and_mutation() {
let env = TestEnv::new();
let db = env.db("bulk-update-guards.sqlite");
contains_all(
&fail(env.aven(&db, ["bulk-update", "--set-status", "done"])),
&["error bulk-update-requires-selector"],
);
contains_all(
&fail(env.aven(&db, ["bulk-update", "--all"])),
&["error bulk-update-requires-mutation"],
);
}
#[test]
fn bulk_update_all_excludes_deleted_unless_requested() {
let env = TestEnv::new();
let db = env.db("bulk-update-deleted.sqlite");
let live = extract_ref(&ok(env.aven(&db, ["add", "live", "--project", "app"])));
let deleted = extract_ref(&ok(env.aven(&db, ["add", "deleted", "--project", "app"])));
ok(env.aven(&db, ["delete", &deleted]));
let updated = ok(env.aven(&db, ["bulk-update", "--all", "--set-priority", "high"]));
contains_all(
&updated,
&["bulk-update-summary matched=1 changed=1 would_change=1 unchanged=0 dry_run=no"],
);
contains_all(&ok(env.aven(&db, ["show", &live])), &["priority=high"]);
contains_all(&ok(env.aven(&db, ["show", &deleted])), &["priority=none"]);
let included = ok(env.aven(
&db,
[
"bulk-update",
"--all",
"--include-deleted",
"--set-priority",
"urgent",
],
));
contains_all(
&included,
&["bulk-update-summary matched=2 changed=2 would_change=2 unchanged=0 dry_run=no"],
);
contains_all(&ok(env.aven(&db, ["show", &deleted])), &["priority=urgent"]);
}
#[test]
fn bulk_update_rejects_contradictory_label_mutation() {
let env = TestEnv::new();
let db = env.db("bulk-update-label-conflict.sqlite");
ok(env.aven(&db, ["label", "create", "bug"]));
ok(env.aven(&db, ["add", "bug", "--project", "app", "--label", "bug"]));
let error = fail(env.aven(
&db,
[
"bulk-update",
"--filter-label",
"bug",
"--label",
"bug",
"--remove-label",
"bug",
],
));
contains_all(&error, &["error bulk-update-label-conflict label=bug"]);
}
#[test]
fn bulk_update_ignores_duplicate_label_mutation_args() {
let env = TestEnv::new();
let db = env.db("bulk-update-duplicate-labels.sqlite");
ok(env.aven(&db, ["label", "create", "bug"]));
let task_ref = extract_ref(&ok(env.aven(&db, ["add", "task", "--project", "app"])));
let updated = ok(env.aven(
&db,
["bulk-update", "--all", "--label", "bug", "--label", "bug"],
));
contains_all(
&updated,
&["bulk-update-summary matched=1 changed=1 would_change=1 unchanged=0 dry_run=no"],
);
let noop = ok(env.aven(
&db,
["bulk-update", "--all", "--label", "bug", "--label", "bug"],
));
contains_all(
&noop,
&["bulk-update-summary matched=1 changed=0 would_change=0 unchanged=1 dry_run=no"],
);
contains_all(&ok(env.aven(&db, ["show", &task_ref])), &["labels=bug"]);
}