use std::borrow::Cow;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as _;
use std::io::Read;
use std::path::Path;
use chrono::{DateTime, NaiveDate, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::config::Config;
use crate::detect::Finding;
use crate::quality_gate;
use crate::report::{AcknowledgedFinding, Report, Warning, warnings};
pub const MAX_ACKNOWLEDGMENTS_FILE_BYTES: u64 = 16 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportOrigin {
FreshAnalysis,
Precomputed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Acknowledgment {
pub signature: String,
pub acknowledged_by: String,
pub acknowledged_at: String,
pub reason: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub service: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_endpoint: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AcknowledgmentsFile {
#[serde(default)]
pub acknowledged: Vec<Acknowledgment>,
}
#[must_use]
pub fn compute_signature(finding: &Finding) -> String {
let mut hasher = Sha256::new();
hasher.update(finding.pattern.template.as_bytes());
let digest = hasher.finalize();
let safe_service = crate::text_safety::strip_bidi_and_invisible(&finding.service);
let sanitized_endpoint = sanitize_endpoint(&finding.source_endpoint);
let safe_endpoint = crate::text_safety::strip_bidi_and_invisible(&sanitized_endpoint);
let kind = finding.finding_type.as_str();
let mut out = String::with_capacity(kind.len() + safe_service.len() + safe_endpoint.len() + 35);
out.push_str(kind);
out.push(':');
out.push_str(safe_service.as_ref());
out.push(':');
out.push_str(safe_endpoint.as_ref());
out.push(':');
for byte in &digest[..16] {
let _ = write!(out, "{byte:02x}");
}
out
}
fn sanitize_endpoint(endpoint: &str) -> Cow<'_, str> {
if endpoint.bytes().any(|b| matches!(b, b'/' | b' ')) {
Cow::Owned(endpoint.replace(['/', ' '], "_"))
} else {
Cow::Borrowed(endpoint)
}
}
pub fn enrich_with_signatures(findings: &mut [Finding]) {
for finding in findings.iter_mut() {
finding.signature = compute_signature(finding);
}
}
pub fn load_from_file(path: &Path) -> Result<AcknowledgmentsFile, AcknowledgmentLoadError> {
match std::fs::symlink_metadata(path) {
Ok(meta) => {
if meta.file_type().is_symlink() {
return Err(AcknowledgmentLoadError::SymlinkRefused);
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
return Ok(AcknowledgmentsFile::default());
}
Err(err) => return Err(AcknowledgmentLoadError::Io(err)),
}
let file = std::fs::File::open(path).map_err(AcknowledgmentLoadError::Io)?;
let mut buf = String::new();
file.take(MAX_ACKNOWLEDGMENTS_FILE_BYTES + 1)
.read_to_string(&mut buf)
.map_err(AcknowledgmentLoadError::Io)?;
if buf.len() as u64 > MAX_ACKNOWLEDGMENTS_FILE_BYTES {
return Err(AcknowledgmentLoadError::TooLarge {
cap: MAX_ACKNOWLEDGMENTS_FILE_BYTES,
});
}
let parsed: AcknowledgmentsFile =
toml::from_str(&buf).map_err(AcknowledgmentLoadError::Parse)?;
for (idx, ack) in parsed.acknowledged.iter().enumerate() {
if let Some(ref expires) = ack.expires_at {
NaiveDate::parse_from_str(expires, "%Y-%m-%d").map_err(|e| {
AcknowledgmentLoadError::InvalidDate {
entry_index: idx,
field: "expires_at",
value: expires.clone(),
message: e.to_string(),
}
})?;
}
}
Ok(parsed)
}
pub fn apply_to_report(
report: &mut Report,
acks: &AcknowledgmentsFile,
config: &Config,
now: DateTime<Utc>,
origin: ReportOrigin,
) {
report.acknowledged_findings.clear();
report
.warning_details
.retain(|w| w.kind != warnings::UNMATCHED_ACKNOWLEDGMENT);
let active: HashMap<&str, &Acknowledgment> = acks
.acknowledged
.iter()
.filter(|a| is_ack_active(a, now))
.map(|a| (a.signature.as_str(), a))
.collect();
if !active.is_empty() {
let mut matched: HashSet<&str> = HashSet::with_capacity(active.len());
let original = std::mem::take(&mut report.findings);
let mut kept = Vec::with_capacity(original.len());
for finding in original {
let sig: Cow<'_, str> = if finding.signature.is_empty() {
Cow::Owned(compute_signature(&finding))
} else {
Cow::Borrowed(finding.signature.as_str())
};
if let Some((ack_sig, ack)) = active.get_key_value(sig.as_ref()) {
matched.insert(ack_sig);
report.acknowledged_findings.push(AcknowledgedFinding {
finding,
acknowledgment: (*ack).clone(),
});
} else {
kept.push(finding);
}
}
report.findings = kept;
if origin == ReportOrigin::FreshAnalysis {
let mut unmatched: Vec<&Acknowledgment> = active
.values()
.filter(|a| !matched.contains(a.signature.as_str()))
.copied()
.collect();
unmatched.sort_unstable_by(|a, b| a.signature.cmp(&b.signature));
let observed: HashSet<(&str, &str)> = report
.per_endpoint_io_ops
.iter()
.map(|e| (e.service.as_str(), e.endpoint.as_str()))
.collect();
let new_warnings: Vec<Warning> = unmatched
.iter()
.map(|ack| {
Warning::from_untrusted(
warnings::UNMATCHED_ACKNOWLEDGMENT,
&unmatched_message(ack, &observed),
)
})
.collect();
report.warning_details.extend(new_warnings);
}
}
report.quality_gate = quality_gate::evaluate(
&report.findings,
&report.green_summary,
&config.thresholds,
report.analysis.ingest.as_ref(),
);
}
fn unmatched_message(ack: &Acknowledgment, observed: &HashSet<(&str, &str)>) -> String {
let sig = &ack.signature;
match (&ack.service, &ack.source_endpoint) {
(Some(service), Some(endpoint)) => {
if observed.contains(&(service.as_str(), endpoint.as_str())) {
format!(
"acknowledgment {sig} matched no finding in this run: \
{service} {endpoint} was exercised and the finding did not \
fire, the problem looks fixed and the entry can be removed"
)
} else {
format!(
"acknowledgment {sig} matched no finding in this run: \
{service} {endpoint} emitted no I/O in this run (not \
exercised, or its I/O was removed outright), so this \
proves nothing, keep the entry"
)
}
}
_ => format!(
"acknowledgment {sig} matched no finding in this run: \
the problem is either fixed, and the entry can be removed, \
or the scenario that produced it did not run (add service and \
source_endpoint to the entry to tell the two apart)"
),
}
}
pub(crate) fn is_ack_active(ack: &Acknowledgment, now: DateTime<Utc>) -> bool {
let Some(ref expires) = ack.expires_at else {
return true;
};
let Ok(parsed) = NaiveDate::parse_from_str(expires, "%Y-%m-%d") else {
return false;
};
let Some(end_of_day) = parsed.and_hms_opt(23, 59, 59) else {
return false;
};
end_of_day.and_utc() >= now
}
#[derive(Debug, thiserror::Error)]
pub enum AcknowledgmentLoadError {
#[error("Failed to read acknowledgments file: {0}")]
Io(#[from] std::io::Error),
#[error("Acknowledgments file exceeds the {cap}-byte cap")]
TooLarge { cap: u64 },
#[error("Failed to parse acknowledgments TOML: {0}")]
Parse(toml::de::Error),
#[error("Entry {entry_index}: invalid {field} value '{value}': {message}")]
InvalidDate {
entry_index: usize,
field: &'static str,
value: String,
message: String,
},
#[error("Acknowledgments file is a symlink, refusing to follow")]
SymlinkRefused,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::detect::{FindingType, Severity};
use crate::report::{Analysis, GreenSummary, QualityGate};
use crate::test_helpers::make_finding;
use chrono::TimeZone;
use core::assert_matches;
fn empty_report(findings: Vec<Finding>) -> Report {
Report {
analysis: Analysis {
duration_ms: 0,
events_processed: findings.len(),
traces_analyzed: 1,
ingest: None,
},
findings,
green_summary: GreenSummary::disabled(0),
quality_gate: QualityGate {
passed: true,
rules: vec![],
},
per_endpoint_io_ops: vec![],
correlations: vec![],
embedded_traces: vec![],
warnings: vec![],
warning_details: vec![],
acknowledged_findings: vec![],
binary_version: String::new(),
detection_config: None,
disclosure_waste: None,
}
}
fn ack(signature: &str, expires_at: Option<&str>) -> Acknowledgment {
Acknowledgment {
signature: signature.to_string(),
acknowledged_by: "test@example.com".to_string(),
acknowledged_at: "2026-05-02".to_string(),
reason: "test".to_string(),
expires_at: expires_at.map(str::to_string),
service: None,
source_endpoint: None,
}
}
fn now_2026_05_02() -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 5, 2, 12, 0, 0).unwrap()
}
#[test]
fn compute_signature_deterministic() {
let f = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let sig1 = compute_signature(&f);
let sig2 = compute_signature(&f);
assert_eq!(sig1, sig2);
}
#[test]
fn compute_signature_differs_with_template() {
let mut f1 = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let mut f2 = f1.clone();
f1.pattern.template = "SELECT * FROM users WHERE id = ?".to_string();
f2.pattern.template = "SELECT * FROM orders WHERE id = ?".to_string();
assert_ne!(compute_signature(&f1), compute_signature(&f2));
}
#[test]
fn compute_signature_sanitizes_endpoint() {
let mut f = make_finding(FindingType::NPlusOneSql, Severity::Warning);
f.source_endpoint = "GET /api/foo bar".to_string();
let sig = compute_signature(&f);
let parts: Vec<&str> = sig.split(':').collect();
assert_eq!(
parts.len(),
4,
"signature must have 4 colon-separated parts: {sig}"
);
assert!(
!parts[2].contains('/'),
"endpoint segment must not contain '/'"
);
assert!(
!parts[2].contains(' '),
"endpoint segment must not contain ' '"
);
}
#[test]
fn compute_signature_strips_bidi_and_invisible_from_service_and_endpoint() {
let mut f1 = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let mut f2 = f1.clone();
f1.service = "alice\u{202E}@evil.com".to_string();
f1.source_endpoint = "GET /api/items\u{200B}".to_string();
f2.service = "alice@evil.com".to_string();
f2.source_endpoint = "GET /api/items".to_string();
assert_eq!(
compute_signature(&f1),
compute_signature(&f2),
"BiDi/invisible characters must be stripped before signature construction"
);
}
#[test]
fn compute_signature_format_matches_brief() {
let mut f = make_finding(FindingType::RedundantSql, Severity::Warning);
f.service = "order-service".to_string();
f.source_endpoint = "POST /api/orders".to_string();
f.pattern.template = "SELECT 1".to_string();
let sig = compute_signature(&f);
let mut parts = sig.splitn(4, ':');
assert_eq!(parts.next(), Some("redundant_sql"));
assert_eq!(parts.next(), Some("order-service"));
assert_eq!(parts.next(), Some("POST__api_orders"));
let hex = parts.next().expect("hex prefix present");
assert_eq!(hex.len(), 32, "hex prefix is 32 characters (16 bytes)");
assert!(
hex.chars().all(|c| c.is_ascii_hexdigit()),
"hex prefix is hex"
);
}
#[test]
fn signature_stable_across_trace_id_changes() {
let mut f1 = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let mut f2 = f1.clone();
f1.trace_id = "aaaaaaaaaaaaaaaa0000000000000000".to_string();
f2.trace_id = "ffffffffffffffff1111111111111111".to_string();
assert_ne!(f1.trace_id, f2.trace_id);
assert_eq!(
compute_signature(&f1),
compute_signature(&f2),
"signature must not depend on trace_id (acks survive service restarts)"
);
}
#[test]
fn compute_signature_differs_with_endpoint() {
let mut f1 = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let mut f2 = f1.clone();
f1.source_endpoint = "POST /api/orders".to_string();
f2.source_endpoint = "POST /api/users".to_string();
assert_ne!(compute_signature(&f1), compute_signature(&f2));
}
#[test]
fn compute_signature_differs_with_service() {
let mut f1 = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let mut f2 = f1.clone();
f1.service = "order-svc".to_string();
f2.service = "user-svc".to_string();
assert_ne!(compute_signature(&f1), compute_signature(&f2));
}
#[test]
fn compute_signature_differs_with_finding_type() {
let f1 = make_finding(FindingType::NPlusOneSql, Severity::Warning);
let f2 = make_finding(FindingType::RedundantSql, Severity::Warning);
assert_ne!(compute_signature(&f1), compute_signature(&f2));
}
#[test]
fn load_from_file_rejects_oversized_input() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("acks.toml");
let payload = vec![b'x'; (MAX_ACKNOWLEDGMENTS_FILE_BYTES + 1) as usize];
std::fs::write(&path, &payload).unwrap();
let err = load_from_file(&path).expect_err("oversized file must fail");
assert!(
matches!(err, AcknowledgmentLoadError::TooLarge { .. }),
"expected TooLarge, got: {err:?}"
);
}
#[test]
fn apply_to_report_clears_prior_acked_entries() {
let stale_finding = make_finding(FindingType::SlowSql, Severity::Warning);
let stale_ack = Acknowledgment {
signature: "stale".to_string(),
acknowledged_by: "stale@example.com".to_string(),
acknowledged_at: "2020-01-01".to_string(),
reason: "from a previous run".to_string(),
expires_at: None,
service: None,
source_endpoint: None,
};
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let mut report = empty_report(findings);
report.acknowledged_findings.push(AcknowledgedFinding {
finding: stale_finding,
acknowledgment: stale_ack,
});
let acks = AcknowledgmentsFile::default();
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert!(
report.acknowledged_findings.is_empty(),
"stale ack pair must be cleared on entry"
);
assert_eq!(report.findings.len(), 1, "active findings preserved");
}
#[test]
fn load_from_file_nonexistent_returns_empty() {
let path = std::path::PathBuf::from("/tmp/perf-sentinel-acks-does-not-exist.toml");
let result = load_from_file(&path).expect("missing file should be Ok");
assert!(result.acknowledged.is_empty());
}
#[test]
fn load_from_file_valid_parses() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("acks.toml");
std::fs::write(
&path,
r#"
[[acknowledged]]
signature = "n_plus_one_sql:svc:GET_/a:abcd1234abcd1234abcd1234abcd1234"
acknowledged_by = "alice@example.com"
acknowledged_at = "2026-04-15"
reason = "documented"
[[acknowledged]]
signature = "redundant_sql:svc:POST_/b:11223344112233441122334411223344"
acknowledged_by = "bob@example.com"
acknowledged_at = "2026-04-20"
reason = "won't fix"
expires_at = "2026-12-31"
"#,
)
.unwrap();
let parsed = load_from_file(&path).expect("valid TOML parses");
assert_eq!(parsed.acknowledged.len(), 2);
assert_eq!(parsed.acknowledged[0].acknowledged_by, "alice@example.com");
assert_eq!(
parsed.acknowledged[1].expires_at.as_deref(),
Some("2026-12-31")
);
}
#[test]
fn load_from_file_missing_signature_field_fails() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("acks.toml");
std::fs::write(
&path,
r#"
[[acknowledged]]
acknowledged_by = "alice@example.com"
acknowledged_at = "2026-04-15"
reason = "missing signature"
"#,
)
.unwrap();
let err = load_from_file(&path).expect_err("missing field must fail");
assert_matches!(err, AcknowledgmentLoadError::Parse(_));
}
#[test]
fn load_from_file_invalid_expires_at_fails() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("acks.toml");
std::fs::write(
&path,
r#"
[[acknowledged]]
signature = "redundant_sql:svc:POST_/b:11223344112233441122334411223344"
acknowledged_by = "alice@example.com"
acknowledged_at = "2026-04-15"
reason = "bad date"
expires_at = "not-a-date"
"#,
)
.unwrap();
let err = load_from_file(&path).expect_err("invalid date must fail");
assert_matches!(
err,
AcknowledgmentLoadError::InvalidDate {
field: "expires_at",
..
}
);
}
#[test]
fn apply_to_report_filters_matching() {
let mut findings = vec![
make_finding(FindingType::NPlusOneSql, Severity::Warning),
make_finding(FindingType::RedundantSql, Severity::Warning),
make_finding(FindingType::SlowSql, Severity::Warning),
];
findings[0].pattern.template = "T1".to_string();
findings[1].pattern.template = "T2".to_string();
findings[2].pattern.template = "T3".to_string();
enrich_with_signatures(&mut findings);
let target_sig = findings[1].signature.clone();
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack(&target_sig, None)],
};
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(report.findings.len(), 2);
assert_eq!(report.acknowledged_findings.len(), 1);
assert_eq!(
report.acknowledged_findings[0].finding.signature,
target_sig
);
}
#[test]
fn apply_to_report_no_match_keeps_all() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack(
"n_plus_one_sql:nope:nope:00000000000000000000000000000000",
None,
)],
};
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(report.findings.len(), 1);
assert!(report.acknowledged_findings.is_empty());
}
#[test]
fn apply_to_report_expired_ack_ignored() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let target_sig = findings[0].signature.clone();
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack(&target_sig, Some("2020-01-01"))],
};
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(report.findings.len(), 1);
assert!(report.acknowledged_findings.is_empty());
}
#[test]
fn apply_to_report_reports_an_ack_that_matched_nothing() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack("deadbeef", None)],
};
apply_to_report(
&mut report,
&acks,
&Config::default(),
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(report.findings.len(), 1, "the unrelated finding survives");
let unmatched: Vec<&Warning> = report
.warning_details
.iter()
.filter(|w| w.kind == warnings::UNMATCHED_ACKNOWLEDGMENT)
.collect();
assert_eq!(unmatched.len(), 1);
assert!(
unmatched[0].message.contains("deadbeef"),
"the warning must name the entry to remove, got: {}",
unmatched[0].message
);
}
#[test]
fn apply_to_report_precomputed_origin_emits_no_unmatched_warning() {
let mut report = empty_report(vec![]);
report.per_endpoint_io_ops = vec![crate::report::PerEndpointIoOps {
service: "order-service".to_string(),
endpoint: "GET /api/orders".to_string(),
io_ops: 12,
}];
let acks = AcknowledgmentsFile {
acknowledged: vec![Acknowledgment {
service: Some("order-service".to_string()),
source_endpoint: Some("GET /api/orders".to_string()),
..ack("deadbeef", None)
}],
};
apply_to_report(
&mut report,
&acks,
&Config::default(),
now_2026_05_02(),
ReportOrigin::Precomputed,
);
assert!(
!report
.warning_details
.iter()
.any(|w| w.kind == warnings::UNMATCHED_ACKNOWLEDGMENT),
"a precomputed report must not claim anything, got: {:?}",
report.warning_details
);
}
#[test]
fn apply_to_report_unmatched_ack_splits_fixed_from_not_run() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let mut report = empty_report(findings);
report.per_endpoint_io_ops = vec![crate::report::PerEndpointIoOps {
service: "order-service".to_string(),
endpoint: "GET /api/orders".to_string(),
io_ops: 12,
}];
let located = |sig: &str, endpoint: &str| Acknowledgment {
service: Some("order-service".to_string()),
source_endpoint: Some(endpoint.to_string()),
..ack(sig, None)
};
let acks = AcknowledgmentsFile {
acknowledged: vec![
located("aaaa-exercised", "GET /api/orders"),
located("bbbb-not-run", "GET /api/legacy/export"),
],
};
apply_to_report(
&mut report,
&acks,
&Config::default(),
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
let messages: Vec<&str> = report
.warning_details
.iter()
.filter(|w| w.kind == warnings::UNMATCHED_ACKNOWLEDGMENT)
.map(|w| w.message.as_str())
.collect();
assert_eq!(messages.len(), 2);
assert!(
messages[0].contains("aaaa-exercised") && messages[0].contains("looks fixed"),
"exercised endpoint must read as fixed, got: {}",
messages[0]
);
assert!(
messages[1].contains("bbbb-not-run") && messages[1].contains("proves nothing"),
"absent endpoint must prove nothing, got: {}",
messages[1]
);
}
#[test]
fn apply_to_report_does_not_report_matched_or_expired_acks() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let target_sig = findings[0].signature.clone();
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![
ack(&target_sig, None),
ack("expired-and-unmatched", Some("2020-01-01")),
],
};
apply_to_report(
&mut report,
&acks,
&Config::default(),
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(report.acknowledged_findings.len(), 1);
assert!(
!report
.warning_details
.iter()
.any(|w| w.kind == warnings::UNMATCHED_ACKNOWLEDGMENT),
"got: {:?}",
report.warning_details
);
}
#[test]
fn apply_to_report_does_not_accumulate_unmatched_warnings() {
let mut report = empty_report(vec![]);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack("deadbeef", None)],
};
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(
report
.warning_details
.iter()
.filter(|w| w.kind == warnings::UNMATCHED_ACKNOWLEDGMENT)
.count(),
1
);
}
#[test]
fn apply_to_report_future_ack_applied() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let target_sig = findings[0].signature.clone();
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack(&target_sig, Some("2030-01-01"))],
};
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert!(report.findings.is_empty());
assert_eq!(report.acknowledged_findings.len(), 1);
}
#[test]
fn apply_to_report_no_expires_at_permanent() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Warning)];
enrich_with_signatures(&mut findings);
let target_sig = findings[0].signature.clone();
let mut report = empty_report(findings);
let acks = AcknowledgmentsFile {
acknowledged: vec![ack(&target_sig, None)],
};
let config = Config::default();
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert_eq!(report.acknowledged_findings.len(), 1);
}
#[test]
fn apply_to_report_reevaluates_quality_gate() {
let mut findings = vec![make_finding(FindingType::NPlusOneSql, Severity::Critical)];
enrich_with_signatures(&mut findings);
let target_sig = findings[0].signature.clone();
let config = Config::default();
let pre_gate = quality_gate::evaluate(
&findings,
&GreenSummary::disabled(0),
&config.thresholds,
None,
);
assert!(!pre_gate.passed, "baseline gate must fail before ack");
let mut report = empty_report(findings);
report.quality_gate = pre_gate;
let acks = AcknowledgmentsFile {
acknowledged: vec![ack(&target_sig, None)],
};
apply_to_report(
&mut report,
&acks,
&config,
now_2026_05_02(),
ReportOrigin::FreshAnalysis,
);
assert!(
report.quality_gate.passed,
"gate must flip green after the offending finding is acked"
);
}
#[test]
fn enrich_with_signatures_overwrites() {
let mut findings = vec![
make_finding(FindingType::NPlusOneSql, Severity::Warning),
make_finding(FindingType::RedundantSql, Severity::Warning),
];
findings[0].signature = "stale".to_string();
findings[1].signature = "also-stale".to_string();
enrich_with_signatures(&mut findings);
assert_ne!(findings[0].signature, "stale");
assert_ne!(findings[1].signature, "also-stale");
assert!(!findings[0].signature.is_empty());
assert!(!findings[1].signature.is_empty());
}
}