mod common;
#[test]
fn test_edit_completed_task_is_rejected() {
let (dir, bin) = common::setup_test_project("edit_terminal");
common::init_and_agent(&dir, &bin);
common::run_cmd(
&dir,
&bin,
&["task", "create", "--title", "done deal", "--json"],
);
let id = task_display_id(&dir, &bin, "done deal");
common::run_cmd_as(&dir, &bin, "tester", &["task", "claim", &id, "--json"]);
let complete = common::run_cmd_as(&dir, &bin, "tester", &["task", "complete", &id, "--json"]);
assert!(complete.status.success(), "complete should succeed");
let retitle = common::run_cmd(
&dir,
&bin,
&[
"task",
"edit",
&id,
"--title",
"renamed after death",
"--json",
],
);
assert!(
!retitle.status.success(),
"editing a completed task must fail"
);
let stderr = String::from_utf8_lossy(&retitle.stderr);
assert!(
stderr.contains("STATE_CONFLICT"),
"terminal edit must report STATE_CONFLICT: {stderr}"
);
common::run_cmd(
&dir,
&bin,
&["task", "create", "--title", "dead on arrival", "--json"],
);
let id2 = task_display_id(&dir, &bin, "dead on arrival");
common::run_cmd(
&dir,
&bin,
&["task", "cancel", &id2, "--reason", "not needed", "--json"],
);
let reprioritize = common::run_cmd(
&dir,
&bin,
&["task", "edit", &id2, "--priority", "urgent", "--json"],
);
assert!(
!reprioritize.status.success(),
"editing a cancelled task must fail"
);
}
#[test]
fn test_edit_can_clear_optional_fields() {
let (dir, bin) = common::setup_test_project("edit_clear_optional");
common::init_and_agent(&dir, &bin);
let create = common::run_cmd(
&dir,
&bin,
&[
"task",
"create",
"--title",
"clearable",
"--description",
"original text",
"--required-role",
"reviewer",
"--json",
],
);
assert!(create.status.success(), "create failed");
let id = task_display_id(&dir, &bin, "clearable");
let clear = common::run_cmd(
&dir,
&bin,
&[
"task",
"edit",
&id,
"--description",
"",
"--required-role",
"",
"--json",
],
);
assert!(
clear.status.success(),
"clearing optional fields must succeed: {}",
String::from_utf8_lossy(&clear.stderr)
);
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&clear.stdout)).expect("valid json");
assert_eq!(value["data"]["description"], serde_json::Value::Null);
assert_eq!(value["data"]["required_role"], serde_json::Value::Null);
let keep = common::run_cmd(
&dir,
&bin,
&["task", "edit", &id, "--priority", "high", "--json"],
);
assert!(keep.status.success());
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&keep.stdout)).expect("valid json");
assert_eq!(value["data"]["description"], serde_json::Value::Null);
}
#[test]
fn test_edit_audit_event_records_required_role() {
let (dir, bin) = common::setup_test_project("edit_audit_role");
common::init_and_agent(&dir, &bin);
let create = common::run_cmd(
&dir,
&bin,
&[
"task",
"create",
"--title",
"audited",
"--required-role",
"implementer",
"--json",
],
);
assert!(create.status.success(), "create failed");
let id = task_display_id(&dir, &bin, "audited");
let edit = common::run_cmd(
&dir,
&bin,
&["task", "edit", &id, "--required-role", "reviewer", "--json"],
);
assert!(edit.status.success(), "edit failed");
let events = common::run_cmd(
&dir,
&bin,
&["event", "list", "--event-type", "task.edited", "--json"],
);
assert!(events.status.success(), "event list failed");
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&events.stdout)).expect("valid json");
let payload = &value["data"]["events"][0]["payload"];
assert_eq!(payload["before"]["required_role"], "implementer");
assert_eq!(payload["after"]["required_role"], "reviewer");
}
#[test]
fn test_title_length_cap_rejected() {
let (dir, bin) = common::setup_test_project("title_cap");
common::init_and_agent(&dir, &bin);
let long_title = "x".repeat(201);
let out = common::run_cmd(
&dir,
&bin,
&["task", "create", "--title", &long_title, "--json"],
);
assert!(!out.status.success(), "201-char title must be rejected");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("200"),
"error must mention the cap: {stderr}"
);
let ok_title = "y".repeat(200);
let out = common::run_cmd(
&dir,
&bin,
&["task", "create", "--title", &ok_title, "--json"],
);
assert!(
out.status.success(),
"200-char title must be accepted: {}",
String::from_utf8_lossy(&out.stderr)
);
}
#[test]
fn test_transition_events_store_canonical_actor_ulid() {
let (dir, bin) = common::setup_test_project("actor_canonical");
common::init_and_agent(&dir, &bin);
common::run_cmd(
&dir,
&bin,
&["task", "create", "--title", "tracked", "--json"],
);
let id = task_display_id(&dir, &bin, "tracked");
let start = common::run_cmd_as(&dir, &bin, "tester", &["task", "start", &id, "--json"]);
assert!(start.status.success(), "start failed");
let edit = common::run_cmd_as(
&dir,
&bin,
"tester",
&["task", "edit", &id, "--priority", "high", "--json"],
);
assert!(edit.status.success(), "edit failed");
let events = common::run_cmd(
&dir,
&bin,
&["event", "list", "--agent", "tester", "--json"],
);
assert!(events.status.success(), "event list failed");
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&events.stdout)).expect("valid json");
let events = value["data"]["events"].as_array().expect("events array");
assert!(!events.is_empty(), "expected transition/edit events");
let whoami = common::run_cmd(&dir, &bin, &["agent", "show", "tester", "--json"]);
assert!(
whoami.status.success(),
"agent show failed: {}",
String::from_utf8_lossy(&whoami.stderr)
);
let ulid: String =
serde_json::from_str::<serde_json::Value>(&String::from_utf8_lossy(&whoami.stdout))
.expect("valid agent json")["data"]["id"]
.as_str()
.expect("agent id")
.to_string();
for event in events {
let actor = event["actor_agent_id"].as_str().unwrap_or_default();
assert_eq!(
actor, ulid,
"audit event must carry the canonical ULID, got '{actor}'"
);
}
}
fn task_display_id(dir: &std::path::Path, bin: &std::path::Path, title: &str) -> String {
let list = common::run_cmd(dir, bin, &["task", "list", "--json"]);
assert!(list.status.success(), "task list failed");
let value: serde_json::Value =
serde_json::from_str(&String::from_utf8_lossy(&list.stdout)).expect("valid json envelope");
value["data"]
.as_array()
.expect("task list array")
.iter()
.find(|t| t["title"] == serde_json::Value::String(title.to_string()))
.unwrap_or_else(|| panic!("task with title '{title}' not found in {value}"))["display_id"]
.as_str()
.expect("display id")
.to_string()
}