use assert_cmd::{Command, cargo_bin_cmd};
use assert_fs::prelude::*;
use predicates::prelude::*;
use std::fs;
fn adrs() -> Command {
cargo_bin_cmd!("adrs")
}
#[test]
fn test_help() {
adrs()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Architecture Decision Records"));
}
#[test]
fn test_long_help_contains_env_vars() {
adrs()
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("ENVIRONMENT VARIABLES"))
.stdout(predicate::str::contains("ADR_DIRECTORY"));
}
#[test]
fn test_short_help_no_env_vars() {
adrs()
.arg("-h")
.assert()
.success()
.stdout(predicate::str::contains("ENVIRONMENT VARIABLES").not());
}
#[test]
fn test_version() {
adrs()
.arg("--version")
.assert()
.success()
.stdout(predicate::str::contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn test_subcommand_help_init() {
adrs()
.args(["init", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Initialize a new ADR repository"));
}
#[test]
fn test_subcommand_help_new() {
adrs()
.args(["new", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Create a new ADR"))
.stdout(predicate::str::contains("--format"))
.stdout(predicate::str::contains("--status"));
}
#[test]
fn test_subcommand_help_list() {
adrs()
.args(["list", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("List all ADRs"));
}
#[test]
fn test_init_creates_directory_and_first_adr() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
temp.child(".adr-dir").assert(predicate::path::exists());
temp.child(".adr-dir")
.assert(predicate::str::contains("doc/adr"));
temp.child("doc/adr").assert(predicate::path::is_dir());
temp.child("doc/adr/0001-record-architecture-decisions.md")
.assert(predicate::path::exists());
temp.close().unwrap();
}
#[test]
fn test_init_with_custom_directory() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["init", "decisions"])
.assert()
.success();
temp.child(".adr-dir")
.assert(predicate::str::contains("decisions"));
temp.child("decisions").assert(predicate::path::is_dir());
temp.child("decisions/0001-record-architecture-decisions.md")
.assert(predicate::path::exists());
temp.close().unwrap();
}
#[test]
fn test_init_with_cwd_flag() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.args(["-C", temp.path().to_str().unwrap(), "init"])
.assert()
.success();
temp.child(".adr-dir").assert(predicate::path::exists());
temp.close().unwrap();
}
#[test]
fn test_list_without_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("list")
.assert()
.failure();
temp.close().unwrap();
}
#[test]
fn test_list_after_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(
"0001-record-architecture-decisions.md",
));
temp.close().unwrap();
}
#[test]
fn test_new_without_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["new", "Test ADR"])
.env("EDITOR", "true") .assert()
.failure()
.stderr(predicate::str::contains("No ADR repository found"));
temp.close().unwrap();
}
#[test]
fn test_config_without_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("config")
.assert()
.success()
.stdout(predicate::str::contains("Config source: defaults"));
temp.close().unwrap();
}
#[test]
fn test_config_after_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.arg("config")
.assert()
.success()
.stdout(predicate::str::contains("doc/adr"));
temp.close().unwrap();
}
#[test]
fn test_generate_toc_help() {
adrs()
.args(["generate", "toc", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Generate a table of contents"));
}
#[test]
fn test_generate_toc_after_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["generate", "toc"])
.assert()
.success()
.stdout(predicate::str::contains("Record architecture decisions"));
temp.close().unwrap();
}
#[test]
fn test_generate_toc_ordered() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["generate", "toc", "--ordered"])
.assert()
.success()
.stdout(predicate::str::contains("1."));
temp.close().unwrap();
}
#[test]
fn test_generate_graph_help() {
adrs()
.args(["generate", "graph", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Generate a Graphviz graph"));
}
#[test]
fn test_generate_graph_after_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["generate", "graph"])
.assert()
.success()
.stdout(predicate::str::contains("digraph"));
temp.close().unwrap();
}
#[test]
fn test_link_help() {
adrs()
.args(["link", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Link two ADRs together"));
}
#[test]
fn test_status_help() {
adrs()
.args(["status", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Change an ADR's status"));
}
#[test]
fn test_status_change_to_accepted() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "Test decision"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "2", "accepted"])
.assert()
.success()
.stdout(predicate::str::contains("status changed to Accepted"));
let adr_path = temp.child("doc/adr/0002-test-decision.md");
let content = fs::read_to_string(adr_path.path()).unwrap();
assert!(content.contains("Accepted"));
temp.close().unwrap();
}
#[test]
fn test_status_change_to_deprecated() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "Old decision"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "2", "deprecated"])
.assert()
.success()
.stdout(predicate::str::contains("status changed to Deprecated"));
temp.close().unwrap();
}
#[test]
fn test_status_superseded_with_by_flag() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "Old decision"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "New decision"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "2", "superseded", "--by", "3"])
.assert()
.success()
.stdout(predicate::str::contains("superseded by 3"));
let adr_path = temp.child("doc/adr/0002-old-decision.md");
let content = fs::read_to_string(adr_path.path()).unwrap();
assert!(content.contains("Superseded"));
assert!(content.contains("Superseded by"));
temp.close().unwrap();
}
#[test]
fn test_status_custom_value() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "Draft decision"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "2", "draft"])
.assert()
.success()
.stdout(predicate::str::contains("status changed to draft"));
temp.close().unwrap();
}
#[test]
fn test_status_adr_not_found() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "99", "accepted"])
.assert()
.failure()
.stderr(predicate::str::contains("not found").or(predicate::str::contains("Failed")));
temp.close().unwrap();
}
#[test]
fn test_status_by_flag_requires_superseded() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "Test"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "2", "accepted", "--by", "1"])
.assert()
.failure()
.stderr(predicate::str::contains(
"--by can only be used with 'superseded'",
));
temp.close().unwrap();
}
#[test]
fn test_new_format_flag_help() {
adrs()
.args(["new", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("-f, --format"))
.stdout(predicate::str::contains("nygard"));
}
#[test]
fn test_ng_flag() {
adrs()
.args(["--ng", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("NextGen mode"));
}
#[test]
fn test_cwd_flag() {
adrs()
.args(["--help"])
.assert()
.success()
.stdout(predicate::str::contains("-C, --cwd"));
}
#[test]
fn test_unknown_command() {
adrs()
.arg("unknown")
.assert()
.failure()
.stderr(predicate::str::contains("unrecognized subcommand"));
}
#[test]
fn test_missing_required_arg() {
adrs()
.arg("new")
.assert()
.failure()
.stderr(predicate::str::contains("required"));
}
#[test]
fn test_init_adr_content() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
let content = fs::read_to_string(
temp.path()
.join("doc/adr/0001-record-architecture-decisions.md"),
)
.unwrap();
assert!(content.contains("# 1. Record architecture decisions"));
assert!(content.contains("## Status"));
assert!(content.contains("## Context"));
assert!(content.contains("## Decision"));
assert!(content.contains("## Consequences"));
assert!(content.contains(
"[Documenting Architecture Decisions](https://www.cognitect.com/blog/2011/11/15/documenting-architecture-decisions)"
));
assert!(content.contains("[adrs](https://github.com/joshrotenberg/adrs)"));
assert!(content.contains("[adr-tools](https://github.com/npryce/adr-tools)"));
temp.close().unwrap();
}
#[test]
fn test_adr_dir_file_content() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["init", "my/custom/path"])
.assert()
.success();
let content = fs::read_to_string(temp.path().join(".adr-dir")).unwrap();
assert_eq!(content.trim(), "my/custom/path");
temp.close().unwrap();
}
#[test]
fn test_search_help() {
adrs()
.args(["search", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Search ADRs for matching content"));
}
#[test]
fn test_search_without_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["search", "test"])
.assert()
.failure();
temp.close().unwrap();
}
#[test]
fn test_search_finds_matching_adr() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["search", "architecture"])
.assert()
.success()
.stdout(predicate::str::contains("Record architecture decisions"));
temp.close().unwrap();
}
#[test]
fn test_search_title_only() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["search", "-t", "architecture"])
.assert()
.success()
.stdout(predicate::str::contains("Record architecture decisions"));
temp.close().unwrap();
}
#[test]
fn test_search_no_results() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["search", "xyzzyzzy"])
.assert()
.success()
.stdout(predicate::str::contains("No matches found"));
temp.close().unwrap();
}
#[test]
fn test_template_help() {
adrs()
.args(["template", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Manage ADR templates"));
}
#[test]
fn test_template_list() {
adrs()
.args(["template", "list"])
.assert()
.success()
.stdout(predicate::str::contains("nygard"))
.stdout(predicate::str::contains("madr"));
}
#[test]
fn test_template_show_nygard() {
adrs()
.args(["template", "show", "nygard"])
.assert()
.success()
.stdout(predicate::str::contains("## Status"))
.stdout(predicate::str::contains("## Context"))
.stdout(predicate::str::contains("## Decision"))
.stdout(predicate::str::contains("## Consequences"));
}
#[test]
fn test_template_show_madr() {
adrs()
.args(["template", "show", "madr"])
.assert()
.success()
.stdout(predicate::str::contains("Status"))
.stdout(predicate::str::contains("Context"));
}
#[test]
fn test_template_show_minimal_variant() {
adrs()
.args(["template", "show", "nygard", "--variant", "minimal"])
.assert()
.success()
.stdout(predicate::str::contains("## Status"));
}
#[test]
fn test_completions_help() {
adrs()
.args(["completions", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Generate shell completions"))
.stdout(predicate::str::contains("bash"))
.stdout(predicate::str::contains("zsh"))
.stdout(predicate::str::contains("fish"));
}
#[test]
fn test_completions_bash() {
adrs()
.args(["completions", "bash"])
.assert()
.success()
.stdout(predicate::str::contains("complete"))
.stdout(predicate::str::contains("adrs"));
}
#[test]
fn test_completions_zsh() {
adrs()
.args(["completions", "zsh"])
.assert()
.success()
.stdout(predicate::str::contains("#compdef adrs"));
}
#[test]
fn test_completions_fish() {
adrs()
.args(["completions", "fish"])
.assert()
.success()
.stdout(predicate::str::contains("complete"));
}
#[test]
fn test_cheatsheet_help() {
adrs()
.args(["cheatsheet", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Show quick reference"));
}
#[test]
fn test_cheatsheet_output() {
adrs()
.arg("cheatsheet")
.assert()
.success()
.stdout(predicate::str::contains("GETTING STARTED"))
.stdout(predicate::str::contains("adrs init"))
.stdout(predicate::str::contains("adrs new"));
}
#[test]
fn test_export_help() {
adrs()
.args(["export", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Export ADRs"));
}
#[test]
fn test_export_json_after_init() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["export", "json"])
.assert()
.success()
.stdout(predicate::str::contains("\"adrs\""))
.stdout(predicate::str::contains("Record architecture decisions"));
temp.close().unwrap();
}
#[test]
fn test_export_json_config_base_url_no_flag() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(
temp.path().join("adrs.toml"),
"adr_dir = \"doc/adr\"\n\n[export]\nbase_url = \"https://example.com/adr\"\n",
)
.unwrap();
adrs()
.current_dir(temp.path())
.args(["export", "json"])
.assert()
.success()
.stdout(predicate::str::contains("source_uri"))
.stdout(predicate::str::contains("https://example.com/adr"));
temp.close().unwrap();
}
#[test]
fn test_export_json_flag_overrides_config_base_url() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(
temp.path().join("adrs.toml"),
"adr_dir = \"doc/adr\"\n\n[export]\nbase_url = \"https://config.example.com/adr\"\n",
)
.unwrap();
adrs()
.current_dir(temp.path())
.args([
"export",
"json",
"--base-url",
"https://flag.example.com/adr",
])
.assert()
.success()
.stdout(predicate::str::contains("https://flag.example.com/adr"))
.stdout(predicate::str::is_match("source_uri").unwrap());
let output = adrs()
.current_dir(temp.path())
.args([
"export",
"json",
"--base-url",
"https://flag.example.com/adr",
])
.assert()
.success()
.get_output()
.stdout
.clone();
let output_str = String::from_utf8(output).unwrap();
assert!(
!output_str.contains("https://config.example.com/adr"),
"config base_url should not appear when --base-url flag is set"
);
temp.close().unwrap();
}
#[test]
fn test_export_json_no_base_url_no_source_uri() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(temp.path().join("adrs.toml"), "adr_dir = \"doc/adr\"\n").unwrap();
adrs()
.current_dir(temp.path())
.args(["export", "json"])
.assert()
.success()
.stdout(predicate::str::contains("source_uri").not());
temp.close().unwrap();
}
#[test]
fn test_import_help() {
adrs()
.args(["import", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("Import ADRs"));
}
#[test]
fn test_import_export_round_trip() {
let src = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(src.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(src.path())
.args(["new", "Use PostgreSQL"])
.env("EDITOR", "true")
.assert()
.success();
let export_out = adrs()
.current_dir(src.path())
.args(["export", "json", "--pretty"])
.assert()
.success()
.get_output()
.stdout
.clone();
let json_file = src.child("export.json");
fs::write(json_file.path(), &export_out).unwrap();
let dest = assert_fs::TempDir::new().unwrap();
let dest_adr_dir = dest.child("imported");
adrs()
.current_dir(dest.path())
.args([
"import",
"json",
"--dir",
dest_adr_dir.path().to_str().unwrap(),
json_file.path().to_str().unwrap(),
])
.assert()
.success();
dest_adr_dir
.child("0001-record-architecture-decisions.md")
.assert(predicate::path::exists());
dest_adr_dir
.child("0002-use-postgresql.md")
.assert(predicate::path::exists());
src.close().unwrap();
dest.close().unwrap();
}
#[test]
fn test_import_with_ng_flag() {
let src = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(src.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(src.path())
.args(["new", "NG Import Test"])
.env("EDITOR", "true")
.assert()
.success();
let export_out = adrs()
.current_dir(src.path())
.args(["export", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
let json_file = src.child("export.json");
fs::write(json_file.path(), &export_out).unwrap();
let dest = assert_fs::TempDir::new().unwrap();
let dest_adr_dir = dest.child("ng_imported");
adrs()
.current_dir(dest.path())
.args([
"import",
"json",
"--ng",
"--dir",
dest_adr_dir.path().to_str().unwrap(),
json_file.path().to_str().unwrap(),
])
.assert()
.success();
let imported_path = dest_adr_dir
.path()
.join("0001-record-architecture-decisions.md");
let content = fs::read_to_string(&imported_path).unwrap();
assert!(
content.starts_with("---"),
"ng import should produce YAML frontmatter. Got:\n{content}"
);
src.close().unwrap();
dest.close().unwrap();
}
#[test]
fn test_import_missing_file_exits_nonzero() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["import", "json", "/nonexistent/path/adrs.json"])
.assert()
.failure();
temp.close().unwrap();
}
#[test]
fn test_list_filter_by_status() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "Test decision"])
.env("EDITOR", "true")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["list", "--status", "proposed"])
.assert()
.success()
.stdout(predicate::str::contains("0002-test-decision.md"))
.stdout(predicate::str::contains("0001-record-architecture").not());
temp.close().unwrap();
}
#[test]
fn test_new_no_edit_flag() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "Quick decision"])
.assert()
.success();
temp.child("doc/adr/0002-quick-decision.md")
.assert(predicate::path::exists());
temp.close().unwrap();
}
#[test]
fn test_new_template_flag_missing_file() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args([
"new",
"--no-edit",
"--template",
"/nonexistent/path/template.md",
"Should fail",
])
.assert()
.failure()
.stderr(
predicate::str::contains("custom template")
.or(predicate::str::contains("/nonexistent/path/template.md")),
);
temp.close().unwrap();
}
#[test]
fn test_new_madr_format_listed() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args([
"new",
"--no-edit",
"--format",
"madr",
"Use Redis for caching",
])
.assert()
.success();
adrs()
.current_dir(temp.path())
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains("0002-use-redis-for-caching.md"));
temp.close().unwrap();
}
#[test]
fn test_new_madr_format_searchable() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args([
"new",
"--no-edit",
"--format",
"madr",
"Use Redis for caching",
])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["search", "Redis"])
.assert()
.success()
.stdout(predicate::str::contains("Use Redis for caching"));
temp.close().unwrap();
}
#[test]
fn test_madr_status_change() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "--format", "madr", "Use GraphQL"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["status", "2", "accepted"])
.assert()
.success()
.stdout(predicate::str::contains("status changed to Accepted"));
temp.close().unwrap();
}
#[test]
fn test_madr_export_includes_adr() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "--format", "madr", "Use PostgreSQL"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["export", "json"])
.assert()
.success()
.stdout(predicate::str::contains("Use PostgreSQL"));
temp.close().unwrap();
}
#[test]
fn test_smoke_full_workflow() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "Use PostgreSQL for persistence"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "Implement REST API"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args([
"--ng",
"new",
"--no-edit",
"--format",
"madr",
"Use Kubernetes for orchestration",
])
.assert()
.success();
adrs()
.current_dir(temp.path())
.arg("list")
.assert()
.success()
.stdout(predicate::str::contains(
"0001-record-architecture-decisions.md",
))
.stdout(predicate::str::contains(
"0002-use-postgresql-for-persistence.md",
))
.stdout(predicate::str::contains("0003-implement-rest-api.md"))
.stdout(predicate::str::contains(
"0004-use-kubernetes-for-orchestration.md",
));
adrs()
.current_dir(temp.path())
.args(["search", "PostgreSQL"])
.assert()
.success()
.stdout(predicate::str::contains("Use PostgreSQL"));
adrs()
.current_dir(temp.path())
.args(["search", "Kubernetes"])
.assert()
.success()
.stdout(predicate::str::contains("Use Kubernetes"));
adrs()
.current_dir(temp.path())
.args(["status", "2", "accepted"])
.assert()
.success()
.stdout(predicate::str::contains("status changed to Accepted"));
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "Use MySQL instead"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["link", "5", "Supersedes", "2"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["export", "json"])
.assert()
.success()
.stdout(predicate::str::contains("\"adrs\""))
.stdout(predicate::str::contains("Use PostgreSQL"))
.stdout(predicate::str::contains("Use Kubernetes"))
.stdout(predicate::str::contains("supersedes"));
adrs()
.current_dir(temp.path())
.args(["generate", "toc"])
.assert()
.success()
.stdout(predicate::str::contains("Record architecture decisions"))
.stdout(predicate::str::contains("Use PostgreSQL"))
.stdout(predicate::str::contains("Use Kubernetes"));
adrs()
.current_dir(temp.path())
.args(["generate", "graph"])
.assert()
.success()
.stdout(predicate::str::contains("digraph"))
.stdout(predicate::str::contains("Supersedes"));
adrs()
.current_dir(temp.path())
.arg("doctor")
.assert()
.success();
temp.close().unwrap();
}
#[test]
fn test_doctor_ng_flag_prints_note() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["--ng", "doctor"])
.assert()
.success()
.stderr(predicate::str::contains("--ng has no effect on 'doctor'"));
temp.close().unwrap();
}
#[test]
fn test_doctor_without_ng_flag_prints_no_note() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.arg("doctor")
.assert()
.success()
.stderr(predicate::str::contains("--ng").not());
temp.close().unwrap();
}
fn nygard_adr(number: u32, title: &str) -> String {
format!(
"# {number}. {title}\n\nDate: 2024-01-01\n\n## Status\n\nAccepted\n\n## Context\n\nSome context.\n\n## Decision\n\nA decision.\n\n## Consequences\n\nSome consequences.\n"
)
}
#[test]
fn test_doctor_config_ignore_suppresses_diagnostic() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(
temp.path().join("doc/adr/0002-second.md"),
nygard_adr(2, "Second"),
)
.unwrap();
fs::write(
temp.path().join("doc/adr/0004-fourth.md"),
nygard_adr(4, "Fourth"),
)
.unwrap();
fs::write(
temp.path().join("adrs.toml"),
"adr_dir = \"doc/adr\"\n\n[doctor]\nignore = [\"ADR011\"]\n",
)
.unwrap();
adrs()
.current_dir(temp.path())
.arg("doctor")
.assert()
.success()
.stdout(predicate::str::contains("ADR011").not())
.stdout(predicate::str::contains("suppressed by ignore rules"));
temp.close().unwrap();
}
#[test]
fn test_doctor_warnings_as_errors_flag_exits_1_on_warnings_only() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(
temp.path().join("doc/adr/0002-second.md"),
nygard_adr(2, "Second"),
)
.unwrap();
fs::write(
temp.path().join("doc/adr/0004-fourth.md"),
nygard_adr(4, "Fourth"),
)
.unwrap();
adrs()
.current_dir(temp.path())
.args(["doctor", "--warnings-as-errors"])
.assert()
.failure()
.code(1);
temp.close().unwrap();
}
#[test]
fn test_doctor_config_warnings_as_errors_exits_1_without_flag() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(
temp.path().join("doc/adr/0002-second.md"),
nygard_adr(2, "Second"),
)
.unwrap();
fs::write(
temp.path().join("doc/adr/0004-fourth.md"),
nygard_adr(4, "Fourth"),
)
.unwrap();
fs::write(
temp.path().join("adrs.toml"),
"adr_dir = \"doc/adr\"\n\n[doctor]\nwarnings_as_errors = true\n",
)
.unwrap();
adrs()
.current_dir(temp.path())
.arg("doctor")
.assert()
.failure()
.code(1);
temp.close().unwrap();
}
#[test]
fn test_doctor_ignore_flag_works_without_config() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
fs::write(
temp.path().join("doc/adr/0002-second.md"),
nygard_adr(2, "Second"),
)
.unwrap();
fs::write(
temp.path().join("doc/adr/0004-fourth.md"),
nygard_adr(4, "Fourth"),
)
.unwrap();
adrs()
.current_dir(temp.path())
.args(["doctor", "--ignore", "ADR011"])
.assert()
.success()
.stdout(predicate::str::contains("ADR011").not());
temp.close().unwrap();
}
#[test]
#[cfg(feature = "mcp")]
fn test_smoke_mcp_available() {
adrs()
.args(["mcp", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("MCP server"))
.stdout(predicate::str::contains("serve"));
}
#[test]
fn test_smoke_template_system() {
adrs()
.args(["template", "list"])
.assert()
.success()
.stdout(predicate::str::contains("nygard"))
.stdout(predicate::str::contains("madr"))
.stdout(predicate::str::contains("full"))
.stdout(predicate::str::contains("minimal"))
.stdout(predicate::str::contains("bare"));
adrs()
.args(["template", "show", "nygard"])
.assert()
.success()
.stdout(predicate::str::contains("## Status"));
adrs()
.args(["template", "show", "madr"])
.assert()
.success()
.stdout(predicate::str::contains("status:"));
adrs()
.args(["template", "show", "nygard", "--variant", "minimal"])
.assert()
.success();
adrs()
.args(["template", "show", "madr", "--variant", "bare"])
.assert()
.success();
}
#[test]
fn test_smoke_completions() {
for shell in ["bash", "zsh", "fish", "powershell", "elvish"] {
adrs().args(["completions", shell]).assert().success();
}
}
#[test]
fn test_smoke_cheatsheet() {
adrs()
.arg("cheatsheet")
.assert()
.success()
.stdout(predicate::str::contains("GETTING STARTED"))
.stdout(predicate::str::contains("CREATING ADRs"))
.stdout(predicate::str::contains("SUPERSEDING"))
.stdout(predicate::str::contains("STATUS"))
.stdout(predicate::str::contains("SEARCHING"));
}
#[test]
fn test_smoke_nextgen_workflow() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["--ng", "init"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args([
"--ng",
"new",
"--no-edit",
"-t",
"database,infrastructure",
"Use PostgreSQL",
])
.assert()
.success();
let content = fs::read_to_string(temp.path().join("doc/adr/0002-use-postgresql.md")).unwrap();
assert!(content.starts_with("---"));
assert!(content.contains("number: 2"));
assert!(content.contains("title: Use PostgreSQL"));
assert!(content.contains("status: proposed"));
assert!(content.contains("tags:"));
assert!(content.contains("database"));
assert!(content.contains("infrastructure"));
adrs()
.current_dir(temp.path())
.args(["--ng", "list", "--tag", "database"])
.assert()
.success()
.stdout(predicate::str::contains("0002-use-postgresql.md"));
adrs()
.current_dir(temp.path())
.args(["--ng", "new", "--no-edit", "Use Redis"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["--ng", "list", "--tag", "database"])
.assert()
.success()
.stdout(predicate::str::contains("0002-use-postgresql.md"))
.stdout(predicate::str::contains("0003-use-redis.md").not());
temp.close().unwrap();
}
#[test]
fn test_new_madr_participant_flags() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["--ng", "init"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args([
"--ng",
"new",
"--no-edit",
"--deciders",
"Alice, Bob",
"--consulted",
"Security Team",
"--informed",
"Engineering",
"Use OAuth2",
])
.assert()
.success();
let content = fs::read_to_string(temp.path().join("doc/adr/0002-use-oauth2.md")).unwrap();
assert!(content.starts_with("---"));
assert!(content.contains("decision-makers:"));
assert!(content.contains("Alice"));
assert!(content.contains("Bob"));
assert!(content.contains("consulted:"));
assert!(content.contains("Security Team"));
assert!(content.contains("informed:"));
assert!(content.contains("Engineering"));
adrs()
.current_dir(temp.path())
.args(["--ng", "list", "--decider", "Alice"])
.assert()
.success()
.stdout(predicate::str::contains("0002-use-oauth2.md"));
temp.close().unwrap();
}
#[test]
fn test_new_madr_participant_flags_require_ng() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.args(["init"])
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["new", "--no-edit", "--deciders", "Alice", "Use OAuth2"])
.assert()
.failure()
.stderr(predicate::str::contains("--ng mode"));
temp.close().unwrap();
}
#[test]
fn test_generate_book_with_cwd_flag_relative_output() {
let repo = assert_fs::TempDir::new().unwrap();
let elsewhere = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(repo.path())
.arg("init")
.assert()
.success();
let sentinel = "# pre-existing book.toml, do not overwrite\n";
elsewhere.child("book/src").create_dir_all().unwrap();
elsewhere
.child("book/book.toml")
.write_str(sentinel)
.unwrap();
adrs()
.current_dir(elsewhere.path())
.args(["-C", repo.path().to_str().unwrap(), "generate", "book"])
.assert()
.success();
repo.child("book/book.toml")
.assert(predicate::path::exists());
repo.child("book/src/SUMMARY.md")
.assert(predicate::path::exists());
repo.child("book/src/0001-record-architecture-decisions.md")
.assert(predicate::path::exists());
elsewhere.child("book/book.toml").assert(sentinel);
elsewhere
.child("book/src/SUMMARY.md")
.assert(predicate::path::missing());
repo.close().unwrap();
elsewhere.close().unwrap();
}
#[test]
fn test_import_json_with_cwd_flag_relative_paths() {
let workdir = assert_fs::TempDir::new().unwrap();
let elsewhere = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(workdir.path())
.arg("init")
.assert()
.success();
let export_out = adrs()
.current_dir(workdir.path())
.args(["export", "json"])
.assert()
.success()
.get_output()
.stdout
.clone();
workdir
.child("adrs.json")
.write_binary(&export_out)
.unwrap();
adrs()
.current_dir(elsewhere.path())
.args([
"-C",
workdir.path().to_str().unwrap(),
"import",
"json",
"--dir",
"imported",
"adrs.json",
])
.assert()
.success();
workdir
.child("imported/0001-record-architecture-decisions.md")
.assert(predicate::path::exists());
elsewhere
.child("imported")
.assert(predicate::path::missing());
workdir.close().unwrap();
elsewhere.close().unwrap();
}
#[test]
fn test_export_json_with_cwd_flag_relative_dir() {
let workdir = assert_fs::TempDir::new().unwrap();
let elsewhere = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(workdir.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(elsewhere.path())
.args([
"-C",
workdir.path().to_str().unwrap(),
"export",
"json",
"--dir",
"doc/adr",
])
.assert()
.success()
.stdout(predicate::str::contains("Record architecture decisions"));
workdir.close().unwrap();
elsewhere.close().unwrap();
}
#[test]
fn test_generate_toc_with_cwd_flag_relative_intro() {
let workdir = assert_fs::TempDir::new().unwrap();
let elsewhere = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(workdir.path())
.arg("init")
.assert()
.success();
workdir
.child("intro.md")
.write_str("Intro from workdir\n")
.unwrap();
adrs()
.current_dir(elsewhere.path())
.args([
"-C",
workdir.path().to_str().unwrap(),
"generate",
"toc",
"--intro",
"intro.md",
])
.assert()
.success()
.stdout(predicate::str::contains("Intro from workdir"));
workdir.close().unwrap();
elsewhere.close().unwrap();
}
#[test]
fn test_generate_book_relative_output_without_cwd_flag() {
let temp = assert_fs::TempDir::new().unwrap();
adrs()
.current_dir(temp.path())
.arg("init")
.assert()
.success();
adrs()
.current_dir(temp.path())
.args(["generate", "book", "--output", "mybook"])
.assert()
.success()
.stdout(predicate::str::contains("mybook"));
temp.child("mybook/book.toml")
.assert(predicate::path::exists());
temp.child("mybook/src/SUMMARY.md")
.assert(predicate::path::exists());
temp.close().unwrap();
}