govctl 0.19.0

Project governance CLI for RFC, ADR, and Work Item management
use super::*;

fn write_rfc_fixture(
    project_dir: &std::path::Path,
    title: &str,
    version: &str,
    status: &str,
    phase: &str,
    clauses: &[(&str, &str, &str)],
    changelog_entry: &str,
) -> std::io::Result<std::path::PathBuf> {
    let rfc_dir = project_dir.join("gov/rfc/RFC-0001");
    fs::create_dir_all(rfc_dir.join("clauses"))?;

    let clause_paths = clauses
        .iter()
        .map(|(id, _, _)| format!("\"clauses/{id}.toml\""))
        .collect::<Vec<_>>()
        .join(", ");

    fs::write(
        rfc_dir.join("rfc.toml"),
        format!(
            r#"#:schema ../../schema/rfc.schema.json

[govctl]
id = "RFC-0001"
title = "{title}"
version = "{version}"
status = "{status}"
phase = "{phase}"
owners = ["test@example.com"]
created = "2026-01-01"

[[sections]]
title = "Specification"
clauses = [{clause_paths}]

[[changelog]]
version = "{version}"
date = "2026-01-01"
{changelog_entry}
"#
        ),
    )?;

    for (id, title, text) in clauses {
        write_clause_fixture(&rfc_dir, id, title, version, text)?;
    }

    Ok(rfc_dir)
}

fn write_clause_fixture(
    rfc_dir: &std::path::Path,
    id: &str,
    title: &str,
    since: &str,
    text: &str,
) -> std::io::Result<()> {
    fs::write(
        rfc_dir.join(format!("clauses/{id}.toml")),
        format!(
            r#"#:schema ../../schema/clause.schema.json

[govctl]
id = "{id}"
title = "{title}"
kind = "normative"
status = "active"
since = "{since}"

[content]
text = "{text}"
"#
        ),
    )
}

#[test]
fn test_delete_clause_safeguard_normative() -> TestResult {
    let (temp_dir, date) = init_project_with_date()?;

    write_rfc_fixture(
        temp_dir.path(),
        "Normative RFC",
        "1.0.0",
        "normative",
        "stable",
        &[(
            "C-LOCKED",
            "Locked Clause",
            "This clause cannot be deleted - RFC is normative.",
        )],
        r#"added = ["Initial release"]"#,
    )?;

    let output = run_commands(
        temp_dir.path(),
        &[&["clause", "delete", "RFC-0001:C-LOCKED", "-f"]],
    )?;
    assert_delete_snapshot!(normalize_output(&output, temp_dir.path(), &date)?);

    Ok(())
}

#[test]
fn test_delete_clause_success_draft() -> TestResult {
    let (temp_dir, date) = init_project_with_date()?;

    write_rfc_fixture(
        temp_dir.path(),
        "Draft RFC",
        "0.1.0",
        "draft",
        "spec",
        &[
            ("C-KEEP", "Keep This Clause", "This clause will remain."),
            (
                "C-DELETE",
                "Delete This Clause",
                "This clause will be deleted.",
            ),
        ],
        r#"notes = "Initial draft""#,
    )?;

    let output = run_commands(
        temp_dir.path(),
        &[
            &["clause", "delete", "RFC-0001:C-DELETE", "-f"],
            &["clause", "list", "RFC-0001"],
            &["check"],
        ],
    )?;
    assert_delete_snapshot!(normalize_output(&output, temp_dir.path(), &date)?);

    Ok(())
}

#[test]
fn test_delete_clause_success_current_normative_spec_candidate() -> TestResult {
    let (temp_dir, date) = init_project_with_date()?;

    write_rfc_fixture(
        temp_dir.path(),
        "Normative spec RFC",
        "1.1.0",
        "normative",
        "spec",
        &[
            ("C-KEEP", "Keep This Clause", "This clause will remain."),
            (
                "C-DELETE",
                "Delete This Clause",
                "This candidate Clause references [[RFC-0001:C-DELETE]] itself.",
            ),
        ],
        r#"added = ["Current candidate"]"#,
    )?;

    let output = run_commands(
        temp_dir.path(),
        &[
            &["clause", "delete", "RFC-0001:C-DELETE", "-f"],
            &["clause", "list", "RFC-0001"],
            &["check"],
        ],
    )?;
    assert_delete_snapshot!(normalize_output(&output, temp_dir.path(), &date)?);
    Ok(())
}

#[test]
fn test_delete_clause_rejects_inherited_normative_spec_clause() -> TestResult {
    let (temp_dir, _) = init_project_with_date()?;
    let rfc_dir = write_rfc_fixture(
        temp_dir.path(),
        "Normative spec RFC",
        "1.1.0",
        "normative",
        "spec",
        &[(
            "C-INHERITED",
            "Inherited Clause",
            "This clause was introduced in an earlier version.",
        )],
        r#"added = ["Current candidate"]"#,
    )?;
    let clause_path = rfc_dir.join("clauses/C-INHERITED.toml");
    let mut clause: toml::Value = toml::from_str(&fs::read_to_string(&clause_path)?)?;
    clause["govctl"]["since"] = toml::Value::String("1.0.0".to_string());
    fs::write(&clause_path, toml::to_string_pretty(&clause)?)?;
    let rfc_before = fs::read(rfc_dir.join("rfc.toml"))?;
    let clause_before = fs::read(&clause_path)?;

    let output = run_commands(
        temp_dir.path(),
        &[&["clause", "delete", "RFC-0001:C-INHERITED", "-f"]],
    )?;

    assert!(output.contains("error[E0104]"), "output: {output}");
    assert!(output.contains("version=1.1.0"), "output: {output}");
    assert!(output.contains("clause since=1.0.0"), "output: {output}");
    assert_eq!(fs::read(rfc_dir.join("rfc.toml"))?, rfc_before);
    assert_eq!(fs::read(&clause_path)?, clause_before);
    Ok(())
}

#[test]
fn test_delete_clause_safeguard_referenced_by_artifacts() -> TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    let work_id = first_work_id(&date);

    let rfc_dir = write_rfc_fixture(
        temp_dir.path(),
        "Normative spec RFC",
        "0.1.0",
        "normative",
        "spec",
        &[(
            "C-DELETE",
            "Referenced Clause",
            "This clause is still referenced.",
        )],
        r#"added = ["Current candidate"]"#,
    )?;

    let commands: Vec<Vec<String>> = vec![
        command(&["rfc", "new", "Referencing RFC"]),
        command(&[
            "rfc",
            "edit",
            "RFC-0002",
            "refs",
            "--add",
            "RFC-0001:C-DELETE",
        ]),
        command(&["adr", "new", "Referencing ADR"]),
        command(&[
            "adr",
            "edit",
            "ADR-0001",
            "refs",
            "--add",
            "RFC-0001:C-DELETE",
        ]),
        work_new("Referencing work item"),
        work_add_field(&work_id, "refs", "RFC-0001:C-DELETE"),
        command(&["guard", "new", "Clause guard"]),
        command(&[
            "guard",
            "edit",
            "GUARD-CLAUSE-GUARD",
            "refs",
            "--add",
            "RFC-0001:C-DELETE",
        ]),
        command(&["clause", "delete", "RFC-0001:C-DELETE", "-f"]),
    ];

    let output = run_dynamic_commands(temp_dir.path(), &commands)?;
    assert!(output.contains("exit: 1"), "output: {output}");
    assert!(output.contains("Cannot delete clause"), "output: {output}");
    assert!(output.contains("RFC-0002"), "output: {output}");
    assert!(output.contains("ADR-0001"), "output: {output}");
    assert!(output.contains(&work_id), "output: {output}");
    assert!(output.contains("GUARD-CLAUSE-GUARD"), "output: {output}");
    assert!(
        rfc_dir.join("clauses/C-DELETE.toml").exists(),
        "referenced clause should remain on disk"
    );

    Ok(())
}

#[test]
fn test_delete_clause_reports_inline_and_supersession_referrers() -> TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    let work_id = first_work_id(&date);

    let rfc_dir = write_rfc_fixture(
        temp_dir.path(),
        "Normative spec RFC",
        "0.1.0",
        "normative",
        "spec",
        &[(
            "C-DELETE",
            "Referenced Clause",
            "This clause is still referenced.",
        )],
        r#"added = ["Current candidate"]"#,
    )?;

    let commands: Vec<Vec<String>> = vec![
        command(&[
            "clause",
            "new",
            "RFC-0001:C-OLD",
            "Old Clause",
            "-s",
            "Specification",
            "-k",
            "normative",
        ]),
        command(&[
            "clause",
            "supersede",
            "RFC-0001:C-OLD",
            "--by",
            "C-DELETE",
            "--force",
        ]),
        command(&["rfc", "new", "Inline RFC"]),
        command(&[
            "clause",
            "new",
            "RFC-0002:C-INLINE",
            "Inline Clause",
            "-s",
            "Specification",
            "-k",
            "normative",
        ]),
        command(&[
            "clause",
            "edit",
            "RFC-0002:C-INLINE",
            "text",
            "--set",
            "Uses [[RFC-0001:C-DELETE]].",
        ]),
        command(&["adr", "new", "Inline ADR"]),
        command(&[
            "adr",
            "edit",
            "ADR-0001",
            "context",
            "--set",
            "Uses [[RFC-0001:C-DELETE]].",
        ]),
        work_new("Inline work item"),
        command(&[
            "work",
            "edit",
            &work_id,
            "description",
            "--set",
            "Uses [[RFC-0001:C-DELETE]].",
        ]),
        command(&["clause", "delete", "RFC-0001:C-DELETE", "-f"]),
    ];

    let output = run_dynamic_commands(temp_dir.path(), &commands)?;
    assert!(output.contains("error[E0211]"), "output: {output}");
    assert!(output.contains("RFC-0001:C-OLD"), "output: {output}");
    assert!(output.contains("RFC-0002:C-INLINE"), "output: {output}");
    assert!(output.contains("ADR-0001"), "output: {output}");
    assert!(output.contains(&work_id), "output: {output}");
    assert!(
        rfc_dir.join("clauses/C-DELETE.toml").exists(),
        "referenced clause should remain on disk"
    );
    Ok(())
}