mod common;
use common::run_commands;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_init_creates_gitignore() -> common::TestResult {
let temp_dir = TempDir::new()?;
let output = run_commands(temp_dir.path(), &[&["init"]])?;
assert!(output.contains("Project initialized"));
let gitignore_path = temp_dir.path().join(".gitignore");
assert!(gitignore_path.exists(), ".gitignore should be created");
let content = fs::read_to_string(&gitignore_path)?;
assert!(
content.contains(".govctl.lock"),
".gitignore should contain .govctl.lock"
);
assert!(
content.contains(".govctl/"),
".gitignore should contain .govctl/"
);
Ok(())
}
#[test]
fn test_init_recovers_partial_directories_without_governance_artifacts() -> common::TestResult {
let temp_dir = TempDir::new()?;
fs::create_dir_all(temp_dir.path().join("gov/rfc"))?;
fs::create_dir_all(temp_dir.path().join("gov/adr"))?;
fs::create_dir_all(temp_dir.path().join("gov/schema"))?;
fs::write(temp_dir.path().join("gov/.DS_Store"), "metadata")?;
fs::write(
temp_dir.path().join("gov/schema/rfc.schema.json"),
"partial generated state",
)?;
let output = run_commands(temp_dir.path(), &[&["init", "--force"]])?;
assert!(output.contains("Project initialized"), "{output}");
assert!(temp_dir.path().join("gov/config.toml").exists());
Ok(())
}
#[test]
fn test_init_in_nested_directory_does_not_select_ancestor_project() -> common::TestResult {
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let outer_config = temp_dir.path().join("gov/config.toml");
let outer_before = fs::read(&outer_config)?;
let inner = temp_dir.path().join("inner");
fs::create_dir(&inner)?;
let output = run_commands(&inner, &[&["init"]])?;
assert!(output.contains("Project initialized"), "{output}");
assert!(inner.join("gov/config.toml").exists());
assert_eq!(fs::read(&outer_config)?, outer_before);
Ok(())
}
#[test]
fn test_init_force_recovers_nested_partial_project_without_rewriting_ancestor() -> common::TestResult
{
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let outer_config = temp_dir.path().join("gov/config.toml");
let outer_before = fs::read(&outer_config)?;
let inner = temp_dir.path().join("inner");
fs::create_dir_all(inner.join("gov/rfc"))?;
fs::create_dir_all(inner.join("gov/schema"))?;
let output = run_commands(&inner, &[&["init", "--force"]])?;
assert!(output.contains("Project initialized"), "{output}");
assert!(inner.join("gov/config.toml").exists());
assert_eq!(fs::read(&outer_config)?, outer_before);
Ok(())
}
#[test]
fn test_init_force_dry_run_in_nested_directory_does_not_mutate_either_project() -> common::TestResult
{
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let outer_config = temp_dir.path().join("gov/config.toml");
let outer_before = fs::read(&outer_config)?;
let inner = temp_dir.path().join("inner");
fs::create_dir(&inner)?;
run_commands(&inner, &[&["--dry-run", "init", "--force"]])?;
assert!(!inner.join("gov").exists());
assert_eq!(fs::read(&outer_config)?, outer_before);
Ok(())
}
#[test]
fn test_init_force_rejects_unsupported_existing_schema_without_mutation() -> common::TestResult {
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let config_path = temp_dir.path().join("gov/config.toml");
let mut config: toml::Value = toml::from_str(&fs::read_to_string(&config_path)?)?;
config["schema"]["version"] = toml::Value::Integer(2);
fs::write(&config_path, toml::to_string_pretty(&config)?)?;
let before = fs::read(&config_path)?;
let output = run_commands(temp_dir.path(), &[&["init", "--force"]])?;
assert!(output.contains("error[E0505]"), "{output}");
assert!(
output.contains("schema version 2 is unsupported"),
"{output}"
);
assert_eq!(fs::read(&config_path)?, before);
Ok(())
}
#[test]
fn test_init_dry_run_rejects_existing_project_without_force() -> common::TestResult {
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let config_path = temp_dir.path().join("gov/config.toml");
let before = fs::read(&config_path)?;
let output = run_commands(temp_dir.path(), &[&["init", "--dry-run"]])?;
assert!(output.contains("exit: 1"), "{output}");
assert!(output.contains("already exists"), "{output}");
assert!(!output.contains("Would write"), "{output}");
assert_eq!(fs::read(&config_path)?, before);
Ok(())
}
#[cfg(unix)]
#[test]
fn test_init_allows_symlinked_default_governance_root() -> common::TestResult {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new()?;
let project_dir = temp_dir.path().join("project");
let external_gov = temp_dir.path().join("external-gov");
fs::create_dir(&project_dir)?;
fs::create_dir(&external_gov)?;
symlink(&external_gov, project_dir.join("gov"))?;
let output = run_commands(
&project_dir,
&[&["init"], &["rfc", "new", "Symlinked storage"], &["check"]],
)?;
assert!(output.contains("Project initialized"), "{output}");
assert!(output.contains("Created RFC"), "{output}");
assert!(output.contains("All checks passed"), "{output}");
assert!(external_gov.join("config.toml").exists());
assert!(external_gov.join("rfc/RFC-0001/rfc.toml").exists());
Ok(())
}
#[test]
fn test_init_config_reserves_default_guards_for_universal_checks() -> common::TestResult {
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let content = fs::read_to_string(temp_dir.path().join("gov/config.toml"))?;
assert!(
content.contains("# default_guards = [\"GUARD-GOVCTL-CHECK\"]"),
"config should show a narrow universal default: {content}"
);
assert!(
!content.contains("# default_guards = [\"GUARD-GOVCTL-CHECK\", \"GUARD-CARGO-TEST\"]"),
"config should not present the full test suite as a project default: {content}"
);
assert!(
content.contains("verification.required_guards"),
"config should direct scoped checks to work items: {content}"
);
Ok(())
}
#[test]
fn test_init_creates_artifact_schema_files() -> common::TestResult {
let temp_dir = TempDir::new()?;
let output = run_commands(temp_dir.path(), &[&["init"]])?;
assert!(output.contains("Project initialized"));
for filename in [
"rfc.schema.json",
"clause.schema.json",
"adr.schema.json",
"work.schema.json",
"release.schema.json",
"guard.schema.json",
"loop-state.schema.json",
"loop-round.schema.json",
] {
let schema_path = temp_dir.path().join("gov/schema").join(filename);
assert!(
schema_path.exists(),
"schema file should exist: {}",
filename
);
let schema_text = fs::read_to_string(&schema_path)?;
let schema_value: serde_json::Value = serde_json::from_str(&schema_text)?;
jsonschema::validator_for(&schema_value)?;
if filename == "work.schema.json" {
assert!(
!schema_text.contains("journal"),
"work schema should not advertise legacy inline execution history"
);
}
}
assert!(
temp_dir.path().join("gov/guard").exists(),
"guard directory should exist after init"
);
Ok(())
}
#[test]
fn test_init_appends_to_existing_gitignore() -> common::TestResult {
let temp_dir = TempDir::new()?;
let gitignore_path = temp_dir.path().join(".gitignore");
fs::write(&gitignore_path, "# Existing content\ntarget/\n")?;
let output = run_commands(temp_dir.path(), &[&["init"]])?;
assert!(output.contains("Project initialized"));
let content = fs::read_to_string(&gitignore_path)?;
assert!(
content.contains("target/"),
".gitignore should retain existing content"
);
assert!(
content.contains(".govctl.lock"),
".gitignore should have .govctl.lock appended"
);
assert!(
content.contains(".govctl/"),
".gitignore should have .govctl/ appended"
);
Ok(())
}
#[test]
fn test_init_appends_to_existing_gitignore_without_trailing_newline() -> common::TestResult {
let temp_dir = TempDir::new()?;
let gitignore_path = temp_dir.path().join(".gitignore");
fs::write(&gitignore_path, "target/")?;
let output = run_commands(temp_dir.path(), &[&["init"]])?;
assert!(output.contains("Project initialized"));
let content = fs::read_to_string(&gitignore_path)?;
assert_eq!(content, "target/\n.govctl.lock\n.govctl/\n");
Ok(())
}
#[test]
fn test_init_reports_gitignore_read_error() -> common::TestResult {
let temp_dir = TempDir::new()?;
fs::create_dir(temp_dir.path().join(".gitignore"))?;
let output = run_commands(temp_dir.path(), &[&["init"]])?;
assert!(output.contains("error"), "output: {}", output);
assert!(output.contains("read .gitignore"), "output: {}", output);
assert!(output.contains(".gitignore"), "output: {}", output);
assert!(output.contains("exit: 1"), "output: {}", output);
Ok(())
}
#[test]
fn test_init_no_duplicate_gitignore_entry() -> common::TestResult {
let temp_dir = TempDir::new()?;
let gitignore_path = temp_dir.path().join(".gitignore");
fs::write(&gitignore_path, ".govctl.lock\n.govctl/\n")?;
let output = run_commands(temp_dir.path(), &[&["init"]])?;
assert!(output.contains("Project initialized"));
let content = fs::read_to_string(&gitignore_path)?;
let lock_count = content.matches(".govctl.lock").count();
assert_eq!(
lock_count, 1,
".gitignore should not have duplicate .govctl.lock entries"
);
let state_count = content.matches(".govctl/").count();
assert_eq!(
state_count, 1,
".gitignore should not have duplicate .govctl/ entries"
);
Ok(())
}
#[test]
fn test_init_custom_docs_output() -> common::TestResult {
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let config_path = temp_dir.path().join("gov/config.toml");
let mut config: toml::Value = toml::from_str(&fs::read_to_string(&config_path)?)?;
config["paths"]["docs_output"] = toml::Value::String("documentation".to_string());
fs::write(&config_path, toml::to_string_pretty(&config)?)?;
let output = run_commands(temp_dir.path(), &[&["rfc", "new", "Test RFC"]])?;
assert!(output.contains("Created RFC"));
run_commands(temp_dir.path(), &[&["rfc", "render", "RFC-0001"]])?;
let docs_dir = temp_dir.path().join("documentation/rfc");
assert!(docs_dir.exists(), "docs should be under documentation/rfc/");
Ok(())
}
#[test]
fn test_init_custom_paths_combined() -> common::TestResult {
let temp_dir = TempDir::new()?;
run_commands(temp_dir.path(), &[&["init"]])?;
let config_path = temp_dir.path().join("gov/config.toml");
let mut config: toml::Value = toml::from_str(&fs::read_to_string(&config_path)?)?;
config["paths"]["docs_output"] = toml::Value::String("output/docs".to_string());
fs::write(&config_path, toml::to_string_pretty(&config)?)?;
let output = run_commands(temp_dir.path(), &[&["adr", "new", "Test ADR"]])?;
assert!(output.contains("Created ADR"), "output: {}", output);
let adr_dir = temp_dir.path().join("gov/adr");
assert!(
adr_dir.exists() && adr_dir.read_dir()?.count() > 0,
"ADR should be under gov/adr/"
);
let _output = run_commands(temp_dir.path(), &[&["adr", "render", "ADR-0001"]])?;
let docs_dir = temp_dir.path().join("output/docs/adr");
assert!(docs_dir.exists(), "docs should be under output/docs/adr/");
Ok(())
}