use std::fs;
use std::path::PathBuf;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
fn tix() -> Command {
Command::cargo_bin("tix").unwrap()
}
fn issue_file(dir: &std::path::Path) -> PathBuf {
let issues = dir.join("docs").join("issues");
let mut md: Vec<PathBuf> = Vec::new();
for entry in fs::read_dir(&issues).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
for child in fs::read_dir(&path).unwrap() {
let child = child.unwrap().path();
if child.extension().map(|e| e == "md").unwrap_or(false) {
md.push(child);
}
}
} else if path.extension().map(|e| e == "md").unwrap_or(false) {
md.push(path);
}
}
assert_eq!(md.len(), 1, "expected exactly one issue file in {issues:?}");
md.swap_remove(0)
}
fn created_relative(out: &[u8]) -> String {
let stdout = String::from_utf8(out.to_vec()).unwrap();
stdout
.lines()
.map(str::trim)
.find_map(|l| l.strip_prefix("Created "))
.unwrap_or_else(|| panic!("no `Created ` line in {stdout:?}"))
.to_string()
}
fn git(dir: &std::path::Path, args: &[&str]) {
let status = std::process::Command::new("git")
.current_dir(dir)
.args(args)
.status()
.expect("`git` must be available to run the CLI tests");
assert!(status.success(), "git {args:?} failed");
}
#[test]
fn cli_happy_path_end_to_end() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix()
.current_dir(dir)
.arg("init")
.assert()
.success()
.stdout(predicate::str::contains("Created"));
assert!(dir.join("tix.toml").is_file());
tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Fix login bug"])
.args(["--body", "Users can't log in"])
.args(["--priority", "high"])
.args(["--labels", "bug,auth"])
.args(["--assignee", "@alice"])
.args(["--estimate", "2"])
.assert()
.success()
.stdout(predicate::str::contains("Created "));
let file = issue_file(dir);
let filename = file.file_name().unwrap().to_string_lossy().to_string();
assert!(
filename.ends_with("-fix-login-bug.md"),
"expected slug filename, got {filename}"
);
let out = tix()
.current_dir(dir)
.arg("list")
.assert()
.success()
.get_output()
.stdout
.clone();
let list = String::from_utf8(out).unwrap();
let first = list.lines().next().unwrap();
assert!(
first.contains("NUM") && first.contains("TITLE") && first.contains("STATUS"),
"header row must be first: {list}"
);
assert!(
list.lines()
.any(|l| l.contains("Fix login bug") && l.contains("high")),
"issue row missing: {list}"
);
let out = tix()
.current_dir(dir)
.args(["list", "--status", "done"])
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(String::from_utf8(out).unwrap().lines().count(), 1);
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(
predicate::str::starts_with("---\n").and(
predicate::str::contains("number: 0")
.and(predicate::str::contains("title: Fix login bug"))
.and(predicate::str::contains("status: todo"))
.and(predicate::str::contains("priority: high"))
.and(predicate::str::contains("labels:"))
.and(predicate::str::contains("assignee:"))
.and(predicate::str::contains("@alice"))
.and(predicate::str::contains("estimate:"))
.and(predicate::str::contains("Users can't log in")),
),
);
let out = tix()
.current_dir(dir)
.arg("status")
.assert()
.success()
.get_output()
.stdout
.clone();
let status = String::from_utf8(out).unwrap();
let header = status.lines().next().unwrap();
assert!(
header.contains("COLUMN") && header.contains("COUNT"),
"status header first: {status}"
);
assert!(
status
.lines()
.any(|l| l.trim().starts_with("todo") && l.contains('1')),
"todo count missing: {status}"
);
assert!(
status
.lines()
.any(|l| l.trim().starts_with("Total") && l.contains('1')),
"total row missing: {status}"
);
tix()
.current_dir(dir)
.args(["move", "0", "done"])
.arg("--commit")
.arg("abc123")
.assert()
.success();
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(predicate::str::contains("status: done").and(predicate::str::contains("abc123")));
tix()
.current_dir(dir)
.args(["move", "0", "in progress"])
.assert()
.success();
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(
predicate::str::contains("status: in progress")
.and(predicate::str::contains("abc123").not()),
);
tix()
.current_dir(dir)
.args(["move", "0", "done"])
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("--commit"));
tix()
.current_dir(dir)
.args(["move", "0", "bogus"])
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("valid columns"));
tix()
.current_dir(dir)
.args(["edit", "0"])
.args(["--priority", "bogus"])
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("invalid priority"));
tix()
.current_dir(dir)
.args(["edit", "0"])
.args(["--priority", "medium"])
.args(["--title", "Fix login bug v2"])
.assert()
.success();
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(
predicate::str::contains("priority: medium")
.and(predicate::str::contains("Fix login bug v2")),
);
tix()
.current_dir(dir)
.args(["edit", "0", "--commit", "abc123"])
.assert()
.success();
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(predicate::str::contains("abc123"));
tix()
.current_dir(dir)
.args(["edit", "0", "--commit", "clear"])
.assert()
.success();
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(predicate::str::contains("abc123").not());
tix()
.current_dir(dir)
.args(["delete", "0"])
.arg("--yes")
.assert()
.success()
.stdout(predicate::str::contains("Deleted "));
assert!(!file.exists());
let out = tix()
.current_dir(dir)
.arg("list")
.assert()
.success()
.get_output()
.stdout
.clone();
assert_eq!(String::from_utf8(out).unwrap().lines().count(), 1);
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.failure()
.code(2)
.stderr(predicate::str::contains("not found"));
}
#[test]
fn init_refuses_overwrite() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
tix()
.current_dir(dir)
.arg("init")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("refusing to overwrite"));
let toml = fs::read_to_string(dir.join("tix.toml")).unwrap();
assert!(toml.contains("issues_dir"));
}
#[test]
fn missing_config_notice_on_stderr_but_subcommand_succeeds() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix()
.current_dir(dir)
.arg("list")
.assert()
.success()
.code(0)
.stdout(predicate::str::contains("NUM"))
.stderr(predicate::str::contains(
"no tix.toml found (run 'tix init' to create one)",
));
}
#[test]
fn no_argv_without_config_is_contract_error() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix()
.current_dir(dir)
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains(
"no tix.toml found — run \"tix init\" first",
));
}
#[test]
fn create_without_title_on_pipe_is_usage_error() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
tix()
.current_dir(dir)
.arg("create")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("--title"));
}
#[cfg(unix)]
#[test]
fn edit_invokes_real_editor_on_copy_and_commits_changes() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Editor target"])
.args(["--priority", "high"])
.assert()
.success();
let editor = dir.join("editor.sh");
fs::write(
&editor,
"#!/bin/sh\nsed -i 's/priority: high/priority: low/' \"$1\"\n",
)
.unwrap();
fs::set_permissions(&editor, fs::Permissions::from_mode(0o755)).unwrap();
tix()
.current_dir(dir)
.args(["edit", "0"])
.env("EDITOR", editor.to_str().unwrap())
.assert()
.success();
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(predicate::str::contains("priority: low"));
}
#[test]
fn view_by_slug_prefix_and_title_fragment_resolve() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Refactor auth module"])
.assert()
.success();
let file = issue_file(dir);
let stem = file.file_stem().unwrap().to_string_lossy().to_string();
tix()
.current_dir(dir)
.arg("view")
.arg("refactor-auth")
.assert()
.success()
.stdout(predicate::str::contains("title: Refactor auth module"));
tix()
.current_dir(dir)
.arg("view")
.arg("auth")
.assert()
.success()
.stdout(predicate::str::contains("title: Refactor auth module"));
let _ = stem;
}
#[test]
fn create_without_assignee_without_git_identity_lands_in_unassigned() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
let out = tix()
.current_dir(dir)
.arg("create")
.args(["--title", "No git task"])
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.assert()
.success()
.get_output()
.stdout
.clone();
let rel = created_relative(&out);
assert!(
rel.starts_with("unassigned/"),
"expected unassigned/<stem>.md relative path, got {rel}"
);
assert!(
rel.ends_with("-no-git-task.md"),
"expected slug stem, got {rel}"
);
assert!(
dir.join("docs").join("issues").join(&rel).is_file(),
"file must exist at docs/issues/{rel}"
);
}
#[test]
fn create_without_assignee_defaults_to_git_identity_segment() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
git(dir, &["init", "-q"]);
git(dir, &["config", "user.name", "Herb Mason"]);
git(dir, &["config", "user.email", "herb@example.com"]);
tix().current_dir(dir).arg("init").assert().success();
let out = tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Git task"])
.assert()
.success()
.get_output()
.stdout
.clone();
let rel = created_relative(&out);
assert!(
rel.starts_with("herb-mason/"),
"expected identity segment, got {rel}"
);
assert!(
dir.join("docs").join("issues").join(&rel).is_file(),
"file must exist at docs/issues/{rel}"
);
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(
predicate::str::contains("assignee:")
.and(predicate::str::contains("Herb Mason <herb@example.com>"))
.and(predicate::str::contains("@Herb").not()),
);
}
#[test]
fn edit_assignee_moves_file_and_keeps_number() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
let created = tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Reassign me"])
.args(["--assignee", "@alice"])
.assert()
.success()
.get_output()
.stdout
.clone();
let rel = created_relative(&created);
assert!(
rel.starts_with("alice/"),
"explicit --assignee must win, got {rel}"
);
let issues = dir.join("docs").join("issues");
assert!(issues.join(&rel).is_file(), "{rel} must exist before edit");
tix()
.current_dir(dir)
.args(["edit", "0", "--assignee", "bob"])
.assert()
.success();
assert!(
!issues.join(&rel).exists(),
"old path {rel} must be gone after reassign"
);
let new_rel = rel.replacen("alice/", "bob/", 1);
assert!(
issues.join(&new_rel).is_file(),
"new path {new_rel} must exist after reassign"
);
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(
predicate::str::contains("number: 0")
.and(predicate::str::contains("assignee:"))
.and(predicate::str::contains("bob")),
);
}
#[test]
fn edit_assignee_empty_moves_to_unassigned() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
let created = tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Clear me"])
.args(["--assignee", "alice"])
.assert()
.success()
.get_output()
.stdout
.clone();
let rel = created_relative(&created);
let issues = dir.join("docs").join("issues");
assert!(issues.join(&rel).is_file());
tix()
.current_dir(dir)
.args(["edit", "0", "--assignee", ""])
.assert()
.success();
assert!(
!issues.join(&rel).exists(),
"old path {rel} must be gone after clearing the assignee"
);
let new_rel = rel.replacen("alice/", "unassigned/", 1);
assert!(
issues.join(&new_rel).is_file(),
"new path {new_rel} must exist under unassigned/"
);
tix()
.current_dir(dir)
.args(["view", "0"])
.assert()
.success()
.stdout(
predicate::str::contains("number: 0").and(predicate::str::contains("assignee").not()),
);
}
fn with_store() -> (TempDir, std::path::PathBuf) {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
let issues = dir.join("docs").join("issues");
std::fs::create_dir_all(&issues).unwrap();
(tmp, issues)
}
fn write_frontmatter(path: &std::path::Path, fm: &str) {
let contents = format!("---\n{fm}\n---\n\nbody text\n");
std::fs::write(path, contents).unwrap();
}
const GOOD_FM: &[&str] = &[
"number: '1'",
"title: \"Synthetic issue\"",
"labels: []",
"status: \"todo\"",
"priority: \"medium\"",
"created: \"2026-01-01\"",
"updated: \"2026-01-01\"",
];
fn fm_line(number: u32, title: &str, extra: &[(&str, &str)]) -> Vec<String> {
let mut parts: Vec<String> = vec![format!("number: {number}"), format!("title: \"{title}\"")];
for line in GOOD_FM {
if line.starts_with("number:") || line.starts_with("title:") {
continue;
}
parts.push(line.to_string());
}
for (key, val) in extra {
let rendered = if val.starts_with('[') || val.starts_with('{') || *val == "null" {
format!("{key}: {val}")
} else {
format!("{key}: '{val}'")
};
if let Some(slot) = parts.iter_mut().find(|l| l.starts_with(&format!("{key}:"))) {
*slot = rendered;
} else {
parts.push(rendered);
}
}
parts
}
#[test]
fn search_matches_assignee_field() {
let (_tmp, issues) = with_store();
write_frontmatter(
&issues.join("2026-01-01-alice-task.md"),
&fm_line(1, "Config work", &[("assignee", "@alice")]).join("\n"),
);
tix()
.current_dir(issues.parent().unwrap())
.args(["search", "alice"])
.assert()
.success()
.code(0)
.stdout(
predicate::str::contains("#1 Config work — todo (2026-01-01-alice-task.md)")
.and(predicate::str::contains("@alice")),
);
}
#[test]
fn search_matches_comment_text_only() {
let (_tmp, issues) = with_store();
let contents = format!(
"---\n{}\n---\n\nbody\n\n## Comments\n\n### alice (2026-01-03)\n\nNeed a hotfix for the timeout.\n",
fm_line(1, "Timeout investigation", &[]).join("\n")
);
std::fs::write(issues.join("2026-01-01-timeout.md"), contents).unwrap();
tix()
.current_dir(issues.parent().unwrap())
.args(["search", "hotfix"])
.assert()
.success()
.code(0)
.stdout(
predicate::str::contains("#1 Timeout investigation — todo (2026-01-01-timeout.md)")
.and(predicate::str::contains("hotfix for the timeout")),
);
}
#[test]
fn search_matches_frontmatter_label() {
let (_tmp, issues) = with_store();
write_frontmatter(
&issues.join("2026-01-02-bug-repro.md"),
&fm_line(1, "Repro steps", &[("labels", "[bug]")]).join("\n"),
);
tix()
.current_dir(issues.parent().unwrap())
.args(["search", "bug"])
.assert()
.success()
.stdout(predicate::str::contains("#1 Repro steps"));
}
#[test]
fn search_is_case_insensitive() {
let (_tmp, issues) = with_store();
write_frontmatter(
&issues.join("2026-01-01-case-test.md"),
&fm_line(1, "Login flow", &[]).join("\n"),
);
let out = tix()
.current_dir(issues.parent().unwrap())
.args(["search", "LOGIN"])
.assert()
.success()
.code(0)
.get_output()
.stdout
.clone();
assert!(String::from_utf8(out).unwrap().contains("#1 Login flow"));
}
#[test]
fn search_orders_hits_by_number() {
let (_tmp, issues) = with_store();
let shared = [("labels", "[shared-word]")];
write_frontmatter(
&issues.join("2026-01-01-c.md"),
&fm_line(3, "Third", &shared).join("\n"),
);
write_frontmatter(
&issues.join("2026-01-02-a.md"),
&fm_line(1, "First", &shared).join("\n"),
);
write_frontmatter(
&issues.join("2026-01-03-b.md"),
&fm_line(2, "Second", &shared).join("\n"),
);
let out = tix()
.current_dir(issues.parent().unwrap())
.args(["search", "shared-word"])
.assert()
.success()
.code(0)
.get_output()
.stdout
.clone();
let stdout = String::from_utf8(out).unwrap();
let idx1 = stdout.find("#1 First").expect("hit #1 missing");
let idx2 = stdout.find("#2 Second").expect("hit #2 missing");
let idx3 = stdout.find("#3 Third").expect("hit #3 missing");
assert!(idx1 < idx2 && idx2 < idx3, "hits not ordered: {stdout}");
}
#[test]
fn search_zero_matches_prints_message_and_exits_zero() {
let (_tmp, issues) = with_store();
write_frontmatter(
&issues.join("2026-01-01-foo.md"),
&fm_line(1, "Foo", &[]).join("\n"),
);
tix()
.current_dir(issues.parent().unwrap())
.args(["search", "zzz-nothing"])
.assert()
.success()
.code(0)
.stdout("No issues found.\n");
}
#[test]
fn search_empty_query_is_usage_error_exit_2() {
let (_tmp, issues) = with_store();
tix()
.current_dir(issues.parent().unwrap())
.args(["search", ""])
.assert()
.failure()
.code(2);
}
#[test]
fn search_store_io_failure_exits_1() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
std::fs::create_dir_all(dir.join("docs")).unwrap();
std::fs::write(dir.join("docs/issues"), "not a directory").unwrap();
let assert = tix()
.current_dir(dir)
.args(["search", "anything"])
.assert()
.failure()
.code(1);
assert!(
!assert.get_output().stderr.is_empty(),
"io failure must surface on stderr"
);
}
#[test]
fn lint_flags_bad_status_with_valid_columns() {
let (_tmp, issues) = with_store();
let fm = fm_line(1, "Bad status", &[("status", "donex")]).join("\n");
write_frontmatter(&issues.join("2026-01-01-bad-status.md"), &fm);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("error[E-tix-lint]")
.and(predicate::str::contains("2026-01-01-bad-status.md"))
.and(predicate::str::contains("donex"))
.and(predicate::str::contains(
"valid options: todo, in progress, in review, done",
)),
);
}
#[test]
fn lint_flags_unknown_priority() {
let (_tmp, issues) = with_store();
let fm = fm_line(1, "Urgent", &[("priority", "urgent")]).join("\n");
write_frontmatter(&issues.join("2026-01-01-urgent.md"), &fm);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("error[E-tix-lint]")
.and(predicate::str::contains("priority"))
.and(predicate::str::contains("urgent"))
.and(predicate::str::contains("low, medium, high")),
);
}
#[test]
fn lint_flags_malformed_created_date() {
let (_tmp, issues) = with_store();
let fm = fm_line(1, "Date issue", &[("created", "2026-13-99")]).join("\n");
write_frontmatter(&issues.join("2026-01-01-date.md"), &fm);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("error[E-tix-lint]")
.and(predicate::str::contains("created"))
.and(predicate::str::contains("2026-13-99"))
.and(predicate::str::contains("YYYY-MM-DD")),
);
}
#[test]
fn lint_flags_non_numeric_estimate() {
let (_tmp, issues) = with_store();
let fm = fm_line(1, "Estimate", &[("estimate", "3d")]).join("\n");
write_frontmatter(&issues.join("2026-01-01-estimate.md"), &fm);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("error[E-tix-lint]")
.and(predicate::str::contains("estimate"))
.and(predicate::str::contains("3d")),
);
}
#[test]
fn lint_flags_non_integer_number() {
let (_tmp, issues) = with_store();
write_frontmatter(
&issues.join("2026-01-01-bad-number.md"),
"number: 'abc'\ntitle: \"Bad number\"",
);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("error[E-tix-lint]"));
}
#[test]
fn lint_ignores_unknown_keys() {
let (_tmp, issues) = with_store();
let fm = fm_line(1, "Clean", &[("custom_key", "custom_value")]).join("\n");
write_frontmatter(&issues.join("2026-01-03-clean.md"), &fm);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.success()
.code(0)
.stdout(predicate::str::contains("Lint OK: 1 issues checked."))
.stderr(predicate::str::is_empty());
}
#[test]
fn lint_clean_store_summary() {
let (_tmp, issues) = with_store();
write_frontmatter(
&issues.join("2026-01-01-a.md"),
&fm_line(1, "A", &[]).join("\n"),
);
write_frontmatter(
&issues.join("2026-01-02-b.md"),
&fm_line(2, "B", &[]).join("\n"),
);
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.success()
.code(0)
.stdout(predicate::str::contains("Lint OK: 2 issues checked."));
}
#[test]
fn lint_missing_frontmatter_diagnostic_no_crash() {
let (_tmp, issues) = with_store();
std::fs::write(issues.join("2026-01-01-naked.md"), "no frontmatter here\n").unwrap();
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("error[E-tix-lint]")
.and(predicate::str::contains("2026-01-01-naked.md")),
);
}
#[test]
fn lint_unparsable_yaml_diagnostic_no_crash() {
let (_tmp, issues) = with_store();
write_frontmatter(&issues.join("2026-01-01-broken.md"), "number 0x\n");
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(
predicate::str::contains("error[E-tix-lint]")
.and(predicate::str::contains("2026-01-01-broken.md")),
);
}
#[test]
fn lint_non_utf8_diagnostic_no_crash() {
let (_tmp, issues) = with_store();
std::fs::write(issues.join("2026-01-01-binary.md"), b"\xff\xfe\x00\x01\xff").unwrap();
tix()
.current_dir(issues.parent().unwrap())
.arg("lint")
.assert()
.failure()
.code(1)
.stderr(predicate::str::contains("error[E-tix-lint]"));
}
#[test]
fn edit_assignee_same_segment_is_noop_success() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
tix().current_dir(dir).arg("init").assert().success();
let created = tix()
.current_dir(dir)
.arg("create")
.args(["--title", "Stay put"])
.args(["--assignee", "alice"])
.assert()
.success()
.get_output()
.stdout
.clone();
let rel = created_relative(&created);
let issues = dir.join("docs").join("issues");
tix()
.current_dir(dir)
.args(["edit", "0", "--assignee", "@alice"])
.assert()
.success();
assert!(
issues.join(&rel).is_file(),
"file must stay at {rel} when the segment is unchanged"
);
}