mdt_cli 0.9.0

Write docs once, sync everywhere — markdown templates that eliminate doc drift
Documentation
mod common;

use insta_cmd::assert_cmd_snapshot;
use mdt_cli::AssistOutputFormat;
use mdt_cli::Assistant;
use mdt_cli::Commands;
use mdt_cli::DoctorOutputFormat;
use mdt_cli::InfoOutputFormat;
use mdt_cli::MdtCli;
use predicates::prelude::PredicateBooleanExt;
use rstest::rstest;

const ANSI_ESCAPE: &str = "\u{1b}[";

#[rstest]
#[case("check_up_to_date", "check_passes_when_up_to_date", false)]
#[case("check_stale", "check_fails_when_stale", false)]
#[case("check_no_blocks", "check_with_no_blocks", false)]
#[case("check_single_block", "check_verbose_shows_provider_count", true)]
#[case("update_orphan", "check_warns_missing_provider", false)]
#[case("check_stale_named", "check_stale_shows_block_name_and_file", false)]
#[case("check_multiple_stale", "check_multiple_stale_blocks", false)]
#[case(
	"check_undefined_vars",
	"check_warns_undefined_template_variables",
	false
)]
#[case(
	"check_valid_vars",
	"check_no_warnings_for_valid_template_variables",
	false
)]
fn check_outputs_are_snapshotted(
	#[case] fixture: &str,
	#[case] snapshot_name: &str,
	#[case] verbose: bool,
) -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture(fixture, tmp.path());

	common::with_redacted_temp_dir(tmp.path(), || {
		let mut cmd = common::mdt_cmd_for_path(tmp.path());
		if verbose {
			cmd.arg("--verbose");
		}
		cmd.arg("check");

		assert_cmd_snapshot!(snapshot_name, cmd);
	});

	Ok(())
}

#[test]
fn check_writes_project_cache_artifact() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_up_to_date", tmp.path());

	common::mdt_cmd()
		.arg("check")
		.arg("--path")
		.arg(tmp.path())
		.assert()
		.success();

	let cache_path = tmp.path().join(".mdt").join("cache").join("index-v2.json");
	assert!(
		cache_path.is_file(),
		"expected cache file at {}",
		cache_path.display()
	);

	Ok(())
}

#[test]
fn check_stale_text_output_is_colored_when_forced() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_stale", tmp.path());

	let mut cmd = common::mdt_cmd();
	cmd.env_remove("NO_COLOR")
		.env("CLICOLOR_FORCE", "1")
		.arg("check")
		.arg("--path")
		.arg(tmp.path())
		.assert()
		.failure()
		.stderr(predicates::str::contains("Check failed."))
		.stderr(predicates::str::contains(ANSI_ESCAPE));

	Ok(())
}

#[test]
fn check_stale_text_output_honors_no_color_flag_even_when_forced() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_stale", tmp.path());

	let mut cmd = common::mdt_cmd();
	cmd.env_remove("NO_COLOR")
		.env("CLICOLOR_FORCE", "1")
		.arg("--no-color")
		.arg("check")
		.arg("--path")
		.arg(tmp.path())
		.assert()
		.failure()
		.stderr(predicates::str::contains("Check failed."))
		.stderr(predicates::str::contains(ANSI_ESCAPE).not());

	Ok(())
}

#[test]
fn check_stale_text_output_honors_clicolor_zero() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_stale", tmp.path());

	let mut cmd = common::mdt_cmd();
	cmd.env_remove("NO_COLOR")
		.env("CLICOLOR", "0")
		.arg("check")
		.arg("--path")
		.arg(tmp.path())
		.assert()
		.failure()
		.stderr(predicates::str::contains("Check failed."))
		.stderr(predicates::str::contains(ANSI_ESCAPE).not());

	Ok(())
}

#[test]
fn check_validation_diagnostics_are_colored_when_forced() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_invalid_transformer", tmp.path());

	let mut cmd = common::mdt_cmd();
	cmd.env_remove("NO_COLOR")
		.env("CLICOLOR_FORCE", "1")
		.arg("check")
		.arg("--path")
		.arg(tmp.path())
		.assert()
		.failure()
		.stderr(predicates::str::contains("unknown transformer `wat`"))
		.stderr(predicates::str::contains(ANSI_ESCAPE));

	Ok(())
}

#[test]
fn check_validation_diagnostics_honor_no_color_flag_when_forced() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_invalid_transformer", tmp.path());

	let mut cmd = common::mdt_cmd();
	cmd.env_remove("NO_COLOR")
		.env("CLICOLOR_FORCE", "1")
		.arg("--no-color")
		.arg("check")
		.arg("--path")
		.arg(tmp.path())
		.assert()
		.failure()
		.stderr(predicates::str::contains("unknown transformer `wat`"))
		.stderr(predicates::str::contains(ANSI_ESCAPE).not());

	Ok(())
}

#[test]
fn check_watch_flag_is_accepted_by_cli_parser() {
	use clap::Parser;

	let cli = MdtCli::parse_from(["mdt", "check", "--watch"]);
	match cli.command {
		Some(Commands::Check { watch, diff, .. }) => {
			assert!(watch);
			assert!(!diff);
		}
		_ => panic!("expected Check command"),
	}

	let cli = MdtCli::parse_from(["mdt", "check"]);
	match cli.command {
		Some(Commands::Check { watch, .. }) => {
			assert!(!watch);
		}
		_ => panic!("expected Check command"),
	}
}

#[test]
fn check_watch_flag_accepted_by_binary() -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_up_to_date", tmp.path());

	let mut cmd = common::mdt_cmd();
	let _ = cmd
		.arg("check")
		.arg("--watch")
		.arg("--path")
		.arg(tmp.path())
		.timeout(std::time::Duration::from_secs(3))
		.assert();

	Ok(())
}

#[test]
fn info_command_is_accepted_by_cli_parser() {
	use clap::Parser;

	let cli = MdtCli::parse_from(["mdt", "info"]);
	match cli.command {
		Some(Commands::Info { format }) => {
			assert!(matches!(format, InfoOutputFormat::Text));
		}
		_ => panic!("expected Info command"),
	}

	let cli = MdtCli::parse_from(["mdt", "info", "--format", "json"]);
	match cli.command {
		Some(Commands::Info { format }) => {
			assert!(matches!(format, InfoOutputFormat::Json));
		}
		_ => panic!("expected Info command"),
	}
}

#[test]
fn doctor_command_is_accepted_by_cli_parser() {
	use clap::Parser;

	let cli = MdtCli::parse_from(["mdt", "doctor"]);
	match cli.command {
		Some(Commands::Doctor { format }) => {
			assert!(matches!(format, DoctorOutputFormat::Text));
		}
		_ => panic!("expected Doctor command"),
	}

	let cli = MdtCli::parse_from(["mdt", "doctor", "--format", "json"]);
	match cli.command {
		Some(Commands::Doctor { format }) => {
			assert!(matches!(format, DoctorOutputFormat::Json));
		}
		_ => panic!("expected Doctor command"),
	}
}

#[test]
fn assist_command_is_accepted_by_cli_parser() {
	use clap::Parser;

	let cli = MdtCli::parse_from(["mdt", "assist", "claude"]);
	match cli.command {
		Some(Commands::Assist { assistant, format }) => {
			assert!(matches!(assistant, Assistant::Claude));
			assert!(matches!(format, AssistOutputFormat::Text));
		}
		_ => panic!("expected Assist command"),
	}

	let cli = MdtCli::parse_from(["mdt", "assist", "pi", "--format", "json"]);
	match cli.command {
		Some(Commands::Assist { assistant, format }) => {
			assert!(matches!(assistant, Assistant::Pi));
			assert!(matches!(format, AssistOutputFormat::Json));
		}
		_ => panic!("expected Assist command"),
	}
}

#[rstest]
#[case("info", "info_json_includes_cache_observability_fields")]
#[case("doctor", "doctor_json_includes_cache_checks")]
fn cache_observability_outputs_are_snapshotted(
	#[case] command_name: &str,
	#[case] snapshot_name: &str,
) -> std::io::Result<()> {
	let tmp = tempfile::tempdir()?;
	common::copy_fixture("check_up_to_date", tmp.path());

	common::with_redacted_temp_dir(tmp.path(), || {
		assert_cmd_snapshot!(
			snapshot_name,
			common::mdt_cmd_for_path(tmp.path())
				.arg(command_name)
				.arg("--format")
				.arg("json")
		);
	});

	Ok(())
}