1use crate::explanation::ExplanationChain;
13use crate::impact::ImpactReport;
14use serde::{Deserialize, Serialize};
15use ucm_graph_core::edge::ConfidenceTier;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct TestIntent {
20 pub high_confidence: Vec<TestScenario>,
22 pub medium_confidence: Vec<TestScenario>,
24 pub low_confidence: Vec<TestScenario>,
26 pub risks: Vec<Risk>,
28 pub coverage_gaps: Vec<CoverageGap>,
30 pub decided_not_to_test: Vec<SkippedEntity>,
32 pub summary: TestIntentSummary,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct TestScenario {
39 pub description: String,
41 pub rationale: String,
43 pub confidence: f64,
45 pub related_entity: String,
47 pub explanation_chain: ExplanationChain,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct Risk {
54 pub severity: RiskSeverity,
55 pub description: String,
56 pub mitigation: String,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub enum RiskSeverity {
61 High,
62 Medium,
63 Low,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SkippedEntity {
69 pub entity: String,
71 pub reason: String,
73 pub confidence_of_safety: f64,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct CoverageGap {
80 pub entity: String,
81 pub description: String,
82 pub recommendation: String,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct TestIntentSummary {
87 pub total_scenarios: usize,
88 pub high_count: usize,
89 pub medium_count: usize,
90 pub low_count: usize,
91 pub risk_count: usize,
92}
93
94pub fn generate_test_intent(report: &ImpactReport) -> TestIntent {
99 let mut high = Vec::new();
100 let mut medium = Vec::new();
101 let mut low = Vec::new();
102 let mut risks = Vec::new();
103 let mut coverage_gaps = Vec::new();
104
105 for impact in &report.direct_impacts {
107 let tier = ConfidenceTier::from_score(impact.confidence);
108
109 let scenario = TestScenario {
110 description: format!(
111 "Verify {} still functions correctly after change",
112 impact.name
113 ),
114 rationale: impact.reason.clone(),
115 confidence: impact.confidence,
116 related_entity: impact.name.clone(),
117 explanation_chain: impact.explanation_chain.clone(),
118 };
119
120 match tier {
121 ConfidenceTier::High => high.push(scenario),
122 ConfidenceTier::Medium => medium.push(scenario),
123 ConfidenceTier::Low => low.push(scenario),
124 }
125
126 risks.push(Risk {
128 severity: RiskSeverity::High,
129 description: format!(
130 "{} directly depends on changed code — regression risk if behavior changes",
131 impact.name
132 ),
133 mitigation: format!(
134 "Run existing tests for {} and verify expected behavior",
135 impact.name
136 ),
137 });
138
139 high.push(TestScenario {
141 description: format!(
142 "Verify {} properly handles error cases after change",
143 impact.name
144 ),
145 rationale: "Direct dependency means error handling paths may also be affected".into(),
146 confidence: impact.confidence * 0.9,
147 related_entity: impact.name.clone(),
148 explanation_chain: {
149 let mut chain =
150 ExplanationChain::new(format!("Error handling test for {}", impact.name));
151 chain.add_step(
152 "Direct dependency on changed code",
153 "Error paths may also be affected by the change",
154 impact.confidence * 0.9,
155 );
156 chain
157 },
158 });
159 }
160
161 for impact in &report.indirect_impacts {
163 let tier = ConfidenceTier::from_score(impact.confidence);
164
165 let scenario = TestScenario {
166 description: format!(
167 "Verify {} end-to-end flow still works (transitive dependency via {} hops)",
168 impact.name, impact.depth
169 ),
170 rationale: impact.reason.clone(),
171 confidence: impact.confidence,
172 related_entity: impact.name.clone(),
173 explanation_chain: impact.explanation_chain.clone(),
174 };
175
176 match tier {
177 ConfidenceTier::High => high.push(scenario),
178 ConfidenceTier::Medium => medium.push(scenario),
179 ConfidenceTier::Low => low.push(scenario),
180 }
181
182 if impact.confidence > 0.7 {
184 risks.push(Risk {
185 severity: RiskSeverity::Medium,
186 description: format!(
187 "{} is indirectly affected via {}-hop chain with {:.0}% confidence",
188 impact.name,
189 impact.depth,
190 impact.confidence * 100.0
191 ),
192 mitigation: format!(
193 "Integration test covering the path: {}",
194 impact.path.join(" → ")
195 ),
196 });
197 }
198 }
199
200 for impact in report
202 .direct_impacts
203 .iter()
204 .chain(report.indirect_impacts.iter())
205 {
206 coverage_gaps.push(CoverageGap {
208 entity: impact.name.clone(),
209 description: format!(
210 "{} is impacted but has no linked test coverage in the graph",
211 impact.name
212 ),
213 recommendation: format!(
214 "Add test coverage for {} focusing on the changed behavior",
215 impact.name
216 ),
217 });
218 }
219
220 let decided_not_to_test: Vec<SkippedEntity> = report
222 .not_impacted
223 .iter()
224 .map(|entry| SkippedEntity {
225 entity: entry.name.clone(),
226 reason: entry.reason.clone(),
227 confidence_of_safety: entry.confidence,
228 })
229 .collect();
230
231 for ambiguity in &report.ambiguities {
233 risks.push(Risk {
234 severity: RiskSeverity::High,
235 description: format!("Ambiguity: {}", ambiguity.description),
236 mitigation: ambiguity.recommendation.clone(),
237 });
238 }
239
240 let summary = TestIntentSummary {
241 total_scenarios: high.len() + medium.len() + low.len(),
242 high_count: high.len(),
243 medium_count: medium.len(),
244 low_count: low.len(),
245 risk_count: risks.len(),
246 };
247
248 TestIntent {
249 high_confidence: high,
250 medium_confidence: medium,
251 low_confidence: low,
252 risks,
253 coverage_gaps,
254 decided_not_to_test,
255 summary,
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::impact::analyze_impact;
263 use ucm_graph_core::edge::*;
264 use ucm_graph_core::entity::*;
265 use ucm_graph_core::graph::UcmGraph;
266
267 #[test]
268 fn test_generate_test_intent() {
269 let mut graph = UcmGraph::new();
270
271 for (file, symbol, name) in &[
273 ("src/auth/service.ts", "validateToken", "validateToken"),
274 ("src/api/middleware.ts", "authMiddleware", "authMiddleware"),
275 (
276 "src/payments/checkout.ts",
277 "processPayment",
278 "processPayment",
279 ),
280 ] {
281 graph
282 .add_entity(UcmEntity::new(
283 EntityId::local(file, symbol),
284 EntityKind::Function {
285 is_async: true,
286 parameter_count: 1,
287 return_type: None,
288 },
289 *name,
290 *file,
291 "typescript",
292 DiscoverySource::StaticAnalysis,
293 ))
294 .unwrap();
295 }
296
297 graph
298 .add_relationship(
299 &EntityId::local("src/api/middleware.ts", "authMiddleware"),
300 &EntityId::local("src/auth/service.ts", "validateToken"),
301 UcmEdge::new(
302 RelationType::Imports,
303 DiscoverySource::StaticAnalysis,
304 0.95,
305 "imports",
306 ),
307 )
308 .unwrap();
309
310 graph
311 .add_relationship(
312 &EntityId::local("src/payments/checkout.ts", "processPayment"),
313 &EntityId::local("src/api/middleware.ts", "authMiddleware"),
314 UcmEdge::new(
315 RelationType::DependsOn,
316 DiscoverySource::StaticAnalysis,
317 0.80,
318 "depends",
319 ),
320 )
321 .unwrap();
322
323 let changed = vec![EntityId::local("src/auth/service.ts", "validateToken")];
324 let report = analyze_impact(&graph, &changed, 0.1, 10);
325 let intent = generate_test_intent(&report);
326
327 assert!(intent.summary.total_scenarios > 0);
329
330 assert!(!intent.risks.is_empty());
332
333 assert!(!intent.high_confidence.is_empty());
335
336 for scenario in &intent.high_confidence {
338 assert!(!scenario.explanation_chain.steps.is_empty());
339 }
340
341 let json = serde_json::to_string_pretty(&intent).unwrap();
343 assert!(json.contains("explanation_chain"));
344 assert!(json.contains("high_confidence"));
345 assert!(json.contains("decided_not_to_test"));
346 }
347}