use crate::explanation::ExplanationChain;
use crate::impact::ImpactReport;
use serde::{Deserialize, Serialize};
use ucm_graph_core::edge::ConfidenceTier;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestIntent {
pub high_confidence: Vec<TestScenario>,
pub medium_confidence: Vec<TestScenario>,
pub low_confidence: Vec<TestScenario>,
pub risks: Vec<Risk>,
pub coverage_gaps: Vec<CoverageGap>,
pub decided_not_to_test: Vec<SkippedEntity>,
pub summary: TestIntentSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestScenario {
pub description: String,
pub rationale: String,
pub confidence: f64,
pub related_entity: String,
pub explanation_chain: ExplanationChain,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Risk {
pub severity: RiskSeverity,
pub description: String,
pub mitigation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RiskSeverity {
High,
Medium,
Low,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SkippedEntity {
pub entity: String,
pub reason: String,
pub confidence_of_safety: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoverageGap {
pub entity: String,
pub description: String,
pub recommendation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TestIntentSummary {
pub total_scenarios: usize,
pub high_count: usize,
pub medium_count: usize,
pub low_count: usize,
pub risk_count: usize,
}
pub fn generate_test_intent(report: &ImpactReport) -> TestIntent {
let mut high = Vec::new();
let mut medium = Vec::new();
let mut low = Vec::new();
let mut risks = Vec::new();
let mut coverage_gaps = Vec::new();
for impact in &report.direct_impacts {
let tier = ConfidenceTier::from_score(impact.confidence);
let scenario = TestScenario {
description: format!(
"Verify {} still functions correctly after change",
impact.name
),
rationale: impact.reason.clone(),
confidence: impact.confidence,
related_entity: impact.name.clone(),
explanation_chain: impact.explanation_chain.clone(),
};
match tier {
ConfidenceTier::High => high.push(scenario),
ConfidenceTier::Medium => medium.push(scenario),
ConfidenceTier::Low => low.push(scenario),
}
risks.push(Risk {
severity: RiskSeverity::High,
description: format!(
"{} directly depends on changed code — regression risk if behavior changes",
impact.name
),
mitigation: format!(
"Run existing tests for {} and verify expected behavior",
impact.name
),
});
high.push(TestScenario {
description: format!(
"Verify {} properly handles error cases after change",
impact.name
),
rationale: "Direct dependency means error handling paths may also be affected".into(),
confidence: impact.confidence * 0.9,
related_entity: impact.name.clone(),
explanation_chain: {
let mut chain =
ExplanationChain::new(format!("Error handling test for {}", impact.name));
chain.add_step(
"Direct dependency on changed code",
"Error paths may also be affected by the change",
impact.confidence * 0.9,
);
chain
},
});
}
for impact in &report.indirect_impacts {
let tier = ConfidenceTier::from_score(impact.confidence);
let scenario = TestScenario {
description: format!(
"Verify {} end-to-end flow still works (transitive dependency via {} hops)",
impact.name, impact.depth
),
rationale: impact.reason.clone(),
confidence: impact.confidence,
related_entity: impact.name.clone(),
explanation_chain: impact.explanation_chain.clone(),
};
match tier {
ConfidenceTier::High => high.push(scenario),
ConfidenceTier::Medium => medium.push(scenario),
ConfidenceTier::Low => low.push(scenario),
}
if impact.confidence > 0.7 {
risks.push(Risk {
severity: RiskSeverity::Medium,
description: format!(
"{} is indirectly affected via {}-hop chain with {:.0}% confidence",
impact.name,
impact.depth,
impact.confidence * 100.0
),
mitigation: format!(
"Integration test covering the path: {}",
impact.path.join(" → ")
),
});
}
}
for impact in report
.direct_impacts
.iter()
.chain(report.indirect_impacts.iter())
{
coverage_gaps.push(CoverageGap {
entity: impact.name.clone(),
description: format!(
"{} is impacted but has no linked test coverage in the graph",
impact.name
),
recommendation: format!(
"Add test coverage for {} focusing on the changed behavior",
impact.name
),
});
}
let decided_not_to_test: Vec<SkippedEntity> = report
.not_impacted
.iter()
.map(|entry| SkippedEntity {
entity: entry.name.clone(),
reason: entry.reason.clone(),
confidence_of_safety: entry.confidence,
})
.collect();
for ambiguity in &report.ambiguities {
risks.push(Risk {
severity: RiskSeverity::High,
description: format!("Ambiguity: {}", ambiguity.description),
mitigation: ambiguity.recommendation.clone(),
});
}
let summary = TestIntentSummary {
total_scenarios: high.len() + medium.len() + low.len(),
high_count: high.len(),
medium_count: medium.len(),
low_count: low.len(),
risk_count: risks.len(),
};
TestIntent {
high_confidence: high,
medium_confidence: medium,
low_confidence: low,
risks,
coverage_gaps,
decided_not_to_test,
summary,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::impact::analyze_impact;
use ucm_graph_core::edge::*;
use ucm_graph_core::entity::*;
use ucm_graph_core::graph::UcmGraph;
#[test]
fn test_generate_test_intent() {
let mut graph = UcmGraph::new();
for (file, symbol, name) in &[
("src/auth/service.ts", "validateToken", "validateToken"),
("src/api/middleware.ts", "authMiddleware", "authMiddleware"),
(
"src/payments/checkout.ts",
"processPayment",
"processPayment",
),
] {
graph
.add_entity(UcmEntity::new(
EntityId::local(file, symbol),
EntityKind::Function {
is_async: true,
parameter_count: 1,
return_type: None,
},
*name,
*file,
"typescript",
DiscoverySource::StaticAnalysis,
))
.unwrap();
}
graph
.add_relationship(
&EntityId::local("src/api/middleware.ts", "authMiddleware"),
&EntityId::local("src/auth/service.ts", "validateToken"),
UcmEdge::new(
RelationType::Imports,
DiscoverySource::StaticAnalysis,
0.95,
"imports",
),
)
.unwrap();
graph
.add_relationship(
&EntityId::local("src/payments/checkout.ts", "processPayment"),
&EntityId::local("src/api/middleware.ts", "authMiddleware"),
UcmEdge::new(
RelationType::DependsOn,
DiscoverySource::StaticAnalysis,
0.80,
"depends",
),
)
.unwrap();
let changed = vec![EntityId::local("src/auth/service.ts", "validateToken")];
let report = analyze_impact(&graph, &changed, 0.1, 10);
let intent = generate_test_intent(&report);
assert!(intent.summary.total_scenarios > 0);
assert!(!intent.risks.is_empty());
assert!(!intent.high_confidence.is_empty());
for scenario in &intent.high_confidence {
assert!(!scenario.explanation_chain.steps.is_empty());
}
let json = serde_json::to_string_pretty(&intent).unwrap();
assert!(json.contains("explanation_chain"));
assert!(json.contains("high_confidence"));
assert!(json.contains("decided_not_to_test"));
}
}