#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::commands::{QaOutputFormat, QaTaskType, QaWorkCommands};
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaChecklist {
pub task_id: String,
pub task_type: String,
pub generated: DateTime<Utc>,
pub categories: ChecklistCategories,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExampleScript {
pub name: String,
pub content: String,
pub description: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum EpicStatus {
Complete,
InProgress,
Pending,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EpicSummary {
pub epic_id: String,
pub total_tasks: usize,
pub total_checks: u32,
pub passed_checks: u32,
pub overall_score: f64,
pub status: EpicStatus,
pub task_scores: Vec<(String, f64)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChecklistCategories {
pub safety_ethics: Vec<ChecklistItem>,
pub code_quality: Vec<ChecklistItem>,
pub testing: Vec<ChecklistItem>,
pub documentation: Vec<ChecklistItem>,
pub process: Vec<ChecklistItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChecklistItem {
pub id: String,
pub description: String,
#[serde(deserialize_with = "deserialize_bool_lenient")]
pub checked: bool,
#[serde(deserialize_with = "deserialize_bool_lenient")]
pub automated: bool,
pub evidence: Option<String>,
}
fn deserialize_bool_lenient<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de;
#[derive(Deserialize)]
#[serde(untagged)]
enum BoolOrString {
Bool(bool),
Str(String),
}
match BoolOrString::deserialize(deserializer)? {
BoolOrString::Bool(b) => Ok(b),
BoolOrString::Str(s) => match s.to_lowercase().as_str() {
"true" | "yes" | "1" => Ok(true),
"false" | "no" | "0" => Ok(false),
other => Err(de::Error::custom(format!("invalid bool string: {other}"))),
},
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QaValidationResult {
pub task_id: String,
pub timestamp: DateTime<Utc>,
pub categories: HashMap<String, CategoryResult>,
pub overall_score: f64,
pub passed: bool,
pub manual_checks_required: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CategoryResult {
pub name: String,
pub passed: u32,
pub total: u32,
pub items: Vec<ValidationItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationItem {
pub id: String,
pub description: String,
pub status: ValidationStatus,
pub value: Option<String>,
pub threshold: Option<String>,
pub evidence: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ValidationStatus {
Passed,
Failed,
Warning,
Skipped,
Manual,
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_qa_work_command(command: QaWorkCommands) -> Result<()> {
match command {
QaWorkCommands::GenerateChecklist {
task_id,
task_type,
path,
output,
} => handle_generate_checklist(&task_id, task_type, &path, output.as_deref()).await,
QaWorkCommands::Validate {
task_id,
path,
strict,
format,
} => handle_validate(&task_id, &path, strict, format).await,
QaWorkCommands::Report {
task_id,
path,
with_evidence,
output,
format,
} => handle_report(&task_id, &path, with_evidence, output.as_deref(), format).await,
QaWorkCommands::Summary {
task_id,
path,
epic,
} => handle_summary(task_id.as_deref(), &path, epic.as_deref()).await,
QaWorkCommands::GenerateExamples {
task_id,
feature_name,
path,
output,
} => handle_generate_examples(&task_id, &feature_name, &path, output.as_deref()).await,
QaWorkCommands::Spec {
target,
path,
full,
format,
output,
threshold,
gateway_threshold,
} => {
handle_spec(
&target,
&path,
full,
format,
output.as_deref(),
threshold,
gateway_threshold,
)
.await
}
}
}
async fn handle_generate_checklist(
task_id: &str,
task_type: QaTaskType,
project_path: &Path,
output: Option<&Path>,
) -> Result<()> {
println!("Generating QA checklist for task: {}", task_id);
let checklist = generate_checklist(task_id, task_type);
let qa_dir = project_path.join(".pmat-qa").join(task_id);
fs::create_dir_all(&qa_dir)?;
let output_path = output
.map(PathBuf::from)
.unwrap_or_else(|| qa_dir.join("checklist.yaml"));
let yaml = serde_yaml_ng::to_string(&checklist)?;
fs::write(&output_path, &yaml)?;
println!("\n{}", format_checklist_text(&checklist));
println!("\nChecklist saved to: {}", output_path.display());
Ok(())
}
include!("impl_checklist_gen.rs");
include!("impl_validation.rs");
include!("impl_print.rs");
include!("impl_epic.rs");
include!("impl_spec.rs");
#[cfg(all(test, feature = "broken-tests"))]
#[path = "tests.rs"]
mod tests;
#[cfg(test)]
mod validation_format_tests {
use super::*;
fn make_item(id: &str, checked: bool, automated: bool) -> ChecklistItem {
ChecklistItem {
id: id.to_string(),
description: format!("{id}-desc"),
checked,
automated,
evidence: None,
}
}
fn make_checklist_with_all_categories() -> QaChecklist {
QaChecklist {
task_id: "PMAT-999".to_string(),
task_type: "Feature".to_string(),
generated: Utc::now(),
categories: ChecklistCategories {
safety_ethics: vec![make_item("A1", true, false)],
code_quality: vec![make_item("B1", false, true)],
testing: vec![make_item("C1", true, true)],
documentation: vec![make_item("D1", false, false)],
process: vec![make_item("E1", true, false)],
},
}
}
#[test]
fn test_format_checklist_text_emits_header_with_task_id_and_type() {
let cl = make_checklist_with_all_categories();
let out = format_checklist_text(&cl);
assert!(out.contains("# QA Checklist for PMAT-999"));
assert!(out.contains("Task Type: Feature"));
assert!(out.contains("Generated:"));
}
#[test]
fn test_format_checklist_text_emits_section_per_category() {
let cl = make_checklist_with_all_categories();
let out = format_checklist_text(&cl);
assert!(out.contains("## Safety & Ethics"));
assert!(out.contains("## Code Quality"));
assert!(out.contains("## Testing"));
assert!(out.contains("## Documentation"));
assert!(out.contains("## Process"));
}
#[test]
fn test_format_checklist_text_renders_checked_vs_unchecked_boxes() {
let cl = make_checklist_with_all_categories();
let out = format_checklist_text(&cl);
assert!(out.contains("[x] A1:"));
assert!(out.contains("[ ] B1:"));
}
#[test]
fn test_format_checklist_text_marks_automated_items_with_auto_suffix() {
let cl = make_checklist_with_all_categories();
let out = format_checklist_text(&cl);
assert!(out.contains("B1: B1-desc (auto)"));
assert!(out.contains("A1: A1-desc\n"));
}
#[test]
fn test_format_checklist_text_with_empty_categories_emits_section_headers_only() {
let cl = QaChecklist {
task_id: "EMPTY".to_string(),
task_type: "T".to_string(),
generated: Utc::now(),
categories: ChecklistCategories {
safety_ethics: vec![],
code_quality: vec![],
testing: vec![],
documentation: vec![],
process: vec![],
},
};
let out = format_checklist_text(&cl);
assert!(out.contains("## Safety & Ethics"));
assert!(out.contains("## Process"));
assert!(!out.contains("[x]"));
assert!(!out.contains("[ ]"));
}
#[test]
fn test_deserialize_bool_lenient_native_bool_true() {
let json =
r#"{"id":"X","description":"d","checked":true,"automated":false,"evidence":null}"#;
let item: ChecklistItem = serde_json::from_str(json).unwrap();
assert!(item.checked);
assert!(!item.automated);
}
#[test]
fn test_deserialize_bool_lenient_string_true() {
let json =
r#"{"id":"X","description":"d","checked":"true","automated":"false","evidence":null}"#;
let item: ChecklistItem = serde_json::from_str(json).unwrap();
assert!(item.checked);
assert!(!item.automated);
}
#[test]
fn test_deserialize_bool_lenient_string_yes_no_aliases() {
let json =
r#"{"id":"X","description":"d","checked":"yes","automated":"no","evidence":null}"#;
let item: ChecklistItem = serde_json::from_str(json).unwrap();
assert!(item.checked);
assert!(!item.automated);
}
#[test]
fn test_deserialize_bool_lenient_string_one_zero_aliases() {
let json = r#"{"id":"X","description":"d","checked":"1","automated":"0","evidence":null}"#;
let item: ChecklistItem = serde_json::from_str(json).unwrap();
assert!(item.checked);
assert!(!item.automated);
}
#[test]
fn test_deserialize_bool_lenient_case_insensitive() {
let json =
r#"{"id":"X","description":"d","checked":"TRUE","automated":"True","evidence":null}"#;
let item: ChecklistItem = serde_json::from_str(json).unwrap();
assert!(item.checked);
assert!(item.automated);
}
#[test]
fn test_deserialize_bool_lenient_invalid_string_rejects() {
let json =
r#"{"id":"X","description":"d","checked":"maybe","automated":false,"evidence":null}"#;
let result: Result<ChecklistItem, _> = serde_json::from_str(json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("invalid bool string: maybe"), "got: {err}");
}
}
#[cfg(test)]
mod r5_pure_helpers_tests {
use super::*;
use std::os::unix::process::ExitStatusExt;
use std::process::{ExitStatus, Output};
fn ok_output(success_code: i32) -> std::io::Result<Output> {
Ok(Output {
status: ExitStatus::from_raw(success_code),
stdout: vec![],
stderr: vec![],
})
}
fn err_output() -> std::io::Result<Output> {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"not found",
))
}
#[test]
fn test_classify_command_outcome_success_is_passed() {
let result = classify_command_outcome(ok_output(0));
assert_eq!(result, ValidationStatus::Passed);
}
#[test]
fn test_classify_command_outcome_non_zero_exit_is_failed() {
let result = classify_command_outcome(ok_output(1 << 8));
assert_eq!(result, ValidationStatus::Failed);
}
#[test]
fn test_classify_command_outcome_spawn_error_is_skipped() {
let result = classify_command_outcome(err_output());
assert_eq!(result, ValidationStatus::Skipped);
}
#[test]
fn test_classify_doc_command_outcome_success_is_passed() {
let result = classify_doc_command_outcome(ok_output(0));
assert_eq!(result, ValidationStatus::Passed);
}
#[test]
fn test_classify_doc_command_outcome_non_zero_is_warning_not_failed() {
let result = classify_doc_command_outcome(ok_output(1 << 8));
assert_eq!(result, ValidationStatus::Warning);
}
#[test]
fn test_classify_doc_command_outcome_spawn_error_is_skipped() {
let result = classify_doc_command_outcome(err_output());
assert_eq!(result, ValidationStatus::Skipped);
}
#[test]
fn test_classify_git_log_for_task_id_present() {
let log = "abc123 fix: PMAT-100 something\ndef456 chore: cleanup";
assert_eq!(
classify_git_log_for_task(log, "PMAT-100"),
ValidationStatus::Passed
);
}
#[test]
fn test_classify_git_log_for_task_hashtag_present() {
let log = "abc123 fix: #42 issue\n";
assert_eq!(
classify_git_log_for_task(log, "42"),
ValidationStatus::Passed
);
}
#[test]
fn test_classify_git_log_for_task_absent_is_warning() {
let log = "abc123 chore: cleanup\ndef456 docs: typo";
assert_eq!(
classify_git_log_for_task(log, "PMAT-100"),
ValidationStatus::Warning
);
}
#[test]
fn test_classify_git_log_for_task_empty_log_is_warning() {
assert_eq!(
classify_git_log_for_task("", "PMAT-100"),
ValidationStatus::Warning
);
}
#[test]
fn test_classify_changelog_for_task_id_present() {
let cl = "## v3.16.0 - 2026-04-25\n- PMAT-100 added thing\n";
assert_eq!(
classify_changelog_for_task(cl, "PMAT-100"),
ValidationStatus::Passed
);
}
#[test]
fn test_classify_changelog_for_task_unreleased_header_passes() {
let cl = "## Unreleased\n- some change\n";
assert_eq!(
classify_changelog_for_task(cl, "PMAT-999"),
ValidationStatus::Passed
);
}
#[test]
fn test_classify_changelog_for_task_absent_is_warning() {
let cl = "## v3.15.0\n- old changes only\n";
assert_eq!(
classify_changelog_for_task(cl, "PMAT-100"),
ValidationStatus::Warning
);
}
fn cat(passed: u32, total: u32) -> CategoryResult {
CategoryResult {
name: "test".into(),
passed,
total,
items: vec![],
}
}
#[test]
fn test_calculate_overall_score_empty_categories() {
let map: HashMap<String, CategoryResult> = HashMap::new();
assert_eq!(calculate_overall_score(&map), 0.0);
}
#[test]
fn test_calculate_overall_score_all_passing() {
let mut map = HashMap::new();
map.insert("a".into(), cat(5, 5));
map.insert("b".into(), cat(3, 3));
assert_eq!(calculate_overall_score(&map), 100.0);
}
#[test]
fn test_calculate_overall_score_partial() {
let mut map = HashMap::new();
map.insert("a".into(), cat(2, 5));
map.insert("b".into(), cat(3, 5));
assert_eq!(calculate_overall_score(&map), 50.0);
}
#[test]
fn test_calculate_overall_score_zero_total_no_div_by_zero() {
let mut map = HashMap::new();
map.insert("a".into(), cat(0, 0));
assert_eq!(calculate_overall_score(&map), 0.0);
}
#[test]
fn test_determine_pass_strict_false_above_80() {
assert!(determine_pass(81.0, false));
assert!(determine_pass(80.0, false));
}
#[test]
fn test_determine_pass_strict_false_below_80_fails() {
assert!(!determine_pass(79.9, false));
}
#[test]
fn test_determine_pass_strict_true_below_80_fails() {
assert!(!determine_pass(79.0, true));
}
#[test]
fn test_determine_pass_strict_true_at_or_above_95_passes() {
assert!(determine_pass(95.0, true));
assert!(determine_pass(99.5, true));
}
#[test]
fn test_determine_pass_operator_precedence_pin() {
assert!(!determine_pass(81.0, true));
assert!(!determine_pass(94.9, true));
}
}
#[cfg(test)]
mod impl_spec_tests {
use super::*;
use chrono::Utc;
use serde_json::json;
use tempfile::TempDir;
fn write(p: &Path, content: &str) {
if let Some(parent) = p.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(p, content).unwrap();
}
fn make_item(checked: bool) -> ChecklistItem {
ChecklistItem {
id: "x".to_string(),
description: "d".to_string(),
checked,
automated: false,
evidence: None,
}
}
fn write_checklist(dir: &Path, all_checked: bool, partial: bool) {
let cats = if partial {
ChecklistCategories {
safety_ethics: vec![make_item(true)],
code_quality: vec![make_item(false)],
testing: vec![],
documentation: vec![],
process: vec![],
}
} else {
ChecklistCategories {
safety_ethics: vec![make_item(all_checked)],
code_quality: vec![],
testing: vec![],
documentation: vec![],
process: vec![],
}
};
let cl = QaChecklist {
task_id: "T-1".to_string(),
task_type: "feat".to_string(),
generated: Utc::now(),
categories: cats,
};
let yaml = serde_yaml_ng::to_string(&cl).unwrap();
write(&dir.join("checklist.yaml"), &yaml);
}
#[test]
fn test_print_task_status_no_checklist_no_panic() {
let tmp = TempDir::new().unwrap();
print_task_status("T-1", tmp.path()).expect("ok");
}
#[test]
fn test_print_task_status_complete_no_panic() {
let tmp = TempDir::new().unwrap();
write_checklist(tmp.path(), true, false);
print_task_status("T-1", tmp.path()).expect("ok");
}
#[test]
fn test_print_task_status_in_progress_no_panic() {
let tmp = TempDir::new().unwrap();
write_checklist(tmp.path(), false, true); print_task_status("T-1", tmp.path()).expect("ok");
}
#[test]
fn test_print_task_status_pending_no_panic() {
let tmp = TempDir::new().unwrap();
write_checklist(tmp.path(), false, false); print_task_status("T-1", tmp.path()).expect("ok");
}
#[test]
fn test_resolve_spec_path_direct_md_file() {
let tmp = TempDir::new().unwrap();
let direct = tmp.path().join("note.md");
write(&direct, "# spec");
let resolved = resolve_spec_path(&direct.display().to_string(), tmp.path()).unwrap();
assert_eq!(resolved, direct);
}
#[test]
fn test_resolve_spec_path_direct_non_md_falls_through() {
let tmp = TempDir::new().unwrap();
let nm = tmp.path().join("nm.txt");
write(&nm, "");
let resolved = resolve_spec_path(&nm.display().to_string(), tmp.path()).unwrap();
assert!(resolved.exists());
}
#[test]
fn test_resolve_spec_path_project_relative_existing() {
let tmp = TempDir::new().unwrap();
write(&tmp.path().join("subdir/foo.md"), "# spec");
let resolved = resolve_spec_path("subdir/foo.md", tmp.path()).unwrap();
assert!(resolved.ends_with("subdir/foo.md"));
}
#[test]
fn test_resolve_spec_path_specs_dir_exact_match() {
let tmp = TempDir::new().unwrap();
write(&tmp.path().join("docs/specifications/myspec.md"), "# spec");
let resolved = resolve_spec_path("myspec", tmp.path()).unwrap();
assert!(resolved.ends_with("myspec.md"));
}
#[test]
fn test_resolve_spec_path_hyphen_normalization() {
let tmp = TempDir::new().unwrap();
write(&tmp.path().join("docs/specifications/my-spec.md"), "");
let resolved = resolve_spec_path("my_spec", tmp.path()).unwrap();
assert!(resolved.ends_with("my-spec.md"));
}
#[test]
fn test_resolve_spec_path_partial_match_via_substring() {
let tmp = TempDir::new().unwrap();
write(
&tmp.path().join("docs/specifications/very-long-name.md"),
"",
);
let resolved = resolve_spec_path("long", tmp.path()).unwrap();
assert!(resolved.ends_with("very-long-name.md"));
}
#[test]
fn test_resolve_spec_path_gh_prefix() {
let tmp = TempDir::new().unwrap();
write(&tmp.path().join("docs/specifications/gh-42.md"), "");
let resolved = resolve_spec_path("GH-42", tmp.path()).unwrap();
assert!(resolved.ends_with("gh-42.md"));
}
#[test]
fn test_resolve_spec_path_hash_prefix() {
let tmp = TempDir::new().unwrap();
write(&tmp.path().join("docs/specifications/gh-99.md"), "");
let resolved = resolve_spec_path("#99", tmp.path()).unwrap();
assert!(resolved.ends_with("gh-99.md"));
}
#[test]
fn test_resolve_spec_path_not_found_err() {
let tmp = TempDir::new().unwrap();
let err = resolve_spec_path("nonexistent", tmp.path())
.unwrap_err()
.to_string();
assert!(err.contains("Specification not found"));
assert!(err.contains("nonexistent"));
}
#[test]
fn test_format_spec_result_markdown_passed_includes_passed_status() {
let v = json!({
"spec_path": "docs/x.md",
"title": "Test Spec",
"issue_refs": ["GH-1"],
"claims_total": 3,
"gateway_score": 20.0,
"gateway_passed": true,
"total_score": 85.0,
"threshold": 60,
"passed": true,
});
let md = format_spec_result_markdown(&v);
assert!(md.contains("# Specification Validation Report"));
assert!(md.contains("docs/x.md"));
assert!(md.contains("Test Spec"));
assert!(md.contains("Gateway (Falsifiability)**: 20.0/25 - PASSED"));
assert!(md.contains("Status**: PASSED"));
assert!(md.contains("| Falsifiability | 20.0/25 | ✓ |"));
}
#[test]
fn test_format_spec_result_markdown_failed_uses_failed_and_x_marker() {
let v = json!({
"spec_path": "docs/y.md",
"title": "Failing Spec",
"issue_refs": [],
"claims_total": 1,
"gateway_score": 10.0,
"gateway_passed": false,
"total_score": 30.0,
"threshold": 60,
"passed": false,
});
let md = format_spec_result_markdown(&v);
assert!(md.contains("- FAILED"));
assert!(md.contains("Status**: FAILED"));
assert!(md.contains("| Falsifiability | 10.0/25 | ✗ |"));
}
#[test]
fn test_format_spec_result_markdown_missing_fields_use_defaults() {
let v = json!({});
let md = format_spec_result_markdown(&v);
assert!(md.contains("Specification**: unknown"));
assert!(md.contains("Title**: unknown"));
assert!(md.contains("0.0/25"));
assert!(md.contains("Status**: FAILED"));
}
}
#[cfg(test)]
mod checklist_gen_tests {
use super::*;
#[test]
fn test_generate_checklist_feature_task_type_string() {
let cl = generate_checklist("PMAT-100", QaTaskType::Feature);
assert_eq!(cl.task_id, "PMAT-100");
assert_eq!(cl.task_type, "feature");
}
#[test]
fn test_generate_checklist_bugfix_task_type_string() {
let cl = generate_checklist("X", QaTaskType::Bugfix);
assert_eq!(cl.task_type, "bugfix");
}
#[test]
fn test_generate_checklist_refactor_task_type_string() {
let cl = generate_checklist("X", QaTaskType::Refactor);
assert_eq!(cl.task_type, "refactor");
}
#[test]
fn test_generate_checklist_docs_task_type_string() {
let cl = generate_checklist("X", QaTaskType::Docs);
assert_eq!(cl.task_type, "docs");
}
#[test]
fn test_generate_checklist_performance_task_type_string() {
let cl = generate_checklist("X", QaTaskType::Performance);
assert_eq!(cl.task_type, "performance");
}
#[test]
fn test_generate_checklist_security_task_type_string() {
let cl = generate_checklist("X", QaTaskType::Security);
assert_eq!(cl.task_type, "security");
}
#[test]
fn test_generate_checklist_safety_ethics_has_5_items() {
let cl = generate_checklist("X", QaTaskType::Feature);
assert_eq!(cl.categories.safety_ethics.len(), 5);
let ids: Vec<&str> = cl
.categories
.safety_ethics
.iter()
.map(|i| i.id.as_str())
.collect();
assert_eq!(ids, vec!["A1", "A2", "A3", "A4", "A5"]);
}
#[test]
fn test_generate_checklist_code_quality_has_at_least_5_items() {
let cl = generate_checklist("X", QaTaskType::Feature);
assert!(cl.categories.code_quality.len() >= 5);
for (i, item) in cl.categories.code_quality.iter().enumerate().take(5) {
assert_eq!(item.id, format!("B{}", i + 1));
}
}
#[test]
fn test_generate_checklist_default_state_is_unchecked() {
let cl = generate_checklist("X", QaTaskType::Feature);
for cat in [
&cl.categories.safety_ethics,
&cl.categories.code_quality,
&cl.categories.testing,
&cl.categories.documentation,
&cl.categories.process,
] {
for item in cat {
assert!(
!item.checked,
"item {} should default to unchecked",
item.id
);
}
}
}
#[test]
fn test_generate_checklist_all_categories_populated() {
let cl = generate_checklist("X", QaTaskType::Feature);
assert!(!cl.categories.safety_ethics.is_empty());
assert!(!cl.categories.code_quality.is_empty());
assert!(!cl.categories.testing.is_empty());
assert!(!cl.categories.documentation.is_empty());
assert!(!cl.categories.process.is_empty());
}
#[test]
fn test_generate_checklist_task_type_string_does_not_vary_by_task_id() {
let cl1 = generate_checklist("PMAT-1", QaTaskType::Bugfix);
let cl2 = generate_checklist("PMAT-2", QaTaskType::Bugfix);
assert_eq!(cl1.task_type, cl2.task_type);
}
#[test]
fn test_generate_checklist_yaml_serializes_round_trip() {
let cl = generate_checklist("PMAT-42", QaTaskType::Feature);
let yaml = serde_yaml_ng::to_string(&cl).unwrap();
assert!(yaml.contains("task_id: PMAT-42"));
assert!(yaml.contains("task_type: feature"));
let parsed: QaChecklist = serde_yaml_ng::from_str(&yaml).unwrap();
assert_eq!(parsed.task_id, "PMAT-42");
assert_eq!(parsed.task_type, "feature");
assert_eq!(
parsed.categories.safety_ethics.len(),
cl.categories.safety_ethics.len()
);
}
}
#[cfg(test)]
mod epic_helpers_tests {
use super::*;
#[test]
fn test_calculate_epic_summary_empty_tasks_pending() {
let summary = calculate_epic_summary("EPIC-1", &[]);
assert_eq!(summary.epic_id, "EPIC-1");
assert_eq!(summary.total_tasks, 0);
assert_eq!(summary.total_checks, 0);
assert_eq!(summary.passed_checks, 0);
assert_eq!(summary.overall_score, 0.0);
assert_eq!(summary.status, EpicStatus::Pending);
assert!(summary.task_scores.is_empty());
}
#[test]
fn test_calculate_epic_summary_all_complete_returns_complete() {
let tasks = vec![
("T1".to_string(), 25u32, 25u32),
("T2".to_string(), 20u32, 20u32),
];
let summary = calculate_epic_summary("EPIC", &tasks);
assert_eq!(summary.status, EpicStatus::Complete);
assert_eq!(summary.total_checks, 45);
assert_eq!(summary.passed_checks, 45);
assert!((summary.overall_score - 100.0).abs() < 1e-9);
}
#[test]
fn test_calculate_epic_summary_partial_progress_returns_in_progress() {
let tasks = vec![
("T1".to_string(), 10u32, 25u32),
("T2".to_string(), 0u32, 25u32),
];
let summary = calculate_epic_summary("EPIC", &tasks);
assert_eq!(summary.status, EpicStatus::InProgress);
assert_eq!(summary.passed_checks, 10);
assert_eq!(summary.total_checks, 50);
assert!((summary.overall_score - 20.0).abs() < 1e-9);
}
#[test]
fn test_calculate_epic_summary_all_zero_passed_pending() {
let tasks = vec![
("T1".to_string(), 0u32, 25u32),
("T2".to_string(), 0u32, 25u32),
];
let summary = calculate_epic_summary("EPIC", &tasks);
assert_eq!(summary.status, EpicStatus::Pending);
assert_eq!(summary.overall_score, 0.0);
}
#[test]
fn test_calculate_epic_summary_zero_total_checks_score_zero() {
let tasks = vec![("T1".to_string(), 0u32, 0u32)];
let summary = calculate_epic_summary("EPIC", &tasks);
assert_eq!(summary.overall_score, 0.0);
}
#[test]
fn test_calculate_epic_summary_individual_task_scores_computed() {
let tasks = vec![
("T1".to_string(), 10u32, 20u32),
("T2".to_string(), 15u32, 30u32),
("T3".to_string(), 0u32, 0u32), ];
let summary = calculate_epic_summary("EPIC", &tasks);
assert_eq!(summary.task_scores.len(), 3);
assert_eq!(summary.task_scores[0].0, "T1");
assert!((summary.task_scores[0].1 - 50.0).abs() < 1e-9);
assert!((summary.task_scores[1].1 - 50.0).abs() < 1e-9);
assert_eq!(summary.task_scores[2].1, 0.0);
}
#[test]
fn test_calculate_epic_summary_epic_id_passthrough() {
let summary = calculate_epic_summary("MyEpic-2026-Q2", &[]);
assert_eq!(summary.epic_id, "MyEpic-2026-Q2");
}
#[test]
fn test_generate_example_scripts_returns_at_least_one() {
let scripts = generate_example_scripts("TASK-1", "my_feature");
assert!(!scripts.is_empty());
}
#[test]
fn test_generate_example_scripts_includes_task_id_or_feature_name() {
let scripts = generate_example_scripts("TASK-42", "feature_x");
let combined = scripts
.iter()
.map(|s| format!("{}\n{}\n{}", s.name, s.description, s.content))
.collect::<Vec<_>>()
.join("\n");
assert!(combined.contains("TASK-42") || combined.contains("feature_x"));
}
#[test]
fn test_generate_example_scripts_each_has_non_empty_name_and_content() {
let scripts = generate_example_scripts("T", "f");
for s in &scripts {
assert!(!s.name.is_empty(), "script name should not be empty");
assert!(!s.content.is_empty(), "script content should not be empty");
}
}
}
#[cfg(test)]
mod print_validation_tests {
use super::*;
fn make_validation_item(id: &str, status: ValidationStatus) -> ValidationItem {
ValidationItem {
id: id.to_string(),
description: format!("{}-desc", id),
status,
value: None,
threshold: None,
evidence: None,
}
}
fn make_category(
name: &str,
passed: u32,
total: u32,
items: Vec<ValidationItem>,
) -> CategoryResult {
CategoryResult {
name: name.to_string(),
passed,
total,
items,
}
}
fn make_result(passed: bool, manual_checks: Vec<String>) -> QaValidationResult {
let mut categories = HashMap::new();
categories.insert(
"all_pass".to_string(),
make_category(
"All-Pass-Cat",
2,
2,
vec![
make_validation_item("A1", ValidationStatus::Passed),
make_validation_item("A2", ValidationStatus::Passed),
],
),
);
categories.insert(
"mixed".to_string(),
make_category(
"Mixed-Cat",
1,
4,
vec![
make_validation_item("M1", ValidationStatus::Passed),
make_validation_item("M2", ValidationStatus::Failed),
make_validation_item("M3", ValidationStatus::Warning),
make_validation_item("M4", ValidationStatus::Skipped),
],
),
);
categories.insert(
"manual".to_string(),
make_category(
"Manual-Cat",
0,
1,
vec![make_validation_item("X1", ValidationStatus::Manual)],
),
);
QaValidationResult {
task_id: "PMAT-100".to_string(),
timestamp: Utc::now(),
categories,
overall_score: 50.0,
passed,
manual_checks_required: manual_checks,
}
}
#[test]
fn test_print_validation_text_passed_no_panic() {
let result = make_result(true, vec![]);
print_validation_text(&result);
}
#[test]
fn test_print_validation_text_failed_with_manual_checks_no_panic() {
let result = make_result(false, vec!["check 1".into(), "check 2".into()]);
print_validation_text(&result);
}
#[test]
fn test_print_validation_text_zero_passed_zero_total_no_panic() {
let mut categories = HashMap::new();
categories.insert(
"empty".to_string(),
make_category("Empty-Cat", 0, 0, vec![]),
);
let result = QaValidationResult {
task_id: "X".to_string(),
timestamp: Utc::now(),
categories,
overall_score: 0.0,
passed: false,
manual_checks_required: vec![],
};
print_validation_text(&result);
}
#[test]
fn test_print_validation_markdown_no_panic() {
let result = make_result(true, vec![]);
print_validation_markdown(&result);
}
#[test]
fn test_print_validation_markdown_with_manual_checks_emits_section() {
let result = make_result(false, vec!["item to verify".into()]);
print_validation_markdown(&result);
}
}