use crate::advisor::types::{
ADVISOR_GUIDANCE, AdvisorDeliveryChannel, AdvisorNote, AdvisorSeverity, DeliveryOpts,
};
#[must_use]
pub fn is_interrupting_severity(severity: Option<AdvisorSeverity>) -> bool {
matches!(
severity,
Some(AdvisorSeverity::Concern | AdvisorSeverity::Blocker)
)
}
#[must_use]
pub fn is_immune_turn_active(
completed_turns: u64,
immune_start: Option<u64>,
immune_turns: u64,
) -> bool {
let Some(start) = immune_start else {
return false;
};
if immune_turns == 0 {
return false;
}
completed_turns < start.saturating_add(immune_turns)
}
#[must_use]
pub fn resolve_delivery_channel(opts: DeliveryOpts) -> AdvisorDeliveryChannel {
if !is_interrupting_severity(opts.severity) {
return AdvisorDeliveryChannel::Aside;
}
if opts.auto_resume_suppressed && (opts.aborting || !opts.streaming) {
return AdvisorDeliveryChannel::Preserve;
}
if opts.interrupt_immune_turn_active {
return AdvisorDeliveryChannel::Aside;
}
AdvisorDeliveryChannel::Steer
}
#[must_use]
pub fn format_advisory_batch(notes: &[AdvisorNote]) -> String {
if notes.is_empty() {
return String::new();
}
let mut parts: Vec<String> = Vec::with_capacity(notes.len());
for n in notes {
let severity_attr = match n.severity {
Some(s) => format!(" severity=\"{}\"", s.as_str()),
None => String::new(),
};
parts.push(format!(
"<advisory{severity_attr} guidance=\"{g}\">\n{note}\n</advisory>",
severity_attr = severity_attr,
g = ADVISOR_GUIDANCE,
note = escape_xml_text(&n.note)
));
}
parts.join("\n")
}
#[must_use]
pub(crate) fn escape_xml_text(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
_ => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn nit_is_non_interrupting() {
assert!(!is_interrupting_severity(None));
assert!(!is_interrupting_severity(Some(AdvisorSeverity::Nit)));
assert!(is_interrupting_severity(Some(AdvisorSeverity::Concern)));
assert!(is_interrupting_severity(Some(AdvisorSeverity::Blocker)));
}
#[test]
fn immune_fence_is_half_open() {
assert!(!is_immune_turn_active(5, None, 3));
assert!(!is_immune_turn_active(0, Some(0), 0));
assert!(is_immune_turn_active(10, Some(10), 2));
assert!(is_immune_turn_active(11, Some(10), 2));
assert!(!is_immune_turn_active(12, Some(10), 2));
}
#[test]
fn channel_nit_always_aside() {
for &streaming in &[false, true] {
for &supp in &[false, true] {
assert_eq!(
resolve_delivery_channel(DeliveryOpts {
severity: Some(AdvisorSeverity::Nit),
auto_resume_suppressed: supp,
streaming,
aborting: false,
interrupt_immune_turn_active: false,
}),
AdvisorDeliveryChannel::Aside
);
}
}
}
#[test]
fn channel_concern_live_steers() {
assert_eq!(
resolve_delivery_channel(DeliveryOpts {
severity: Some(AdvisorSeverity::Concern),
streaming: true,
..Default::default()
}),
AdvisorDeliveryChannel::Steer
);
}
#[test]
fn channel_post_interrupt_idle_preserves() {
assert_eq!(
resolve_delivery_channel(DeliveryOpts {
severity: Some(AdvisorSeverity::Blocker),
auto_resume_suppressed: true,
streaming: false, aborting: false,
..Default::default()
}),
AdvisorDeliveryChannel::Preserve
);
assert_eq!(
resolve_delivery_channel(DeliveryOpts {
severity: Some(AdvisorSeverity::Blocker),
auto_resume_suppressed: true,
streaming: true,
aborting: false,
..Default::default()
}),
AdvisorDeliveryChannel::Steer
);
assert_eq!(
resolve_delivery_channel(DeliveryOpts {
severity: Some(AdvisorSeverity::Concern),
auto_resume_suppressed: true,
streaming: true,
aborting: true,
..Default::default()
}),
AdvisorDeliveryChannel::Preserve
);
}
#[test]
fn channel_immune_downgrades_to_aside() {
assert_eq!(
resolve_delivery_channel(DeliveryOpts {
severity: Some(AdvisorSeverity::Blocker),
streaming: true,
interrupt_immune_turn_active: true,
..Default::default()
}),
AdvisorDeliveryChannel::Aside
);
}
#[test]
fn batch_renders_advisory_elements() {
let notes = vec![
AdvisorNote {
note: "Stop.".into(),
severity: Some(AdvisorSeverity::Blocker),
},
AdvisorNote {
note: "rename x".into(),
severity: None, },
];
let out = format_advisory_batch(¬es);
assert!(
out.contains("<advisory severity=\"blocker\" guidance=\"weigh, don't blindly obey\">")
);
assert!(out.contains("Stop."));
assert!(out.contains("<advisory guidance=\"weigh, don't blindly obey\">\nrename x"));
}
#[test]
fn batch_escapes_xml_significant_chars() {
let notes = vec![AdvisorNote {
note: "a < b & c > d \"e\"".into(),
severity: Some(AdvisorSeverity::Nit),
}];
let out = format_advisory_batch(¬es);
assert!(out.contains("<"));
assert!(out.contains("&"));
assert!(out.contains(">"));
assert!(out.contains("""));
assert!(!out.contains(" < ") || out.matches("<").count() >= 1);
}
#[test]
fn empty_batch_is_empty() {
assert_eq!(format_advisory_batch(&[]), "");
}
}