1use crate::{Check, CheckOutcome, FixProposal};
4
5#[must_use]
7pub fn certification_wording(text: &str) -> bool {
8 text.split(|c: char| !c.is_ascii_alphabetic()).any(|word| {
9 [
10 "certified",
11 "uncertified",
12 "certify",
13 "certifies",
14 "certifying",
15 "certification",
16 "certifications",
17 "certificate",
18 "certificates",
19 ]
20 .iter()
21 .any(|blocked| word.eq_ignore_ascii_case(blocked))
22 })
23}
24
25impl Check {
26 pub fn normalize_presentation(&mut self) {
28 if let Some(result) = &mut self.result
29 && matches!(result.assurance.as_str(), "certified" | "uncertified")
30 {
31 result.assurance = "standard".into();
32 }
33 let outcome = self.result.as_ref().map(|result| result.outcome);
34 let (label, sentence) = match outcome {
35 Some(CheckOutcome::Pass) => ("pass", "No problems found within this supertest."),
36 Some(CheckOutcome::Fail) => ("fail", "This supertest found a problem."),
37 Some(CheckOutcome::Conditional) => (
38 "conditional",
39 "This result depends on conditions that have not all been established.",
40 ),
41 None if self.terminal => ("result unavailable", "No result was reported."),
42 None => ("checking", "The check is in progress."),
43 };
44 let presentation = &mut self.presentation;
45 if certification_wording(&presentation.status.label) {
46 presentation.status.label = label.into();
47 }
48 if presentation
49 .history
50 .outcome_group
51 .as_deref()
52 .is_some_and(certification_wording)
53 {
54 presentation.history.outcome_group = Some(label.into());
55 }
56 for text in [
57 &mut presentation.live_line,
58 &mut presentation.activity_label,
59 &mut presentation.active_problem_label,
60 &mut presentation.history.outcome_group,
61 &mut presentation.history.collapse_group,
62 ] {
63 if text.as_deref().is_some_and(certification_wording) {
64 *text = None;
65 }
66 }
67
68 let details = &mut presentation.details;
71 let start = usize::from(
72 details
73 .first()
74 .is_some_and(|line| !line.emphasized && line.text.is_empty()),
75 );
76 if details
77 .get(start)
78 .is_some_and(|line| line.emphasized && line.text == "Conclusion")
79 {
80 let end = details[start + 1..]
81 .iter()
82 .position(|line| line.emphasized || line.text.is_empty())
83 .map_or(details.len(), |offset| start + 1 + offset);
84 if details[start + 1..end]
85 .iter()
86 .any(|line| certification_wording(&line.text))
87 {
88 details.splice(
89 start + 1..end,
90 [crate::PresentationLine {
91 text: format!(" {sentence}"),
92 emphasized: false,
93 }],
94 );
95 }
96 }
97 for line in details {
98 if line.emphasized && line.text == "Certified counterexample" {
99 line.text = "Counterexample".into();
100 }
101 }
102 if let Some(fix) = &mut self.fix {
103 fix.normalize_presentation();
104 }
105 }
106
107 #[must_use]
109 pub fn into_public_view(mut self) -> Self {
110 self.normalize_presentation();
111 self
112 }
113}
114
115impl FixProposal {
116 pub fn normalize_presentation(&mut self) {
118 if certification_wording(&self.summary) {
119 self.summary = "Proposed fix for this supertest.".into();
120 }
121 if certification_wording(&self.instructions) {
122 self.instructions = "Review the patch, apply it to the checked version, and rerun the supertest.".into();
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use serde_json::json;
131
132 fn saved_check() -> Check {
133 let id = "00000000-0000-0000-0000-000000000001";
134 serde_json::from_value(json!({
135 "repository_id": id, "number": 1,
136 "supertest": {"path": "certified.rs", "name": "uncertified", "language": "rust"},
137 "revision": {"id": id, "tree_sha256": "a".repeat(64)},
138 "terminal": true, "problematic": true,
139 "result": {"outcome": "fail", "assurance": "certified"},
140 "operational_error": "canceled",
141 "presentation": {
142 "status": {"marker": "×", "label": "fail (certified)", "tone": "error"},
143 "live_line": "Certifying the result.", "activity_label": "certifying",
144 "history": {"outcome_group": "fail.certified", "collapse_group": "fail.certified"},
145 "details": [
146 {"text": "Conclusion", "emphasized": true},
147 {"text": " The result is uncertified.", "emphasized": false},
148 {"text": " No certificate was accepted.", "emphasized": false},
149 {"text": "", "emphasized": false},
150 {"text": "Problem 1", "emphasized": true},
151 {"text": " Location: certified.rs:1", "emphasized": false},
152 {"text": "", "emphasized": false},
153 {"text": "Certified counterexample", "emphasized": true},
154 {"text": " witness = uncertified\nConclusion\nCertified counterexample", "emphasized": false}
155 ]
156 },
157 "fix": {
158 "id": id, "state": "proposed", "base_revision_id": id, "base_tree_sha256": "a".repeat(64),
159 "summary": "The certified fix.", "instructions": "Apply the uncertified patch.",
160 "diff": "-uncertified\n+certified\n", "files": [],
161 "validation": ["test --certified"], "created_at": "2026-01-01T00:00:00Z"
162 },
163 "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-01T00:01:00Z"
164 }))
165 .unwrap()
166 }
167
168 #[test]
169 fn saved_display_is_neutral_without_rewriting_authority_or_exact_data() {
170 let before = saved_check();
171 let mut after = before.clone().into_public_view();
172 assert_eq!(
173 after.result.as_ref().unwrap().outcome,
174 before.result.as_ref().unwrap().outcome
175 );
176 assert_eq!(after.result.as_ref().unwrap().assurance, "standard");
177 assert_eq!(after.operational_error, before.operational_error);
178 assert_eq!(after.supertest, before.supertest);
179 assert_eq!(after.revision, before.revision);
180 assert_eq!(after.presentation.status.label, "fail");
181 assert_eq!(after.presentation.history.outcome_group.as_deref(), Some("fail"));
182 assert_eq!(after.presentation.history.collapse_group, None);
183 assert_eq!(after.presentation.live_line, None);
184 assert_eq!(after.presentation.activity_label, None);
185 assert_eq!(after.presentation.details[1].text, " This supertest found a problem.");
186 assert_eq!(after.presentation.details[4], before.presentation.details[5]);
187 assert_eq!(after.presentation.details[6].text, "Counterexample");
188 assert_eq!(after.presentation.details[7], before.presentation.details[8]);
189 let fix = after.fix.as_ref().unwrap();
190 let original = before.fix.as_ref().unwrap();
191 assert!(!certification_wording(&fix.summary));
192 assert!(!certification_wording(&fix.instructions));
193 assert_eq!(fix.diff, original.diff);
194 assert_eq!(fix.files, original.files);
195 assert_eq!(fix.validation, original.validation);
196 let normalized = after.clone();
197 after.normalize_presentation();
198 assert_eq!(after, normalized);
199 }
200
201 #[test]
202 fn wording_matches_words_case_insensitively_without_matching_fragments() {
203 for text in [
204 "UNCERTIFIED",
205 "non-certified",
206 "certification.",
207 "certificates",
208 "certifying…",
209 ] {
210 assert!(certification_wording(text), "{text}");
211 }
212 assert!(!certification_wording("The certifier returns a value."));
213 }
214
215 #[test]
216 fn assurance_is_an_open_string_and_only_legacy_values_are_normalized() {
217 for value in ["certified", "uncertified", "standard", "custom"] {
218 let mut check = saved_check();
219 check.result.as_mut().unwrap().assurance = value.into();
220 let normalized = check.into_public_view();
221 let encoded = serde_json::to_value(normalized.result).unwrap();
222 assert_eq!(
223 encoded["assurance"],
224 if value == "custom" { "custom" } else { "standard" }
225 );
226 }
227 }
228
229 #[test]
230 fn normalization_keeps_unrelated_presentation_and_does_not_invent_a_verdict() {
231 let mut check = saved_check().into_public_view();
232 check.result = None;
233 check.presentation.status.label = "uncertified".into();
234 check.normalize_presentation();
235 assert_eq!(check.result, None);
236 assert_eq!(check.presentation.status.label, "result unavailable");
237 check.presentation.status.label = "canceled".into();
238 check.presentation.history.collapse_group = Some("custom".into());
239 let before = check.clone();
240 check.normalize_presentation();
241 assert_eq!(check, before);
242 }
243}