use super::algorithmic::{AlgorithmicKind, AlgorithmicSignal};
use super::behavioral::{BehavioralKind, BehavioralSignal, BehavioralSignalSource};
use super::taint::{SinkKind, SourceKind, TaintSignal};
use super::tiered::{ConfidenceTier, build_tiered};
use super::{BoundaryCategory, BoundarySignal};
use crate::review::diff::{ChangeStatus, ChangedFile, ChangedRange};
use std::path::PathBuf;
fn boundary(category: BoundaryCategory, path: &str) -> BoundarySignal {
BoundarySignal {
category,
path: path.to_string(),
status: ChangeStatus::Modified,
blast_radius: 0,
}
}
fn behavioral(kind: BehavioralKind, path: &str) -> BehavioralSignal {
BehavioralSignal {
kind,
path: path.to_string(),
line: 1,
detail: "detail".to_string(),
source: BehavioralSignalSource::Ast,
}
}
fn algorithmic(kind: AlgorithmicKind, path: &str) -> AlgorithmicSignal {
AlgorithmicSignal {
kind,
path: path.to_string(),
line: 1,
detail: "detail".to_string(),
}
}
fn taint(sink: SinkKind, path: &str) -> TaintSignal {
TaintSignal {
source: SourceKind::HttpRequest,
sink,
path: path.to_string(),
line: 1,
detail: "detail".to_string(),
}
}
fn large_diff(count: usize) -> Vec<ChangedFile> {
(0..count)
.map(|index| ChangedFile {
path: PathBuf::from(format!("src/f{index}.rs")),
status: ChangeStatus::Modified,
ranges: vec![ChangedRange { start: 1, end: 40 }],
hunks: Vec::new(),
})
.collect()
}
#[test]
fn boundary_signals_land_in_definitely() {
let tiered = build_tiered(
&[boundary(BoundaryCategory::AccessControl, "src/auth.ts")],
&[],
&[],
&[],
&[],
);
assert_eq!(tiered.definitely.len(), 1);
assert!(tiered.maybe.is_empty());
assert!(tiered.noise.is_empty());
}
#[test]
fn behavioral_kinds_tier_by_sensitivity() {
let signals = [
behavioral(BehavioralKind::EnvVarIntroduced, "src/config.ts"),
behavioral(BehavioralKind::NetworkCallAdded, "src/api.ts"),
];
let tiered = build_tiered(&[], &signals, &[], &[], &[]);
assert_eq!(tiered.definitely.len(), 1);
assert_eq!(tiered.maybe.len(), 1);
}
#[test]
fn coarse_behavioral_signals_are_demoted_to_noise() {
let coarse = BehavioralSignal {
kind: BehavioralKind::AuthCheckRemoved,
path: "src/legacy.php".to_string(),
line: 1,
detail: "Authentication/authorization check removed (coarse fallback)".to_string(),
source: BehavioralSignalSource::CoarseFallback,
};
let tiered = build_tiered(&[], &[coarse], &[], &[], &[]);
assert!(tiered.definitely.is_empty());
assert!(tiered.maybe.is_empty());
assert_eq!(tiered.noise.len(), 1);
assert_eq!(tiered.noise[0].tier, ConfidenceTier::LargeDiffOrNoise);
}
#[test]
fn same_kind_is_definitely_when_ast_sourced() {
let ast = BehavioralSignal {
kind: BehavioralKind::AuthCheckRemoved,
path: "src/auth.ts".to_string(),
line: 1,
detail: "Authentication/authorization check removed (coarse fallback)".to_string(),
source: BehavioralSignalSource::Ast,
};
let tiered = build_tiered(&[], &[ast], &[], &[], &[]);
assert_eq!(tiered.definitely.len(), 1);
assert!(tiered.noise.is_empty());
}
#[test]
fn network_call_in_an_auth_path_is_escalated() {
let tiered = build_tiered(
&[boundary(BoundaryCategory::AccessControl, "src/auth.ts")],
&[behavioral(BehavioralKind::NetworkCallAdded, "src/auth.ts")],
&[],
&[],
&[],
);
assert_eq!(tiered.definitely.len(), 2);
assert!(tiered.maybe.is_empty());
}
#[test]
fn algorithmic_signals_are_maybe() {
let tiered = build_tiered(
&[],
&[],
&[algorithmic(
AlgorithmicKind::NestedLoopIntroduced,
"src/x.rs",
)],
&[],
&[],
);
assert_eq!(tiered.maybe.len(), 1);
assert!(tiered.definitely.is_empty());
}
#[test]
fn taint_tiers_by_sink_severity() {
let tiered = build_tiered(
&[],
&[],
&[],
&[
taint(SinkKind::Sql, "src/db.ts"),
taint(SinkKind::FsWrite, "src/files.ts"),
],
&[],
);
assert_eq!(tiered.definitely.len(), 1);
assert_eq!(tiered.maybe.len(), 1);
}
#[test]
fn has_taint_signal_detects_taint_family_only() {
let with_taint = build_tiered(&[], &[], &[], &[taint(SinkKind::Sql, "src/db.ts")], &[]);
assert!(with_taint.has_taint_signal());
let without_taint = build_tiered(
&[boundary(BoundaryCategory::AccessControl, "src/auth.ts")],
&[behavioral(BehavioralKind::FsWriteAdded, "src/f0.rs")],
&[],
&[],
&[],
);
assert!(!without_taint.has_taint_signal());
}
#[test]
fn noise_tier_fires_only_for_a_large_diff_with_nothing_flagged() {
let tiered = build_tiered(&[], &[], &[], &[], &large_diff(6));
assert_eq!(tiered.noise.len(), 1);
assert_eq!(tiered.noise[0].tier, ConfidenceTier::LargeDiffOrNoise);
}
#[test]
fn noise_tier_is_suppressed_when_a_signal_is_present() {
let tiered = build_tiered(
&[],
&[behavioral(BehavioralKind::FsWriteAdded, "src/f0.rs")],
&[],
&[],
&large_diff(6),
);
assert!(tiered.noise.is_empty());
assert_eq!(tiered.maybe.len(), 1);
}
#[test]
fn small_diff_with_nothing_flagged_is_silent() {
let tiered = build_tiered(&[], &[], &[], &[], &large_diff(2));
assert!(tiered.is_empty());
}
#[test]
fn definitely_sensitive_signals_include_verification_plan() {
let tiered = build_tiered(
&[boundary(BoundaryCategory::AccessControl, "src/auth.ts")],
&[],
&[],
&[],
&[],
);
let plan = tiered.definitely[0]
.verification_plan
.as_ref()
.expect("definitely sensitive review signals should include a verification plan");
assert!(plan.steps.len() >= 3);
assert!(plan.steps[0].contains("src/auth.ts"));
assert!(
plan.steps
.iter()
.any(|step| step.contains("static review evidence only"))
);
}
#[test]
fn maybe_and_noise_signals_do_not_get_verification_plans() {
let maybe = build_tiered(
&[],
&[behavioral(BehavioralKind::NetworkCallAdded, "src/api.ts")],
&[],
&[],
&[],
);
assert!(maybe.definitely.is_empty());
assert!(maybe.maybe[0].verification_plan.is_none());
let noise = build_tiered(&[], &[], &[], &[], &large_diff(6));
assert!(noise.noise[0].verification_plan.is_none());
}
#[test]
fn review_signal_verification_plans_are_deterministic() {
let left = build_tiered(
&[],
&[behavioral(
BehavioralKind::EnvVarIntroduced,
"src/config.ts",
)],
&[],
&[],
&[],
);
let right = build_tiered(
&[],
&[behavioral(
BehavioralKind::EnvVarIntroduced,
"src/config.ts",
)],
&[],
&[],
&[],
);
assert_eq!(
left.definitely[0].verification_plan,
right.definitely[0].verification_plan
);
}
#[test]
fn review_signal_verification_plan_serializes_as_stable_steps_array() {
let tiered = build_tiered(
&[boundary(BoundaryCategory::AccessControl, "src/auth.ts")],
&[],
&[],
&[],
&[],
);
let value = serde_json::to_value(&tiered.definitely[0])
.expect("review signals should serialize to JSON");
let steps = value
.get("verification_plan")
.and_then(|plan| plan.get("steps"))
.and_then(serde_json::Value::as_array)
.expect("verification_plan.steps should be a stable JSON array");
assert_eq!(steps.len(), 3);
assert_eq!(
steps[0].as_str(),
Some(
"Open src/auth.ts and confirm the changed diff still supports the review signal: access control changed."
)
);
assert!(steps.iter().any(|step| {
step.as_str()
.is_some_and(|step| step.contains("static review evidence only"))
}));
}
#[test]
fn maybe_signal_omits_verification_plan_in_json_contract() {
let tiered = build_tiered(
&[],
&[behavioral(BehavioralKind::NetworkCallAdded, "src/api.ts")],
&[],
&[],
&[],
);
let value =
serde_json::to_value(&tiered.maybe[0]).expect("review signals should serialize to JSON");
assert!(value.get("verification_plan").is_none());
}