mod common;
use common::{TestEnv, command, 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 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,
[
"update",
&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, ["update", &app_docs, "--status", "active"]));
ok(env.aven(&db, ["update", &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, ["update", &app_todo, "--status", "todo"]));
ok(env.aven(&db, ["update", &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"]);
}