Skip to main content

fallow_output/
pr_status.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{PrDecisionConclusion, PrDecisionSurface};
4
5/// One commit-status context derived from a [`PrDecisionSurface`].
6#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub struct PrStatusContext {
8    /// Status context name as shown in the provider UI: `Fallow` for the
9    /// aggregate context, `Fallow / <gate id>` for split contexts.
10    pub name: String,
11    /// Conclusion reported for this context.
12    pub conclusion: PrDecisionConclusion,
13    /// One-line status description.
14    pub summary: String,
15}
16
17/// How a decision surface fans out into commit-status contexts.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum PrStatusMode {
20    /// One combined context for the overall conclusion.
21    Aggregate,
22    /// One context per gate.
23    Split,
24    /// The aggregate context followed by one context per gate.
25    AggregateAndSplit,
26}
27
28/// Commit-status contexts for `surface`, fanned out according to `mode`.
29pub fn pr_status_contexts_with_mode(
30    surface: &PrDecisionSurface,
31    mode: PrStatusMode,
32) -> Vec<PrStatusContext> {
33    match mode {
34        PrStatusMode::Aggregate => vec![aggregate_status_context(surface)],
35        PrStatusMode::Split => split_status_contexts(surface),
36        PrStatusMode::AggregateAndSplit => {
37            let mut contexts = vec![aggregate_status_context(surface)];
38            contexts.extend(split_status_contexts(surface));
39            contexts
40        }
41    }
42}
43
44fn split_status_contexts(surface: &PrDecisionSurface) -> Vec<PrStatusContext> {
45    surface
46        .gates
47        .iter()
48        .map(|gate| PrStatusContext {
49            name: format!("Fallow / {}", gate.id),
50            conclusion: gate.status,
51            summary: match &gate.threshold {
52                Some(threshold) => {
53                    format!("{}: {} (threshold {threshold})", gate.label, gate.observed)
54                }
55                None => format!("{}: {}", gate.label, gate.observed),
56            },
57        })
58        .collect()
59}
60
61fn aggregate_status_context(surface: &PrDecisionSurface) -> PrStatusContext {
62    let total = surface.gates.len();
63    let failing = surface
64        .gates
65        .iter()
66        .filter(|gate| gate.status == PrDecisionConclusion::Failure)
67        .count();
68    let neutral = surface
69        .gates
70        .iter()
71        .filter(|gate| gate.status == PrDecisionConclusion::Neutral)
72        .count();
73    let summary = if total == 0 {
74        "No PR gates reported.".to_owned()
75    } else if failing > 0 {
76        format!("{failing} of {total} PR gates failed.")
77    } else if neutral > 0 {
78        format!("{neutral} of {total} PR gates need review.")
79    } else {
80        format!("All {total} PR gates passed.")
81    };
82    PrStatusContext {
83        name: "Fallow".to_owned(),
84        conclusion: surface.conclusion,
85        summary,
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::{PR_DECISION_SCHEMA, PrDecisionDetails, PrDecisionGate, PrDecisionSurface};
93
94    #[test]
95    fn builds_status_contexts_from_decision_gates() {
96        let surface = PrDecisionSurface {
97            schema: PR_DECISION_SCHEMA.to_owned(),
98            title: "Fallow".to_owned(),
99            conclusion: PrDecisionConclusion::Neutral,
100            gates: vec![PrDecisionGate {
101                id: "duplication".to_owned(),
102                label: "Duplication".to_owned(),
103                status: PrDecisionConclusion::Neutral,
104                observed: "2 clone groups".to_owned(),
105                threshold: Some("configured rules".to_owned()),
106                scope: "new code".to_owned(),
107            }],
108            annotations: vec![],
109            details: PrDecisionDetails {
110                summary_markdown: "Review recommended.".to_owned(),
111                full_report_path: None,
112                details_url: None,
113            },
114        };
115
116        let contexts = pr_status_contexts_with_mode(&surface, PrStatusMode::Split);
117
118        assert_eq!(contexts.len(), 1);
119        assert_eq!(contexts[0].name, "Fallow / duplication");
120        assert_eq!(contexts[0].conclusion, PrDecisionConclusion::Neutral);
121        assert_eq!(
122            contexts[0].summary,
123            "Duplication: 2 clone groups (threshold configured rules)"
124        );
125    }
126
127    #[test]
128    fn aggregate_and_split_status_contexts_keep_one_consolidated_context() {
129        let surface = PrDecisionSurface {
130            schema: PR_DECISION_SCHEMA.to_owned(),
131            title: "Fallow".to_owned(),
132            conclusion: PrDecisionConclusion::Neutral,
133            gates: vec![PrDecisionGate {
134                id: "health".to_owned(),
135                label: "Health".to_owned(),
136                status: PrDecisionConclusion::Neutral,
137                observed: "1 finding".to_owned(),
138                threshold: None,
139                scope: "new code".to_owned(),
140            }],
141            annotations: vec![],
142            details: PrDecisionDetails {
143                summary_markdown: "Review recommended.".to_owned(),
144                full_report_path: None,
145                details_url: None,
146            },
147        };
148
149        let contexts = pr_status_contexts_with_mode(&surface, PrStatusMode::AggregateAndSplit);
150
151        assert_eq!(contexts.len(), 2);
152        assert_eq!(contexts[0].name, "Fallow");
153        assert_eq!(contexts[0].summary, "1 of 1 PR gates need review.");
154        assert_eq!(contexts[1].name, "Fallow / health");
155    }
156}