pmat 3.16.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
//! Spec Management CLI Handlers (master-plan-pmat-work-system.md)
//!
//! Implements S-001 through S-010 acceptance criteria for specification management.

#![cfg_attr(coverage_nightly, coverage(off))]

use crate::cli::commands::SpecOutputFormat;
use crate::services::spec_parser::{ParsedSpec, SpecParser};
use std::fs;
use std::path::Path;

// Command handlers: handle_spec_score, handle_spec_comply, handle_spec_create, handle_spec_list
include!("spec_handlers_commands.rs");

// Scoring and formatting: calculate_spec_score, format_spec_score_*
include!("spec_handlers_scoring.rs");

// Sync and drift: handle_spec_sync, handle_spec_drift
include!("spec_handlers_sync.rs");

// Tests split for file health compliance (CB-040). Re-enabled after fixing
// the nested `mod tests` issue per spec §4.5 R1 (revive broken-tests gate).
#[cfg(test)]
#[path = "tests.rs"]
mod tests;

#[cfg(test)]
mod sync_pure_tests {
    //! Covers pure helpers from spec_handlers_sync.rs (298 lines, 0 tests
    //! reachable in the default build). Skips fn that hit the filesystem
    //! (sync_spec_to_roadmap, collect_drift_orphans, handle_spec_sync,
    //! handle_spec_drift) — those need the parent's broken-tests rig.
    use super::*;
    use crate::models::roadmap::RoadmapItem;
    use std::path::PathBuf;

    fn drift(path: &str, has_ticket: bool, ticket_id: Option<&str>, linked: bool) -> DriftInfo {
        DriftInfo {
            path: PathBuf::from(path),
            title: format!("{path}-title"),
            has_ticket,
            ticket_id: ticket_id.map(String::from),
            linked_in_roadmap: linked,
        }
    }

    // ── update_roadmap_item ──

    #[test]
    fn test_update_roadmap_item_no_match_no_changes() {
        let mut items = vec![RoadmapItem::new("OTHER-1".into(), "x".into())];
        let mut updates = Vec::new();
        update_roadmap_item(
            &mut items,
            "PMAT-1",
            Path::new("docs/foo.md"),
            false,
            &mut updates,
        );
        assert!(updates.is_empty());
        assert!(items[0].spec.is_none());
    }

    #[test]
    fn test_update_roadmap_item_case_insensitive_match() {
        let mut items = vec![RoadmapItem::new("PMAT-100".into(), "x".into())];
        let mut updates = Vec::new();
        // Lowercase "pmat-100" should still match "PMAT-100".
        update_roadmap_item(
            &mut items,
            "pmat-100",
            Path::new("docs/spec.md"),
            false,
            &mut updates,
        );
        assert_eq!(updates.len(), 1);
        assert_eq!(items[0].spec, Some(PathBuf::from("docs/spec.md")));
    }

    #[test]
    fn test_update_roadmap_item_dry_run_records_but_no_mutation() {
        let mut items = vec![RoadmapItem::new("PMAT-1".into(), "x".into())];
        let mut updates = Vec::new();
        update_roadmap_item(
            &mut items,
            "PMAT-1",
            Path::new("docs/spec.md"),
            true, // dry_run
            &mut updates,
        );
        assert_eq!(updates.len(), 1);
        // dry_run → no mutation.
        assert!(items[0].spec.is_none());
    }

    #[test]
    fn test_update_roadmap_item_already_correct_no_update() {
        let mut items = vec![RoadmapItem::new("PMAT-1".into(), "x".into())];
        items[0].spec = Some(PathBuf::from("docs/spec.md"));
        let mut updates = Vec::new();
        update_roadmap_item(
            &mut items,
            "PMAT-1",
            Path::new("docs/spec.md"),
            false,
            &mut updates,
        );
        // No change needed — no update emitted.
        assert!(updates.is_empty());
    }

    // ── print_drift_* ──

    #[test]
    fn test_print_drift_text_empty_emits_pass_message() {
        // Smoke: no panic on empty input, writes to stdout.
        print_drift_text(&[]);
    }

    #[test]
    fn test_print_drift_text_with_orphans() {
        let orphans = vec![
            drift("docs/a.md", true, Some("PMAT-1"), false),
            drift("docs/b.md", false, None, false),
        ];
        print_drift_text(&orphans);
    }

    #[test]
    fn test_print_drift_markdown_empty() {
        print_drift_markdown(&[]);
    }

    #[test]
    fn test_print_drift_markdown_with_orphans_emits_table() {
        let orphans = vec![drift("docs/a.md", true, Some("PMAT-1"), true)];
        print_drift_markdown(&orphans);
    }

    #[test]
    fn test_print_drift_json_returns_ok() {
        let orphans = vec![drift("docs/a.md", false, None, false)];
        // Parsable JSON output, no panic.
        print_drift_json(&orphans).unwrap();
    }

    #[test]
    fn test_print_drift_json_empty() {
        print_drift_json(&[]).unwrap();
    }

    #[test]
    fn test_print_drift_report_dispatcher_arms() {
        let orphans = vec![drift("docs/a.md", true, Some("X"), true)];
        print_drift_report(&orphans, SpecOutputFormat::Text).unwrap();
        print_drift_report(&orphans, SpecOutputFormat::Json).unwrap();
        print_drift_report(&orphans, SpecOutputFormat::Markdown).unwrap();
    }
}