1#![cfg_attr(not(test), allow(dead_code))]
2
3use super::config::{RoleType, WorkflowPolicy};
4
5pub fn check_wip_limit(policy: &WorkflowPolicy, role_type: RoleType, active_count: u32) -> bool {
6 let limit = match role_type {
7 RoleType::Engineer => policy.wip_limit_per_engineer,
8 RoleType::Architect | RoleType::Manager => policy.wip_limit_per_reviewer,
9 RoleType::User => None,
10 };
11
12 match limit {
13 Some(limit) => active_count < limit,
14 None => true,
15 }
16}
17
18pub fn is_review_nudge_due(policy: &WorkflowPolicy, review_age_secs: u64) -> bool {
19 review_age_secs >= policy.review_nudge_threshold_secs
20}
21
22pub fn is_review_stale(policy: &WorkflowPolicy, review_age_secs: u64) -> bool {
23 review_age_secs >= policy.review_timeout_secs
24}
25
26pub fn should_escalate(policy: &WorkflowPolicy, blocked_age_secs: u64) -> bool {
27 blocked_age_secs >= policy.escalation_threshold_secs
28}
29
30pub fn effective_nudge_threshold(policy: &WorkflowPolicy, priority: &str) -> u64 {
33 if !priority.is_empty() {
34 if let Some(ovr) = policy.review_timeout_overrides.get(priority) {
35 if let Some(secs) = ovr.review_nudge_threshold_secs {
36 return secs;
37 }
38 }
39 }
40 policy.review_nudge_threshold_secs
41}
42
43pub fn effective_escalation_threshold(policy: &WorkflowPolicy, priority: &str) -> u64 {
47 if !priority.is_empty() {
48 if let Some(ovr) = policy.review_timeout_overrides.get(priority) {
49 if let Some(secs) = ovr.review_timeout_secs {
50 return secs;
51 }
52 }
53 }
54 policy.review_timeout_secs
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60
61 #[test]
62 fn default_workflow_policy_has_sensible_defaults() {
63 let policy = WorkflowPolicy::default();
64 assert_eq!(policy.wip_limit_per_engineer, None);
65 assert_eq!(policy.wip_limit_per_reviewer, None);
66 assert_eq!(policy.pipeline_starvation_threshold, Some(1));
67 assert_eq!(policy.escalation_threshold_secs, 3600);
68 assert_eq!(policy.review_nudge_threshold_secs, 1800);
69 assert_eq!(policy.review_timeout_secs, 7200);
70 assert_eq!(policy.auto_archive_done_after_secs, None);
71 assert!(policy.capability_overrides.is_empty());
72 }
73
74 #[test]
75 fn check_wip_limit_enforces_limits() {
76 let policy = WorkflowPolicy {
77 wip_limit_per_engineer: Some(2),
78 wip_limit_per_reviewer: Some(1),
79 ..WorkflowPolicy::default()
80 };
81
82 assert!(check_wip_limit(&policy, RoleType::Engineer, 0));
83 assert!(check_wip_limit(&policy, RoleType::Engineer, 1));
84 assert!(!check_wip_limit(&policy, RoleType::Engineer, 2));
85 assert!(check_wip_limit(&policy, RoleType::Manager, 0));
86 assert!(!check_wip_limit(&policy, RoleType::Manager, 1));
87 assert!(check_wip_limit(&policy, RoleType::User, 99));
88 }
89
90 #[test]
91 fn stale_and_escalation_threshold_checks_are_inclusive() {
92 let policy = WorkflowPolicy {
93 escalation_threshold_secs: 120,
94 review_timeout_secs: 300,
95 ..WorkflowPolicy::default()
96 };
97
98 assert!(!should_escalate(&policy, 119));
99 assert!(should_escalate(&policy, 120));
100 assert!(!is_review_stale(&policy, 299));
101 assert!(is_review_stale(&policy, 300));
102 }
103
104 #[test]
105 fn review_nudge_threshold_check_is_inclusive() {
106 let policy = WorkflowPolicy {
107 review_nudge_threshold_secs: 1800,
108 ..WorkflowPolicy::default()
109 };
110
111 assert!(!is_review_nudge_due(&policy, 1799));
112 assert!(is_review_nudge_due(&policy, 1800));
113 assert!(is_review_nudge_due(&policy, 1801));
114 }
115
116 #[test]
117 fn effective_nudge_threshold_uses_global_when_no_override() {
118 let policy = WorkflowPolicy {
119 review_nudge_threshold_secs: 1800,
120 ..WorkflowPolicy::default()
121 };
122 assert_eq!(effective_nudge_threshold(&policy, "high"), 1800);
123 assert_eq!(effective_nudge_threshold(&policy, ""), 1800);
124 }
125
126 #[test]
127 fn effective_nudge_threshold_uses_priority_override() {
128 use super::super::config::ReviewTimeoutOverride;
129 let mut overrides = std::collections::HashMap::new();
130 overrides.insert(
131 "critical".to_string(),
132 ReviewTimeoutOverride {
133 review_nudge_threshold_secs: Some(300),
134 review_timeout_secs: Some(600),
135 },
136 );
137 let policy = WorkflowPolicy {
138 review_nudge_threshold_secs: 1800,
139 review_timeout_overrides: overrides,
140 ..WorkflowPolicy::default()
141 };
142 assert_eq!(effective_nudge_threshold(&policy, "critical"), 300);
144 assert_eq!(effective_nudge_threshold(&policy, "high"), 1800);
146 }
147
148 #[test]
149 fn effective_escalation_threshold_uses_priority_override() {
150 use super::super::config::ReviewTimeoutOverride;
151 let mut overrides = std::collections::HashMap::new();
152 overrides.insert(
153 "critical".to_string(),
154 ReviewTimeoutOverride {
155 review_nudge_threshold_secs: Some(300),
156 review_timeout_secs: Some(600),
157 },
158 );
159 overrides.insert(
160 "high".to_string(),
161 ReviewTimeoutOverride {
162 review_nudge_threshold_secs: None,
163 review_timeout_secs: Some(3600),
164 },
165 );
166 let policy = WorkflowPolicy {
167 review_timeout_secs: 7200,
168 review_timeout_overrides: overrides,
169 ..WorkflowPolicy::default()
170 };
171 assert_eq!(effective_escalation_threshold(&policy, "critical"), 600);
173 assert_eq!(effective_escalation_threshold(&policy, "high"), 3600);
175 assert_eq!(effective_escalation_threshold(&policy, "medium"), 7200);
177 }
178
179 #[test]
180 fn partial_override_falls_back_per_field() {
181 use super::super::config::ReviewTimeoutOverride;
182 let mut overrides = std::collections::HashMap::new();
183 overrides.insert(
184 "high".to_string(),
185 ReviewTimeoutOverride {
186 review_nudge_threshold_secs: Some(900),
187 review_timeout_secs: None, },
189 );
190 let policy = WorkflowPolicy {
191 review_nudge_threshold_secs: 1800,
192 review_timeout_secs: 7200,
193 review_timeout_overrides: overrides,
194 ..WorkflowPolicy::default()
195 };
196 assert_eq!(effective_nudge_threshold(&policy, "high"), 900);
198 assert_eq!(effective_escalation_threshold(&policy, "high"), 7200);
200 }
201
202 #[test]
205 fn wip_limit_none_means_unlimited() {
206 let policy = WorkflowPolicy {
207 wip_limit_per_engineer: None,
208 wip_limit_per_reviewer: None,
209 ..WorkflowPolicy::default()
210 };
211
212 assert!(check_wip_limit(&policy, RoleType::Engineer, 100));
214 assert!(check_wip_limit(&policy, RoleType::Engineer, u32::MAX));
215 assert!(check_wip_limit(&policy, RoleType::Manager, 100));
216 assert!(check_wip_limit(&policy, RoleType::Architect, u32::MAX));
217 }
218
219 #[test]
220 fn wip_limit_zero_blocks_all_work() {
221 let policy = WorkflowPolicy {
222 wip_limit_per_engineer: Some(0),
223 wip_limit_per_reviewer: Some(0),
224 ..WorkflowPolicy::default()
225 };
226
227 assert!(!check_wip_limit(&policy, RoleType::Engineer, 0));
228 assert!(!check_wip_limit(&policy, RoleType::Manager, 0));
229 assert!(!check_wip_limit(&policy, RoleType::Architect, 0));
230 }
231
232 #[test]
233 fn architect_uses_reviewer_wip_limit() {
234 let policy = WorkflowPolicy {
235 wip_limit_per_engineer: Some(5),
236 wip_limit_per_reviewer: Some(2),
237 ..WorkflowPolicy::default()
238 };
239
240 assert!(check_wip_limit(&policy, RoleType::Architect, 1));
242 assert!(!check_wip_limit(&policy, RoleType::Architect, 2));
243 assert!(!check_wip_limit(&policy, RoleType::Architect, 3));
244 }
245
246 #[test]
247 fn user_role_always_passes_wip_check() {
248 let policy = WorkflowPolicy {
249 wip_limit_per_engineer: Some(1),
250 wip_limit_per_reviewer: Some(1),
251 ..WorkflowPolicy::default()
252 };
253
254 assert!(check_wip_limit(&policy, RoleType::User, 0));
255 assert!(check_wip_limit(&policy, RoleType::User, 100));
256 assert!(check_wip_limit(&policy, RoleType::User, u32::MAX));
257 }
258
259 #[test]
260 fn escalation_boundary_values() {
261 let policy = WorkflowPolicy {
262 escalation_threshold_secs: 0,
263 ..WorkflowPolicy::default()
264 };
265
266 assert!(should_escalate(&policy, 0));
268 assert!(should_escalate(&policy, 1));
269 }
270
271 #[test]
272 fn review_nudge_at_zero_threshold() {
273 let policy = WorkflowPolicy {
274 review_nudge_threshold_secs: 0,
275 ..WorkflowPolicy::default()
276 };
277
278 assert!(is_review_nudge_due(&policy, 0));
279 assert!(is_review_nudge_due(&policy, 1));
280 }
281
282 #[test]
283 fn review_stale_at_zero_threshold() {
284 let policy = WorkflowPolicy {
285 review_timeout_secs: 0,
286 ..WorkflowPolicy::default()
287 };
288
289 assert!(is_review_stale(&policy, 0));
290 assert!(is_review_stale(&policy, 1));
291 }
292
293 #[test]
294 fn review_nudge_well_before_threshold() {
295 let policy = WorkflowPolicy {
296 review_nudge_threshold_secs: 10000,
297 ..WorkflowPolicy::default()
298 };
299
300 assert!(!is_review_nudge_due(&policy, 0));
301 assert!(!is_review_nudge_due(&policy, 5000));
302 assert!(!is_review_nudge_due(&policy, 9999));
303 }
304
305 #[test]
306 fn override_with_both_fields_none_falls_back_to_global() {
307 use super::super::config::ReviewTimeoutOverride;
308 let mut overrides = std::collections::HashMap::new();
309 overrides.insert(
310 "low".to_string(),
311 ReviewTimeoutOverride {
312 review_nudge_threshold_secs: None,
313 review_timeout_secs: None,
314 },
315 );
316 let policy = WorkflowPolicy {
317 review_nudge_threshold_secs: 1800,
318 review_timeout_secs: 7200,
319 review_timeout_overrides: overrides,
320 ..WorkflowPolicy::default()
321 };
322
323 assert_eq!(effective_nudge_threshold(&policy, "low"), 1800);
325 assert_eq!(effective_escalation_threshold(&policy, "low"), 7200);
326 }
327
328 #[test]
329 fn multiple_priority_overrides_are_independent() {
330 use super::super::config::ReviewTimeoutOverride;
331 let mut overrides = std::collections::HashMap::new();
332 overrides.insert(
333 "critical".to_string(),
334 ReviewTimeoutOverride {
335 review_nudge_threshold_secs: Some(60),
336 review_timeout_secs: Some(120),
337 },
338 );
339 overrides.insert(
340 "high".to_string(),
341 ReviewTimeoutOverride {
342 review_nudge_threshold_secs: Some(300),
343 review_timeout_secs: Some(600),
344 },
345 );
346 overrides.insert(
347 "low".to_string(),
348 ReviewTimeoutOverride {
349 review_nudge_threshold_secs: Some(3600),
350 review_timeout_secs: Some(14400),
351 },
352 );
353 let policy = WorkflowPolicy {
354 review_nudge_threshold_secs: 1800,
355 review_timeout_secs: 7200,
356 review_timeout_overrides: overrides,
357 ..WorkflowPolicy::default()
358 };
359
360 assert_eq!(effective_nudge_threshold(&policy, "critical"), 60);
361 assert_eq!(effective_escalation_threshold(&policy, "critical"), 120);
362 assert_eq!(effective_nudge_threshold(&policy, "high"), 300);
363 assert_eq!(effective_escalation_threshold(&policy, "high"), 600);
364 assert_eq!(effective_nudge_threshold(&policy, "low"), 3600);
365 assert_eq!(effective_escalation_threshold(&policy, "low"), 14400);
366 assert_eq!(effective_nudge_threshold(&policy, "medium"), 1800);
368 assert_eq!(effective_escalation_threshold(&policy, "medium"), 7200);
369 }
370
371 #[test]
372 fn override_with_only_escalation_set() {
373 use super::super::config::ReviewTimeoutOverride;
374 let mut overrides = std::collections::HashMap::new();
375 overrides.insert(
376 "urgent".to_string(),
377 ReviewTimeoutOverride {
378 review_nudge_threshold_secs: None,
379 review_timeout_secs: Some(300),
380 },
381 );
382 let policy = WorkflowPolicy {
383 review_nudge_threshold_secs: 1800,
384 review_timeout_secs: 7200,
385 review_timeout_overrides: overrides,
386 ..WorkflowPolicy::default()
387 };
388
389 assert_eq!(effective_nudge_threshold(&policy, "urgent"), 1800);
391 assert_eq!(effective_escalation_threshold(&policy, "urgent"), 300);
392 }
393
394 #[test]
395 fn wip_limit_at_exact_boundary() {
396 let policy = WorkflowPolicy {
397 wip_limit_per_engineer: Some(3),
398 ..WorkflowPolicy::default()
399 };
400
401 assert!(check_wip_limit(&policy, RoleType::Engineer, 2)); assert!(!check_wip_limit(&policy, RoleType::Engineer, 3)); assert!(!check_wip_limit(&policy, RoleType::Engineer, 4)); }
405
406 #[test]
407 fn default_policy_stall_and_health_fields() {
408 let policy = WorkflowPolicy::default();
409 assert_eq!(policy.stall_threshold_secs, 300);
410 assert_eq!(policy.max_stall_restarts, 2);
411 assert_eq!(policy.health_check_interval_secs, 60);
412 assert_eq!(policy.uncommitted_warn_threshold, 200);
413 }
414
415 #[test]
416 fn escalation_with_large_values() {
417 let policy = WorkflowPolicy {
418 escalation_threshold_secs: u64::MAX,
419 ..WorkflowPolicy::default()
420 };
421
422 assert!(!should_escalate(&policy, 0));
424 assert!(!should_escalate(&policy, u64::MAX - 1));
425 assert!(should_escalate(&policy, u64::MAX));
426 }
427}