govctl 0.16.0

Project governance CLI for RFC, ADR, and Work Item management
//! Happy path integration tests - basic functionality validation.

mod common;

use common::{
    first_work_id, init_project_with_date, run_commands, run_dynamic_commands, work_add_acceptance,
    work_new_active,
};
use std::fs;
use std::path::Path;

/// Create a minimal valid project with RFC, clause, ADR, and work item
fn setup_minimal_valid(dir: &Path, date: &str) -> common::TestResult {
    let wi1 = first_work_id(date);

    // Create RFC with clause
    let rfc_dir = dir.join("gov/rfc/RFC-0001");
    fs::create_dir_all(rfc_dir.join("clauses"))?;

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

[govctl]
id = "RFC-0001"
title = "Test RFC"
version = "1.0.0"
status = "normative"
phase = "stable"
owners = ["test@example.com"]
created = "2026-01-01"

[[sections]]
title = "Overview"
clauses = ["clauses/C-EXAMPLE.toml"]

[[changelog]]
version = "1.0.0"
date = "2026-01-01"
added = ["Initial release"]
"#,
    )?;

    fs::write(
        rfc_dir.join("clauses/C-EXAMPLE.toml"),
        r#"#:schema ../../../schema/clause.schema.json

[govctl]
id = "C-EXAMPLE"
title = "Example Clause"
kind = "normative"
status = "active"
since = "1.0.0"

[content]
text = "This is an example clause for testing."
"#,
    )?;

    // Create ADR
    fs::write(
        dir.join("gov/adr/ADR-0001-test-decision.toml"),
        r#"[govctl]
id = "ADR-0001"
title = "Test Decision"
status = "accepted"
date = "2026-01-01"
refs = ["RFC-0001"]

[content]
context = "We need to test ADR functionality."
decision = "We will create a test ADR."
consequences = "Tests will pass."
"#,
    )?;

    // Create work item via commands
    let commands: Vec<Vec<String>> = vec![
        work_new_active("Test work item"),
        work_add_acceptance(&wi1, "add: Test criterion"),
    ];

    let _ = run_dynamic_commands(dir, &commands)?;
    Ok(())
}

#[test]
fn test_minimal_valid_check() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[&["check"]]
    );
    Ok(())
}

#[test]
fn test_minimal_valid_list_rfc() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[&["rfc", "list"]]
    );
    Ok(())
}

#[test]
fn test_minimal_valid_list_clause() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[&["clause", "list"]]
    );
    Ok(())
}

#[test]
fn test_minimal_valid_list_adr() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[&["adr", "list"]]
    );
    Ok(())
}

#[test]
fn test_minimal_valid_list_work() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[&["work", "list"]]
    );
    Ok(())
}

#[test]
fn test_work_list_status_filters() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    let output = run_commands(
        temp_dir.path(),
        &[
            &["work", "list", "active", "-o", "json"],
            &["work", "list", "queue", "-o", "json"],
        ],
    )?;

    assert!(
        output.contains(&format!("\"id\": \"WI-{date}-001\""))
            && output.contains("\"status\": \"active\"")
            && output.contains("\"title\": \"Test work item\""),
        "active filter should include the active Work Item: {output}"
    );
    assert_eq!(
        output.matches("WI-").count(),
        1,
        "queue filter should not repeat the active Work Item: {output}"
    );
    Ok(())
}

#[test]
fn test_minimal_valid_list_json_and_yaml_output() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    let output = run_commands(
        temp_dir.path(),
        &[
            &["rfc", "list", "-o", "json"],
            &["work", "list", "-o", "yaml"],
        ],
    )?;

    assert!(output.contains("\"id\": \"RFC-0001\""), "output: {output}");
    assert!(output.contains("\"phase\": \"stable\""), "output: {output}");
    assert!(
        output.contains(&format!("id: WI-{date}-001"))
            && output.contains("status: active")
            && output.contains("title: Test work item"),
        "output: {output}"
    );
    Ok(())
}

#[test]
fn test_minimal_valid_status() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[&["status"]]
    );
    Ok(())
}

#[test]
fn test_status_covers_terminal_adrs_guards_and_releases() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;
    let work_id = first_work_id(&date);

    let output = run_dynamic_commands(
        temp_dir.path(),
        &[
            vec![
                "adr".to_string(),
                "new".to_string(),
                "Rejected decision".to_string(),
            ],
            vec![
                "adr".to_string(),
                "reject".to_string(),
                "ADR-0002".to_string(),
            ],
            vec![
                "guard".to_string(),
                "new".to_string(),
                "Status guard".to_string(),
            ],
            vec![
                "work".to_string(),
                "edit".to_string(),
                work_id.clone(),
                "acceptance_criteria[0]".to_string(),
                "--tick".to_string(),
                "done".to_string(),
            ],
            vec![
                "work".to_string(),
                "move".to_string(),
                work_id,
                "done".to_string(),
            ],
            vec!["release".to_string(), "1.0.0".to_string()],
            vec!["status".to_string()],
        ],
    )?;

    assert!(output.contains("  rejected     1"), "{output}");
    assert!(
        output.contains("Verification Guards\n  Total        1"),
        "{output}"
    );
    assert!(output.contains("Releases\n  Total        1"), "{output}");
    Ok(())
}

#[test]
fn test_minimal_valid_full_workflow() -> common::TestResult {
    let (temp_dir, date) = init_project_with_date()?;
    setup_minimal_valid(temp_dir.path(), &date)?;

    crate::assert_normalized_command_snapshot!(
        "test_happy_path",
        temp_dir.path(),
        &date,
        &[
            &["check"],
            &["rfc", "list"],
            &["clause", "list"],
            &["adr", "list"],
            &["work", "list"],
            &["status"],
        ],
    );
    Ok(())
}