use assert_cmd::Command;
use serde_json::Value;
use tempfile::TempDir;
fn kanban() -> Command {
Command::cargo_bin("agent-kanban").unwrap()
}
fn run_json(db_path: &std::path::Path, args: &[&str]) -> Value {
let mut full_args = vec!["--db", db_path.to_str().unwrap()];
full_args.extend_from_slice(args);
let output = kanban().args(&full_args).output().unwrap();
assert!(
output.status.success(),
"command {:?} failed: stdout={} stderr={}",
full_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={}",
full_args,
String::from_utf8_lossy(&output.stdout)
)
})
}
#[test]
fn db_flag_creates_exact_path_and_parent_dirs() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("nested").join("my-board.db");
assert!(!db_path.parent().unwrap().exists());
run_json(&db_path, &["init"]);
assert!(db_path.is_file());
let mut cmd = kanban();
cmd.current_dir(&dir).arg("list");
cmd.assert()
.failure()
.stderr(predicates::str::contains("not a kanban project"));
}
#[test]
fn db_flag_supports_full_lifecycle() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("board.db");
run_json(&db_path, &["init"]);
run_json(&db_path, &["agent", "register", "alice"]);
let created = run_json(
&db_path,
&[
"add",
"--title",
"t",
"--priority",
"low",
"--test",
r#"{"describe":"d","input":"i","output":"o"}"#,
],
);
let id = created["id"].as_i64().unwrap().to_string();
let claimed = run_json(&db_path, &["claim", &id, "--agent", "alice"]);
assert_eq!(claimed["executor"], "alice");
assert_eq!(claimed["status"], "in_progress");
let listed = run_json(&db_path, &["list"]);
assert_eq!(listed.as_array().unwrap().len(), 1);
}
#[test]
fn db_flag_on_nonexistent_file_fails_cleanly_without_creating_it() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("does-not-exist.db");
let mut cmd = kanban();
cmd.args(["--db", db_path.to_str().unwrap(), "list"]);
cmd.assert()
.failure()
.stderr(predicates::str::contains("not found"));
assert!(
!db_path.exists(),
"a failed lookup must not leave behind an empty database file"
);
}
#[test]
#[cfg(unix)]
fn db_flag_init_propagates_parent_dir_creation_failure() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let readonly_parent = dir.path().join("readonly");
std::fs::create_dir(&readonly_parent).unwrap();
std::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o555)).unwrap();
let db_path = readonly_parent.join("nested").join("board.db");
let mut cmd = kanban();
cmd.args(["--db", db_path.to_str().unwrap(), "init"]);
cmd.assert()
.failure()
.stderr(predicates::str::contains("Permission denied"));
std::fs::set_permissions(&readonly_parent, std::fs::Permissions::from_mode(0o755)).unwrap();
}
#[test]
#[cfg(unix)]
fn db_flag_init_propagates_non_lock_errors_without_retrying() {
use std::os::unix::fs::PermissionsExt;
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("existing.db");
std::fs::File::create(&db_path).unwrap();
std::fs::set_permissions(&db_path, std::fs::Permissions::from_mode(0o444)).unwrap();
let mut cmd = kanban();
cmd.args(["--db", db_path.to_str().unwrap(), "init"]);
cmd.assert()
.failure()
.stderr(predicates::str::contains("readonly"));
std::fs::set_permissions(&db_path, std::fs::Permissions::from_mode(0o644)).unwrap();
}
#[test]
fn db_flag_composes_with_pretty() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("board.db");
let mut init_cmd = kanban();
init_cmd.args(["--db", db_path.to_str().unwrap(), "init"]);
init_cmd.assert().success();
let mut cmd = kanban();
cmd.args([
"--pretty",
"--db",
db_path.to_str().unwrap(),
"agent",
"list",
]);
let output = cmd.output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains('\n'),
"expected pretty multi-line output, got: {stdout:?}"
);
}
#[test]
fn global_flags_work_after_the_subcommand_too() {
let dir = TempDir::new().unwrap();
let db_path = dir.path().join("board.db");
let mut init_cmd = kanban();
init_cmd.args(["init", "--db", db_path.to_str().unwrap()]);
init_cmd.assert().success();
assert!(db_path.is_file());
let mut cmd = kanban();
cmd.args([
"agent",
"register",
"alice",
"--db",
db_path.to_str().unwrap(),
"--pretty",
]);
let output = cmd.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('\n'),
"expected pretty multi-line output, got: {stdout:?}"
);
}