use std::fs;
use std::path::Path;
use std::process::Command;
use haz_cli::EXIT_INTERNAL_OR_CONFIG_ERROR;
use tempfile::TempDir;
const HAZ_BIN: &str = env!("CARGO_BIN_EXE_haz");
fn fixture_tempdir(name: &str) -> TempDir {
let src = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/check")
.join(name);
let dst = TempDir::new().expect("tempdir creation failed");
copy_dir_recursive(&src, dst.path());
dst
}
fn copy_dir_recursive(src: &Path, dst: &Path) {
fs::create_dir_all(dst).unwrap();
for entry in fs::read_dir(src).unwrap() {
let entry = entry.unwrap();
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if entry.file_type().unwrap().is_dir() {
copy_dir_recursive(&src_path, &dst_path);
} else {
fs::copy(&src_path, &dst_path).unwrap();
}
}
}
fn haz_check(cwd: &Path, args: &[&str]) -> (String, String, Option<i32>) {
let output = Command::new(HAZ_BIN)
.arg("check")
.args(args)
.current_dir(cwd)
.output()
.expect("failed to spawn haz");
(
String::from_utf8(output.stdout).expect("haz stdout was not utf-8"),
String::from_utf8(output.stderr).expect("haz stderr was not utf-8"),
output.status.code(),
)
}
#[test]
fn aux_010_check_clean_workspace_emits_count_line_and_exits_zero() {
let tmp = fixture_tempdir("clean_workspace");
let (stdout, stderr, code) = haz_check(tmp.path(), &[]);
assert_eq!(code, Some(0), "expected exit 0; stderr: {stderr}");
assert_eq!(stdout, "2 task(s) validated.\n");
}
#[test]
fn aux_010_check_surfaces_every_dag_error_in_one_run() {
let tmp = fixture_tempdir("multi_error");
let (stdout, stderr, _code) = haz_check(tmp.path(), &["--workspace"]);
assert!(
stdout.is_empty(),
"validation failures should not print the success line: {stdout}",
);
assert!(
stderr.contains("DAG-014"),
"diagnostic should surface the cycle rule: {stderr}",
);
assert!(
stderr.contains("DAG-011") || stderr.contains("unresolved"),
"diagnostic should surface the unresolved reference: {stderr}",
);
assert!(
stderr.contains("DAG-015"),
"diagnostic should surface the duplicate literal output rule: {stderr}",
);
}
#[test]
fn aux_012_check_returns_config_error_class_on_validation_failure() {
let tmp = fixture_tempdir("multi_error");
let (_stdout, _stderr, code) = haz_check(tmp.path(), &["--workspace"]);
assert_eq!(
code,
Some(EXIT_INTERNAL_OR_CONFIG_ERROR),
"expected EXIT_INTERNAL_OR_CONFIG_ERROR (=2)",
);
}
#[test]
fn aux_009_check_default_inside_project_counts_bearing_only() {
let tmp = fixture_tempdir("scope_flags");
let (stdout, stderr, code) = haz_check(&tmp.path().join("lib_a"), &[]);
assert_eq!(code, Some(0), "expected exit 0; stderr: {stderr}");
assert_eq!(stdout, "1 task(s) validated.\n");
}
#[test]
fn aux_009_check_all_inside_project_counts_bearing_only() {
let tmp = fixture_tempdir("scope_flags");
let (stdout, stderr, code) = haz_check(&tmp.path().join("lib_a"), &["--all"]);
assert_eq!(code, Some(0), "expected exit 0; stderr: {stderr}");
assert_eq!(stdout, "1 task(s) validated.\n");
}
#[test]
fn aux_009_check_workspace_counts_every_project() {
let tmp = fixture_tempdir("scope_flags");
let (stdout, stderr, code) = haz_check(tmp.path(), &["--workspace"]);
assert_eq!(code, Some(0), "expected exit 0; stderr: {stderr}");
assert_eq!(stdout, "3 task(s) validated.\n");
}
#[test]
fn cfg_021_command_scalar_check_renders_rich_diagnostic() {
let tmp = fixture_tempdir("workspace_load_errors/parse_errors/cfg_021_command_scalar");
let (stdout, stderr, code) = haz_check(tmp.path(), &[]);
assert_eq!(
code,
Some(EXIT_INTERNAL_OR_CONFIG_ERROR),
"expected internal/config-error class; stderr: {stderr}",
);
assert!(
stdout.is_empty(),
"validation failures should not print the success line: {stdout}",
);
assert!(
stderr.contains("haz::cfg::021"),
"stderr must surface the CFG-021 code; got: {stderr}",
);
assert!(
stderr.contains("tasks.build.command"),
"stderr must surface the resolved YAML key path; got: {stderr}",
);
assert!(
stderr.contains("echo hi"),
"stderr must include the offending source snippet; got: {stderr}",
);
assert!(
stderr.contains("command:") && stderr.contains("shell:"),
"stderr must surface the argv-vs-shell help text; got: {stderr}",
);
assert_eq!(
stderr.matches("haz::cfg::021").count(),
1,
"CFG code must appear exactly once; got: {stderr}",
);
}
#[test]
fn cfg_009_projects_scalar_check_renders_rich_diagnostic() {
let tmp = fixture_tempdir("workspace_load_errors/parse_errors/cfg_009_projects_scalar");
let (stdout, stderr, code) = haz_check(tmp.path(), &[]);
assert_eq!(
code,
Some(EXIT_INTERNAL_OR_CONFIG_ERROR),
"expected internal/config-error class; stderr: {stderr}",
);
assert!(
stdout.is_empty(),
"validation failures should not print the success line: {stdout}",
);
assert!(
stderr.contains("haz::cfg::009"),
"stderr must surface the CFG-009 code; got: {stderr}",
);
assert!(
stderr.contains("`projects`)"),
"stderr must surface the resolved YAML key path; got: {stderr}",
);
assert!(
stderr.contains("lib_core"),
"stderr must include the offending source snippet; got: {stderr}",
);
assert!(
stderr.contains("projects: []"),
"stderr must surface the empty-sequence example from the help; got: {stderr}",
);
assert_eq!(
stderr.matches("haz::cfg::009").count(),
1,
"CFG code must appear exactly once; got: {stderr}",
);
}
#[test]
fn cfg_025_overlay_missing_tasks_check_renders_rich_diagnostic() {
let tmp = fixture_tempdir("workspace_load_errors/parse_errors/cfg_025_overlay_missing_tasks");
let (stdout, stderr, code) = haz_check(tmp.path(), &[]);
assert_eq!(
code,
Some(EXIT_INTERNAL_OR_CONFIG_ERROR),
"expected internal/config-error class; stderr: {stderr}",
);
assert!(
stdout.is_empty(),
"validation failures should not print the success line: {stdout}",
);
assert!(
stderr.contains("haz::cfg::025"),
"stderr must surface the CFG-025 code; got: {stderr}",
);
assert!(
stderr.contains("tasks"),
"stderr must mention the missing `tasks` key; got: {stderr}",
);
assert!(
stderr.contains(".haz/tasks/lint.yml"),
"stderr must include the offending overlay file path; got: {stderr}",
);
assert!(
stderr.contains("must contain a top-level `tasks` key")
|| stderr.contains("MUST contain a top-level `tasks` key"),
"stderr must surface the overlay help text; got: {stderr}",
);
assert_eq!(
stderr.matches("haz::cfg::025").count(),
1,
"CFG code must appear exactly once; got: {stderr}",
);
}