use assert_cmd::Command;
use predicates::str::contains;
use serde_json::Value;
use std::path::Path;
use tempfile::TempDir;
fn kanban(dir: &TempDir) -> Command {
let mut cmd = Command::cargo_bin("agent-kanban").unwrap();
cmd.current_dir(dir);
cmd
}
fn kanban_at(dir: impl AsRef<Path>) -> Command {
let mut cmd = Command::cargo_bin("agent-kanban").unwrap();
cmd.current_dir(dir.as_ref());
cmd
}
fn run_json_at(dir: impl AsRef<Path>, args: &[&str]) -> Value {
let output = kanban_at(dir.as_ref()).args(args).output().unwrap();
assert!(
output.status.success(),
"command {:?} failed: stdout={} stderr={}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
panic!(
"invalid JSON from {:?}: {e}\nstdout={}",
args,
String::from_utf8_lossy(&output.stdout)
)
})
}
fn run_json(dir: &TempDir, args: &[&str]) -> Value {
let output = kanban(dir).args(args).output().unwrap();
assert!(
output.status.success(),
"command {:?} failed: stdout={} stderr={}",
args,
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).unwrap_or_else(|e| {
panic!(
"invalid JSON from {:?}: {e}\nstdout={}",
args,
String::from_utf8_lossy(&output.stdout)
)
})
}
fn init(dir: &TempDir) {
kanban(dir).arg("init").assert().success();
}
fn register(dir: &TempDir, name: &str) {
kanban(dir)
.args(["agent", "register", name])
.assert()
.success();
}
#[test]
fn golden_path_full_lifecycle() {
let dir = TempDir::new().unwrap();
kanban(&dir)
.arg("init")
.assert()
.success()
.stdout(contains("initialized"));
register(&dir, "agent-alpha");
let test_json = r#"{"describe":"basic add","input":"2+2","output":"4"}"#;
let created = run_json(
&dir,
&[
"add",
"--title",
"Implement feature X",
"--priority",
"high",
"--tag",
"backend",
"--tag",
"urgent-fix",
"--test",
test_json,
],
);
let task_id = created["id"].as_i64().unwrap();
assert_eq!(created["title"], "Implement feature X");
assert_eq!(created["priority"], "high");
assert_eq!(created["status"], "todo");
assert_eq!(created["executor"], Value::Null);
assert_eq!(created["tags"].as_array().unwrap().len(), 2);
assert_eq!(created["tests"].as_array().unwrap().len(), 1);
let listed = run_json(&dir, &["list"]);
let arr = listed.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"].as_i64().unwrap(), task_id);
let shown = run_json(&dir, &["show", &task_id.to_string()]);
assert_eq!(shown["title"], "Implement feature X");
assert_eq!(shown["tags"][0], "backend");
assert_eq!(shown["tags"][1], "urgent-fix");
assert_eq!(shown["tests"][0]["describe"], "basic add");
let claimed = run_json(
&dir,
&["claim", &task_id.to_string(), "--agent", "agent-alpha"],
);
assert_eq!(claimed["executor"], "agent-alpha");
assert_eq!(claimed["status"], "in_progress");
let moved = run_json(&dir, &["move", &task_id.to_string(), "--status", "review"]);
assert_eq!(moved["status"], "review");
kanban(&dir)
.args(["edit", &task_id.to_string(), "--title", "renamed"])
.assert()
.failure()
.stderr(contains("claimed"));
let released = run_json(&dir, &["release", &task_id.to_string()]);
assert_eq!(released["executor"], Value::Null);
assert_eq!(released["status"], "todo");
let edited = run_json(
&dir,
&["edit", &task_id.to_string(), "--title", "renamed task"],
);
assert_eq!(edited["title"], "renamed task");
let removed = run_json(&dir, &["remove", &task_id.to_string()]);
assert_eq!(removed["removed"].as_i64().unwrap(), task_id);
kanban(&dir)
.args(["show", &task_id.to_string()])
.assert()
.failure()
.stderr(contains("not found"));
let agent_removed = run_json(&dir, &["agent", "remove", "agent-alpha"]);
assert_eq!(agent_removed["removed"], "agent-alpha");
assert_eq!(agent_removed["released_tasks"].as_array().unwrap().len(), 0);
}
#[test]
fn registering_duplicate_agent_fails_cleanly() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "dup-agent");
kanban(&dir)
.args(["agent", "register", "dup-agent"])
.assert()
.failure()
.stderr(contains("already exists"));
}
#[test]
fn add_with_invalid_priority_fails() {
let dir = TempDir::new().unwrap();
init(&dir);
kanban(&dir)
.args([
"add",
"--title",
"bad task",
"--priority",
"urgentish",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
])
.assert()
.failure()
.stderr(contains("invalid priority"));
}
#[test]
fn add_with_test_missing_required_field_fails() {
let dir = TempDir::new().unwrap();
init(&dir);
kanban(&dir)
.args([
"add",
"--title",
"bad task",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i"}"#, ])
.assert()
.failure()
.stderr(contains("output"));
}
#[test]
fn claim_unregistered_agent_fails_and_leaves_executor_null() {
let dir = TempDir::new().unwrap();
init(&dir);
let created = run_json(
&dir,
&[
"add",
"--title",
"t1",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap();
kanban(&dir)
.args(["claim", &id.to_string(), "--agent", "ghost-agent"])
.assert()
.failure()
.stderr(contains("not registered"));
let shown = run_json(&dir, &["show", &id.to_string()]);
assert_eq!(shown["executor"], Value::Null);
assert_eq!(shown["status"], "todo");
}
#[test]
fn claim_on_already_claimed_task_fails() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "agent-a");
register(&dir, "agent-b");
let created = run_json(
&dir,
&[
"add",
"--title",
"t1",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap();
run_json(&dir, &["claim", &id.to_string(), "--agent", "agent-a"]);
kanban(&dir)
.args(["claim", &id.to_string(), "--agent", "agent-b"])
.assert()
.failure()
.stderr(contains("already claimed"));
let shown = run_json(&dir, &["show", &id.to_string()]);
assert_eq!(shown["executor"], "agent-a");
}
#[test]
fn edit_and_remove_blocked_while_claimed() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "agent-a");
let created = run_json(
&dir,
&[
"add",
"--title",
"t1",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap();
run_json(&dir, &["claim", &id.to_string(), "--agent", "agent-a"]);
kanban(&dir)
.args(["edit", &id.to_string(), "--title", "x"])
.assert()
.failure()
.stderr(contains("claimed"));
kanban(&dir)
.args(["remove", &id.to_string()])
.assert()
.failure()
.stderr(contains("claimed"));
}
#[test]
fn edit_and_remove_blocked_when_done() {
let dir = TempDir::new().unwrap();
init(&dir);
let created = run_json(
&dir,
&[
"add",
"--title",
"t1",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap();
run_json(&dir, &["move", &id.to_string(), "--status", "done"]);
kanban(&dir)
.args(["edit", &id.to_string(), "--title", "x"])
.assert()
.failure()
.stderr(contains("immutable"));
kanban(&dir)
.args(["remove", &id.to_string()])
.assert()
.failure()
.stderr(contains("can't be removed"));
}
#[test]
fn commands_before_init_fail_cleanly() {
let dir = TempDir::new().unwrap();
kanban(&dir)
.args(["agent", "list"])
.assert()
.failure()
.stderr(contains("agent-kanban init"));
kanban(&dir)
.args(["list"])
.assert()
.failure()
.stderr(contains("agent-kanban init"));
}
#[test]
fn agent_remove_cascade_releases_claimed_task() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "agent-a");
let created = run_json(
&dir,
&[
"add",
"--title",
"t1",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap();
run_json(&dir, &["claim", &id.to_string(), "--agent", "agent-a"]);
let removed = run_json(&dir, &["agent", "remove", "agent-a"]);
assert_eq!(removed["removed"], "agent-a");
assert_eq!(removed["released_tasks"][0].as_i64().unwrap(), id);
let shown = run_json(&dir, &["show", &id.to_string()]);
assert_eq!(shown["executor"], Value::Null);
assert_eq!(shown["status"], "todo");
}
#[test]
fn list_filtering_and_sorting() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "agent-a");
let t1 = run_json(
&dir,
&[
"add",
"--title",
"t1",
"--priority",
"urgent",
"--tag",
"alpha",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
run_json(
&dir,
&[
"add",
"--title",
"t2",
"--priority",
"low",
"--tag",
"beta",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
run_json(
&dir,
&[
"add",
"--title",
"t3",
"--priority",
"medium",
"--tag",
"alpha",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
run_json(
&dir,
&[
"add",
"--title",
"t4",
"--priority",
"high",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let t1_id = t1["id"].as_i64().unwrap();
run_json(&dir, &["claim", &t1_id.to_string(), "--agent", "agent-a"]);
let by_status = run_json(&dir, &["list", "--status", "in_progress"]);
let arr = by_status.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["title"], "t1");
let by_tag = run_json(&dir, &["list", "--tag", "alpha"]);
let arr = by_tag.as_array().unwrap();
let titles: Vec<&str> = arr.iter().map(|t| t["title"].as_str().unwrap()).collect();
assert_eq!(titles.len(), 2);
assert!(titles.contains(&"t1"));
assert!(titles.contains(&"t3"));
let by_priority = run_json(&dir, &["list", "--priority", "medium"]);
let arr = by_priority.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["title"], "t3");
let by_executor = run_json(&dir, &["list", "--executor", "agent-a"]);
let arr = by_executor.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["title"], "t1");
let sorted = run_json(&dir, &["list", "--sort", "priority"]);
let arr = sorted.as_array().unwrap();
let titles: Vec<&str> = arr.iter().map(|t| t["title"].as_str().unwrap()).collect();
assert_eq!(titles, vec!["t1", "t4", "t3", "t2"]);
}
#[test]
fn nested_project_discovery_child_wins() {
let root = TempDir::new().unwrap();
init(&root);
run_json(
&root,
&[
"add",
"--title",
"parent task",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let child = root.path().join("child");
std::fs::create_dir_all(&child).unwrap();
run_json_at(&child, &["init"]);
run_json_at(
&child,
&[
"add",
"--title",
"child task",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let deeper = child.join("deeper");
std::fs::create_dir_all(&deeper).unwrap();
let listed = run_json_at(&deeper, &["list"]);
let arr = listed.as_array().unwrap();
let titles: Vec<&str> = arr.iter().map(|t| t["title"].as_str().unwrap()).collect();
assert_eq!(titles, vec!["child task"]);
assert!(!titles.contains(&"parent task"));
}
#[test]
fn runs_from_subdirectory_of_initialized_project() {
let root = TempDir::new().unwrap();
init(&root);
let created = run_json(
&root,
&[
"add",
"--title",
"root task",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap();
let sub = root.path().join("subdir");
std::fs::create_dir_all(&sub).unwrap();
let listed = run_json_at(&sub, &["list"]);
let arr = listed.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"].as_i64().unwrap(), id);
assert_eq!(arr[0]["title"], "root task");
}
#[test]
fn pretty_flag_produces_indented_output() {
let dir = TempDir::new().unwrap();
init(&dir);
let test_json = r#"{"describe":"d","input":"i","output":"o"}"#;
let compact_output = kanban(&dir)
.args([
"add",
"--title",
"t",
"--priority",
"low",
"--test",
test_json,
])
.output()
.unwrap();
assert!(compact_output.status.success());
let compact_stdout = String::from_utf8_lossy(&compact_output.stdout).to_string();
assert_eq!(
compact_stdout.matches('\n').count(),
1,
"compact JSON should have exactly one trailing newline, got: {compact_stdout:?}"
);
let pretty_output = kanban(&dir)
.args([
"--pretty",
"add",
"--title",
"t2",
"--priority",
"low",
"--test",
test_json,
])
.output()
.unwrap();
assert!(pretty_output.status.success());
let pretty_stdout = String::from_utf8_lossy(&pretty_output.stdout).to_string();
assert!(
pretty_stdout.matches('\n').count() > 1,
"pretty JSON should be multi-line, got: {pretty_stdout:?}"
);
assert!(
pretty_stdout.contains("\n ") || pretty_stdout.contains("\n\t"),
"pretty JSON should contain indented lines, got: {pretty_stdout:?}"
);
}
#[test]
fn init_twice_is_idempotent_and_preserves_data() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "agent-a");
kanban(&dir).arg("init").assert().success();
let agents = run_json(&dir, &["agent", "list"]);
let arr = agents.as_array().unwrap();
let has_agent_a = arr.iter().any(|a| {
a.get("name")
.and_then(|n| n.as_str())
.unwrap_or_else(|| a.as_str().unwrap())
== "agent-a"
});
assert!(
has_agent_a,
"expected agent-a to survive a second init, got: {arr:?}"
);
}
#[test]
fn clap_parse_errors_are_still_json() {
let dir = TempDir::new().unwrap();
init(&dir);
let output = kanban(&dir).args(["show", "abc"]).output().unwrap();
assert!(!output.status.success());
assert_eq!(output.status.code(), Some(2));
let stderr: Value = serde_json::from_slice(&output.stderr).unwrap_or_else(|e| {
panic!(
"stderr was not valid JSON: {e}\nstderr={}",
String::from_utf8_lossy(&output.stderr)
)
});
assert!(stderr["error"].as_str().unwrap().contains("invalid digit"));
let output = kanban(&dir).args(["add", "--title", "t"]).output().unwrap();
assert!(!output.status.success());
let stderr: Value = serde_json::from_slice(&output.stderr).unwrap_or_else(|e| {
panic!(
"stderr was not valid JSON: {e}\nstderr={}",
String::from_utf8_lossy(&output.stderr)
)
});
assert!(stderr["error"].as_str().unwrap().contains("required"));
let output = kanban(&dir).args(["bogus-command"]).output().unwrap();
assert!(!output.status.success());
let stderr: Value = serde_json::from_slice(&output.stderr).unwrap_or_else(|e| {
panic!(
"stderr was not valid JSON: {e}\nstderr={}",
String::from_utf8_lossy(&output.stderr)
)
});
assert!(
stderr["error"]
.as_str()
.unwrap()
.contains("unrecognized subcommand")
);
}
#[test]
fn help_flag_remains_plain_text_not_json() {
let dir = TempDir::new().unwrap();
init(&dir);
let output = kanban(&dir).arg("--help").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("Usage:"));
assert!(
serde_json::from_str::<Value>(&stdout).is_err(),
"--help output should not be JSON: {stdout:?}"
);
}
#[test]
fn version_flag_is_registered_and_plain_text() {
let dir = TempDir::new().unwrap();
init(&dir);
let output = kanban(&dir).arg("--version").output().unwrap();
assert!(
output.status.success(),
"stdout={} stderr={}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("agent-kanban"));
assert!(
serde_json::from_str::<Value>(&stdout).is_err(),
"--version output should not be JSON: {stdout:?}"
);
}
#[test]
fn pretty_flag_applies_to_error_output_too() {
let dir = TempDir::new().unwrap();
init(&dir);
let compact = kanban(&dir).args(["show", "999"]).output().unwrap();
assert!(!compact.status.success());
let compact_stderr = String::from_utf8_lossy(&compact.stderr).to_string();
assert_eq!(
compact_stderr.matches('\n').count(),
1,
"compact error JSON should have exactly one trailing newline, got: {compact_stderr:?}"
);
let pretty = kanban(&dir)
.args(["--pretty", "show", "999"])
.output()
.unwrap();
assert!(!pretty.status.success());
let pretty_stderr = String::from_utf8_lossy(&pretty.stderr).to_string();
assert!(
pretty_stderr.matches('\n').count() > 1,
"pretty error JSON should be multi-line, got: {pretty_stderr:?}"
);
let parsed: Value = serde_json::from_str(&pretty_stderr).unwrap();
assert_eq!(parsed["error"], "task 999 not found");
}
#[test]
fn sql_special_characters_are_treated_as_inert_data() {
let dir = TempDir::new().unwrap();
init(&dir);
let evil_title = "robert'); DROP TABLE tasks; --";
let evil_tag = "'; DROP TABLE agents; --";
let created = run_json(
&dir,
&[
"add",
"--title",
evil_title,
"--priority",
"low",
"--tag",
evil_tag,
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
assert_eq!(created["title"], evil_title);
let id = created["id"].as_i64().unwrap();
let by_tag = run_json(&dir, &["list", "--tag", evil_tag]);
let arr = by_tag.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["id"].as_i64().unwrap(), id);
let shown = run_json(&dir, &["show", &id.to_string()]);
assert_eq!(shown["title"], evil_title);
run_json(&dir, &["agent", "register", "alice"]);
let all = run_json(&dir, &["list"]);
assert_eq!(all.as_array().unwrap().len(), 1);
}
#[test]
fn list_combines_filter_and_sort_in_one_call() {
let dir = TempDir::new().unwrap();
init(&dir);
for (title, priority, status) in [
("backlog-low", "low", "backlog"),
("todo-urgent", "urgent", "todo"),
("todo-high", "high", "todo"),
("todo-low", "low", "todo"),
] {
let created = run_json(
&dir,
&[
"add",
"--title",
title,
"--priority",
priority,
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
if status != "todo" {
let id = created["id"].as_i64().unwrap().to_string();
run_json(&dir, &["move", &id, "--status", status]);
}
}
let result = run_json(&dir, &["list", "--status", "todo", "--sort", "priority"]);
let arr = result.as_array().unwrap();
let titles: Vec<&str> = arr.iter().map(|t| t["title"].as_str().unwrap()).collect();
assert_eq!(titles, vec!["todo-urgent", "todo-high", "todo-low"]);
}
#[test]
fn table_flag_renders_list_as_aligned_table() {
let dir = TempDir::new().unwrap();
init(&dir);
run_json(
&dir,
&[
"add",
"--title",
"fix bug",
"--priority",
"high",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let output = kanban(&dir).args(["--table", "list"]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("TITLE"));
assert!(stdout.contains("PRIORITY"));
assert!(stdout.contains("fix bug"));
assert!(stdout.contains("high"));
assert!(
serde_json::from_str::<Value>(&stdout).is_err(),
"--table output should not be JSON: {stdout:?}"
);
}
#[test]
fn table_flag_renders_single_object_as_key_value_table() {
let dir = TempDir::new().unwrap();
init(&dir);
let created = run_json(
&dir,
&[
"add",
"--title",
"fix bug",
"--priority",
"high",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap().to_string();
let output = kanban(&dir)
.args(["--table", "show", &id])
.output()
.unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("FIELD"));
assert!(stdout.contains("VALUE"));
assert!(stdout.contains("title"));
assert!(stdout.contains("fix bug"));
}
#[test]
fn table_flag_renders_status_as_key_value_table_with_inline_agents() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "alice");
let output = kanban(&dir).args(["--table", "status"]).output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("FIELD"));
assert!(stdout.contains("VALUE"));
assert!(stdout.contains("total"));
assert!(stdout.contains("agents"));
assert!(stdout.contains(r#"{"alice":0}"#));
}
#[test]
fn table_flag_renders_empty_list_as_friendly_message() {
let dir = TempDir::new().unwrap();
init(&dir);
kanban(&dir)
.args(["--table", "list"])
.assert()
.success()
.stdout(contains("no results"));
}
#[test]
fn table_flag_renders_error_as_table() {
let dir = TempDir::new().unwrap();
init(&dir);
let output = kanban(&dir)
.args(["--table", "show", "999"])
.output()
.unwrap();
assert!(!output.status.success());
let stdout_and_stderr = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(stdout_and_stderr.contains("error"));
assert!(stdout_and_stderr.contains("not found"));
assert!(
serde_json::from_str::<Value>(&stdout_and_stderr).is_err(),
"--table error output should not be JSON: {stdout_and_stderr:?}"
);
}
#[test]
fn pretty_and_table_are_mutually_exclusive() {
let dir = TempDir::new().unwrap();
init(&dir);
let output = kanban(&dir)
.args(["--pretty", "--table", "list"])
.output()
.unwrap();
assert!(!output.status.success());
assert_eq!(output.status.code(), Some(2));
let stderr: Value = serde_json::from_slice(&output.stderr).unwrap();
assert!(
stderr["error"]
.as_str()
.unwrap()
.contains("cannot be used with")
);
}
#[test]
fn init_sets_schema_version_pragma() {
let dir = TempDir::new().unwrap();
init(&dir);
let db_path = dir.path().join(".kanban").join("board.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
let version: i32 = conn
.query_row("PRAGMA user_version", [], |row| row.get(0))
.unwrap();
assert_eq!(version, 1);
}
#[test]
fn opening_project_with_newer_schema_version_fails_cleanly() {
let dir = TempDir::new().unwrap();
init(&dir);
let db_path = dir.path().join(".kanban").join("board.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch("PRAGMA user_version = 999;").unwrap();
drop(conn);
kanban(&dir)
.args(["agent", "list"])
.assert()
.failure()
.stderr(contains("newer than this build"));
}
#[test]
fn status_reports_task_counts_and_agent_workload() {
let dir = TempDir::new().unwrap();
init(&dir);
register(&dir, "alice");
register(&dir, "bob");
register(&dir, "carol");
let test_json = r#"{"describe":"d","input":"i","output":"o"}"#;
for title in ["t1", "t2", "t3"] {
run_json(
&dir,
&[
"add",
"--title",
title,
"--priority",
"low",
"--test",
test_json,
],
);
}
kanban(&dir)
.args(["claim", "1", "--agent", "alice"])
.assert()
.success();
kanban(&dir)
.args(["claim", "2", "--agent", "bob"])
.assert()
.success();
kanban(&dir)
.args(["move", "2", "--status", "done"])
.assert()
.success();
let status = run_json(&dir, &["status"]);
assert_eq!(status["backlog"], 0);
assert_eq!(status["todo"], 1);
assert_eq!(status["in_progress"], 1);
assert_eq!(status["review"], 0);
assert_eq!(status["done"], 1);
assert_eq!(status["total"], 3);
assert_eq!(status["agents"]["alice"], 1);
assert_eq!(status["agents"]["bob"], 1);
assert_eq!(status["agents"]["carol"], 0);
}