use super::*;
use crate::{
integrations::{
analyze_client::{AnalyzeClientError, AnalyzeHealthResponse, ComplexityHotspot, Smell},
search_client::{
EmbedderState, HealthResponse, IndexInfo, SearchClientError, SearchResult,
},
},
llm::{LlmError, LlmProvider, LlmRequest, LlmResponse},
models::ReviewStatus,
};
use async_trait::async_trait;
use std::path::PathBuf;
struct FakeLlm {
response: String,
error: Option<String>,
output_tokens: Option<u32>,
}
impl FakeLlm {
fn approves() -> Self {
Self {
response: r#"Looks good.
```json
{"verdict":"APPROVE","summary":"LGTM","findings":[]}
```"#
.to_string(),
error: None,
output_tokens: None,
}
}
fn truncated_at_ceiling() -> Self {
Self {
response: r#"```json
{"verdict":"APPROVE","summary":"looks fine","findings":[]}
```"#
.to_string(),
error: None,
output_tokens: Some(4096),
}
}
fn request_changes() -> Self {
Self {
response: r#"There is a bug.
```json
{"verdict":"REQUEST_CHANGES","summary":"SQL injection","findings":[{"title":"SQL injection","body":"line 42","severity":"medium","confidence":0.9,"file":"src/a.rs","line":42}]}
```"#
.to_string(),
error: None,
output_tokens: None,
}
}
fn errors(msg: impl Into<String>) -> Self {
Self {
response: String::new(),
error: Some(msg.into()),
output_tokens: None,
}
}
}
#[async_trait]
impl LlmProvider for FakeLlm {
fn name(&self) -> &str {
"fake"
}
async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
if let Some(ref err) = self.error {
return Err(LlmError::Transport(err.clone()));
}
Ok(LlmResponse {
text: self.response.clone(),
model: req.model.clone(),
input_tokens: 100,
output_tokens: self.output_tokens.unwrap_or(50),
latency_ms: 42,
cost_usd: 0.000042,
finish_reason: None,
})
}
}
struct FakeVerifier {
judgment: &'static str,
}
#[async_trait]
impl LlmProvider for FakeVerifier {
fn name(&self) -> &str {
"fake-verifier"
}
async fn complete(&self, req: LlmRequest) -> Result<LlmResponse, LlmError> {
Ok(LlmResponse {
text: format!(r#"{{"judgment":"{}","reason":"test"}}"#, self.judgment),
model: req.model.clone(),
input_tokens: 5,
output_tokens: 3,
latency_ms: 1,
cost_usd: 0.0,
finish_reason: None,
})
}
}
struct FakeSearch;
#[async_trait]
impl SearchClient for FakeSearch {
async fn health(&self) -> Result<HealthResponse, SearchClientError> {
Ok(HealthResponse {
status: "ok".to_string(),
embedder: EmbedderState::Bool(true),
})
}
async fn list_indexes(&self) -> Result<Vec<IndexInfo>, SearchClientError> {
Ok(vec![IndexInfo {
id: "main".to_string(),
name: None,
root_path: None,
}])
}
async fn search(
&self,
_index_id: &str,
_query: &str,
_top_k: Option<u32>,
) -> Result<Vec<SearchResult>, SearchClientError> {
Ok(vec![SearchResult {
file: "src/auth.rs".to_string(),
snippet: Some("pub fn authenticate() {}".to_string()),
score: 0.9,
start_line: None,
end_line: None,
}])
}
}
struct FailingSearch;
#[async_trait]
impl SearchClient for FailingSearch {
async fn health(&self) -> Result<HealthResponse, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
async fn list_indexes(&self) -> Result<Vec<IndexInfo>, SearchClientError> {
Err(SearchClientError::Unavailable("down".to_string()))
}
async fn search(
&self,
_: &str,
_: &str,
_: Option<u32>,
) -> Result<Vec<SearchResult>, SearchClientError> {
Err(SearchClientError::Transport("refused".to_string()))
}
}
struct FakeAnalyze;
#[async_trait]
impl AnalyzeClient for FakeAnalyze {
async fn health(&self) -> Result<AnalyzeHealthResponse, AnalyzeClientError> {
Err(AnalyzeClientError::Unavailable("not running".to_string()))
}
async fn has_analysis(&self, _: &str) -> bool {
false
}
async fn complexity_hotspots(
&self,
_: &str,
_: Option<u32>,
) -> Result<Vec<ComplexityHotspot>, AnalyzeClientError> {
Ok(vec![])
}
async fn smells(&self, _: &str) -> Result<Vec<Smell>, AnalyzeClientError> {
Ok(vec![])
}
}
struct ReadyAnalyze;
#[async_trait]
impl AnalyzeClient for ReadyAnalyze {
async fn health(&self) -> Result<AnalyzeHealthResponse, AnalyzeClientError> {
Ok(AnalyzeHealthResponse {
status: "ok".to_string(),
search_reachable: true,
})
}
async fn has_analysis(&self, _: &str) -> bool {
true
}
async fn complexity_hotspots(
&self,
_: &str,
_: Option<u32>,
) -> Result<Vec<ComplexityHotspot>, AnalyzeClientError> {
Ok(vec![])
}
async fn smells(&self, _: &str) -> Result<Vec<Smell>, AnalyzeClientError> {
Ok(vec![])
}
}
fn ready_deps(llm: Arc<dyn LlmProvider>, verifier: Option<Arc<dyn LlmProvider>>) -> ReviewDeps {
ReviewDeps {
llm,
verifier,
search: Arc::new(FakeSearch),
analyze: Some(Arc::new(ReadyAnalyze)),
dedup: None,
}
}
fn local_diff_source(diff: &str) -> (DiffSource, tempfile::NamedTempFile) {
use std::io::Write as _;
let mut tmp = tempfile::NamedTempFile::new().expect("tempfile");
tmp.write_all(diff.as_bytes()).expect("write");
let path = tmp.path().to_path_buf();
(DiffSource::LocalFile { path }, tmp)
}
fn default_config() -> ReviewConfig {
ReviewConfig::load(None)
}
#[tokio::test]
async fn run_review_with_fake_provider_approves() {
let diff = "+fn hello() { println!(\"hi\"); }\n";
let (source, _tmp) = local_diff_source(diff);
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::approves()), None);
let result = run_review(&config, input, deps).await;
assert_eq!(result.verdict, Verdict::Approve);
assert!(
result.error.is_none(),
"no error expected: {:?}",
result.error
);
assert_eq!(
result.status,
ReviewStatus::Completed,
"both deps healthy → authoritative Completed status"
);
assert!(result.dry_run, "MVP must always be dry-run");
assert_eq!(result.findings.len(), 0);
}
#[tokio::test]
async fn run_review_request_changes_parsed_correctly() {
let (source, _tmp) = local_diff_source(
"+fn bad_query(id: &str) { db.exec(format!(\"SELECT * FROM users WHERE id={id}\")) }\n",
);
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::request_changes()), None);
let result = run_review(&config, input, deps).await;
assert_eq!(result.verdict, Verdict::RequestChanges);
assert_eq!(result.findings.len(), 1);
assert_eq!(result.findings[0].kind, "SQL injection");
}
#[tokio::test]
async fn run_review_fail_safe_on_llm_error() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::errors("simulated transport error")), None);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::Unknown,
"LLM error must fail CLOSED to UNKNOWN, never silently APPROVE (#1241)"
);
assert!(
result.error.is_some(),
"error field must be set when LLM fails"
);
}
#[tokio::test]
async fn run_review_truncated_output_is_unknown() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::truncated_at_ceiling()), None);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::Unknown,
"output at the token ceiling must fail CLOSED to UNKNOWN (#1241)"
);
let err = result
.error
.expect("truncation must set an actionable error");
assert!(
err.contains("truncat"),
"error must explain the truncation: {err}"
);
}
#[test]
fn is_truncated_finish_reason_length_true() {
assert!(
is_truncated(Some("length"), 10, 4096),
"finish_reason=length is truncated even well under the ceiling"
);
assert!(
is_truncated(Some("max_tokens"), 10, 4096),
"finish_reason=max_tokens (Bedrock) is truncated"
);
assert!(is_truncated(Some(" LENGTH "), 10, 4096));
}
#[test]
fn is_truncated_finish_reason_stop_at_high_ratio_false() {
assert!(
!is_truncated(Some("stop"), 4096, 4096),
"finish_reason=stop at 100% of ceiling is NOT truncated (#1357 false-positive fix)"
);
assert!(
!is_truncated(Some("end_turn"), 4090, 4096),
"finish_reason=end_turn (Bedrock natural stop) near the ceiling is NOT truncated"
);
}
#[test]
fn is_truncated_ratio_fallback_at_ceiling_true() {
assert!(
is_truncated(None, 4096, 4096),
"no finish_reason, exactly at ceiling → truncated"
);
assert!(
is_truncated(None, 3892, 4096),
"no finish_reason, at the 95% threshold → truncated"
);
assert!(
is_truncated(Some(""), 4096, 4096),
"empty reason falls back to ratio"
);
}
#[test]
fn is_truncated_ratio_fallback_well_under_false() {
assert!(
!is_truncated(None, 3891, 4096),
"no finish_reason, one below the 95% threshold → NOT truncated"
);
assert!(
!is_truncated(None, 50, 4096),
"no finish_reason, a short response → NOT truncated"
);
}
#[test]
fn is_truncated_unset_ceiling_false() {
assert!(
!is_truncated(None, 10_000, 0),
"unknown ceiling (0) disables the fallback truncation check"
);
}
static RATIO_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn truncation_ratio_env_override_applies() {
let _guard = RATIO_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var(TRUNCATION_TOKEN_RATIO_ENV).ok();
unsafe { std::env::set_var(TRUNCATION_TOKEN_RATIO_ENV, "0.50") };
assert!(
(truncation_token_ratio() - 0.50).abs() < f64::EPSILON,
"env override should set the ratio to 0.50"
);
assert!(
is_truncated(None, 2048, 4096),
"with ratio 0.50, 50% of ceiling is truncated on the fallback path"
);
match prev {
Some(v) => unsafe { std::env::set_var(TRUNCATION_TOKEN_RATIO_ENV, v) },
None => unsafe { std::env::remove_var(TRUNCATION_TOKEN_RATIO_ENV) },
}
}
#[test]
fn truncation_ratio_env_invalid_falls_back() {
let _guard = RATIO_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var(TRUNCATION_TOKEN_RATIO_ENV).ok();
unsafe { std::env::set_var(TRUNCATION_TOKEN_RATIO_ENV, "2.0") };
assert!(
(truncation_token_ratio() - DEFAULT_TRUNCATION_TOKEN_RATIO).abs() < f64::EPSILON,
"out-of-range override (>1.0) must fall back to the default"
);
unsafe { std::env::set_var(TRUNCATION_TOKEN_RATIO_ENV, "not-a-number") };
assert!(
(truncation_token_ratio() - DEFAULT_TRUNCATION_TOKEN_RATIO).abs() < f64::EPSILON,
"unparseable override must fall back to the default"
);
match prev {
Some(v) => unsafe { std::env::set_var(TRUNCATION_TOKEN_RATIO_ENV, v) },
None => unsafe { std::env::remove_var(TRUNCATION_TOKEN_RATIO_ENV) },
}
}
#[tokio::test]
async fn run_review_search_down_skips_when_required() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let config = default_config(); let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ReviewDeps {
llm: Arc::new(FakeLlm::approves()), verifier: None,
search: Arc::new(FailingSearch), analyze: Some(Arc::new(ReadyAnalyze)),
dedup: None,
};
let result = run_review(&config, input, deps).await;
assert_eq!(
result.status,
ReviewStatus::Skipped,
"search down + required must SKIP, not silently APPROVE"
);
assert!(!result.posted, "a skipped review must never be posted live");
assert!(result.dry_run, "a skipped review is dry-run");
let err = result.error.expect("skip must set an actionable error");
assert!(
err.contains("trusty-search"),
"error must name the dep: {err}"
);
assert!(
err.contains("start"),
"error must be actionable (how to fix): {err}"
);
assert_ne!(
result.verdict,
Verdict::Approve,
"a skip must not masquerade as APPROVE"
);
}
#[tokio::test]
async fn run_review_analyze_down_skips_when_required() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let config = default_config(); let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ReviewDeps {
llm: Arc::new(FakeLlm::approves()),
verifier: None,
search: Arc::new(FakeSearch), analyze: Some(Arc::new(FakeAnalyze)), dedup: None,
};
let result = run_review(&config, input, deps).await;
assert_eq!(
result.status,
ReviewStatus::Skipped,
"analyze down + required must SKIP"
);
let err = result.error.expect("skip must set an actionable error");
assert!(
err.contains("trusty-analyze"),
"error must name the dep: {err}"
);
}
#[tokio::test]
async fn run_review_search_down_degraded_when_optout() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let mut config = default_config();
config.context.require_search = false; let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ReviewDeps {
llm: Arc::new(FakeLlm::approves()),
verifier: None,
search: Arc::new(FailingSearch), analyze: Some(Arc::new(ReadyAnalyze)),
dedup: None,
};
let result = run_review(&config, input, deps).await;
assert_eq!(
result.status,
ReviewStatus::Degraded,
"opted-out + search down must PROCEED but be tagged Degraded"
);
assert!(
!result.status.is_authoritative(),
"a degraded review must not be authoritative"
);
assert!(
result.review_body.contains("NOT AUTHORITATIVE"),
"degraded body must carry a loud banner: {:?}",
result.review_body
);
let err = result
.error
.expect("degraded run must record a non-authoritative reason");
assert!(err.contains("degraded"), "reason must say degraded: {err}");
}
#[tokio::test]
async fn run_review_both_healthy_completes_authoritative() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::approves()), None);
let result = run_review(&config, input, deps).await;
assert_eq!(result.status, ReviewStatus::Completed);
assert!(result.status.is_authoritative());
assert_eq!(result.verdict, Verdict::Approve);
assert!(
result.error.is_none(),
"healthy run sets no error: {:?}",
result.error
);
assert!(
!result.review_body.contains("NOT AUTHORITATIVE"),
"authoritative review must not carry the degraded banner"
);
}
#[tokio::test]
async fn run_review_local_diff_skips_github() {
let diff = "+fn local_fn() {}\n";
let (source, _tmp) = local_diff_source(diff);
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-nano-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::approves()), None);
let result = run_review(&config, input, deps).await;
assert_eq!(result.owner, "local");
assert_eq!(result.verdict, Verdict::Approve);
}
#[tokio::test]
async fn run_review_missing_diff_file_sets_error() {
let config = default_config();
let input = ReviewInput {
diff_source: DiffSource::LocalFile {
path: PathBuf::from("/nonexistent/path/nope.diff"),
},
reviewer_model: "openai/gpt-5.4-nano-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ReviewDeps {
llm: Arc::new(FakeLlm::approves()),
verifier: None,
search: Arc::new(FakeSearch),
analyze: None,
dedup: None,
};
let result = run_review(&config, input, deps).await;
assert!(
result.error.is_some(),
"missing diff file must set error field"
);
}
#[tokio::test]
async fn run_review_local_diff_is_dry_run_and_not_posted() {
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let mut config = default_config();
config.dry_run = false; let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-nano-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::ForceLive, run_mode: RunMode::Serve,
allow_posting: true,
};
let deps = ready_deps(Arc::new(FakeLlm::approves()), None);
let result = run_review(&config, input, deps).await;
assert!(
result.dry_run,
"local-diff source must never post — always dry-run"
);
assert!(!result.posted, "local-diff must not be marked posted");
}
#[tokio::test]
async fn run_review_writes_dry_run_log_on_log_only_path() {
let dir = tempfile::tempdir().expect("tempdir");
let (source, _tmp) = local_diff_source("+fn x() {}\n");
let mut config = default_config();
config.log_dir = dir.path().to_path_buf();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-nano-20260317".to_string(),
write_log: true,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(Arc::new(FakeLlm::approves()), None);
let _result = run_review(&config, input, deps).await;
let json_count = std::fs::read_dir(dir.path())
.expect("read_dir")
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "json").unwrap_or(false))
.count();
assert_eq!(json_count, 1, "a dry-run JSON log must be written");
}
#[tokio::test]
async fn run_review_verification_refutes_and_relaxes_verdict() {
let (source, _tmp) = local_diff_source("+fn bad() {}\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(
Arc::new(FakeLlm::request_changes()), Some(Arc::new(FakeVerifier {
judgment: "REFUTED",
})),
);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::Approve,
"refuting the sole finding must relax REQUEST_CHANGES to APPROVE"
);
assert_eq!(
result.findings.len(),
1,
"the finding is demoted, not dropped"
);
}
#[tokio::test]
async fn run_review_verification_confirms_and_preserves_verdict() {
let (source, _tmp) = local_diff_source("+fn bad() {}\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(
Arc::new(FakeLlm::request_changes()),
Some(Arc::new(FakeVerifier {
judgment: "CONFIRMED",
})),
);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::ApproveWithReservations,
"confirmed Medium → APPROVE* (path a2 — #1015); not REQUEST_CHANGES"
);
}
#[tokio::test]
async fn run_review_verification_disabled_skips_round() {
let (source, _tmp) = local_diff_source("+fn bad() {}\n");
let mut config = default_config();
config.verification.enabled = false; let input = ReviewInput {
diff_source: source,
reviewer_model: "openai/gpt-5.4-mini-20260317".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(
Arc::new(FakeLlm::request_changes()),
Some(Arc::new(FakeVerifier {
judgment: "REFUTED",
})),
);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::RequestChanges,
"with verification disabled the verdict must remain REQUEST_CHANGES"
);
assert!(
result.findings[0].verified.is_none(),
"disabled verification must not mark any finding"
);
}
#[tokio::test]
#[ignore = "requires a live GitHub PR + credentials"]
async fn run_review_live_post_and_dedup_skip_integration() {
}
#[tokio::test]
async fn envelope_grade_tracks_verdict_after_verification_relaxation_1486() {
let llm_response = r#"Code looks good overall, minor concern.
```json
{"verdict":"APPROVE","grade":"B-","summary":"Looks solid","findings":[{"title":"Potential XSS","body":"line 5 unescaped","severity":"high","confidence":0.95,"file":"src/render.rs","line":5}]}
```"#;
let (source, _tmp) = local_diff_source("+fn render(s: &str) { println!(\"{s}\"); }\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "fake-model".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(
Arc::new(FakeLlm {
response: llm_response.to_string(),
error: None,
output_tokens: None,
}),
Some(Arc::new(FakeVerifier {
judgment: "REFUTED",
})),
);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::Approve,
"#1486: verification refutes the only blocking finding → verdict must be APPROVE (got {:?})",
result.verdict,
);
let grade = result.grade.as_deref().unwrap_or("(none)");
assert_eq!(
grade, "B-",
"#1486: envelope grade must be the original LLM grade B- after verification \
relaxes the verdict to APPROVE (before fix, it was F)"
);
assert_eq!(result.findings.len(), 1, "finding must be preserved");
assert!(
matches!(
result.findings[0].verified,
Some(crate::models::VerifyOutcome::Refuted)
),
"the High-effort finding must be marked Refuted"
);
}
#[tokio::test]
async fn envelope_grade_stays_block_when_high_effort_confirmed_1486() {
let llm_response = r#"Review with confirmed critical finding.
```json
{"verdict":"APPROVE","grade":"B-","summary":"Mostly OK","findings":[{"title":"Auth bypass","body":"line 10","severity":"high","confidence":0.95,"file":"src/auth.rs","line":10}]}
```"#;
let (source, _tmp) = local_diff_source("+fn auth(t: &str) {}\n");
let config = default_config();
let input = ReviewInput {
diff_source: source,
reviewer_model: "fake-model".to_string(),
write_log: false,
print_result: false,
trigger: TriggerDecision::None,
run_mode: RunMode::Cli,
allow_posting: false,
};
let deps = ready_deps(
Arc::new(FakeLlm {
response: llm_response.to_string(),
error: None,
output_tokens: None,
}),
Some(Arc::new(FakeVerifier {
judgment: "CONFIRMED",
})),
);
let result = run_review(&config, input, deps).await;
assert_eq!(
result.verdict,
Verdict::Block,
"#1486 stable path: confirmed High-effort finding must keep verdict at BLOCK"
);
let grade = result.grade.as_deref().unwrap_or("(none)");
assert_eq!(
grade, "F",
"#1486 stable path: confirmed BLOCK must clamp B- → F (consistent with verdict)"
);
}
#[test]
fn attach_inline_comments_maps_on_diff() {
use crate::models::{Effort, Finding};
use crate::pipeline::runner_helpers::attach_inline_comments;
let raw_diff = "\
diff --git a/src/db.rs b/src/db.rs
--- a/src/db.rs
+++ b/src/db.rs
@@ -1,1 +1,2 @@
fn a() {}
+fn b() {}
";
let mut result = crate::models::ReviewResult::new("o", "r", 1, "t", "u");
let mut on_diff = Finding::new("src/db.rs", "bug", "desc", "fix", 0.9, Effort::Medium);
on_diff.line = Some(2); let mut off_diff = Finding::new("src/db.rs", "bug2", "desc2", "fix2", 0.9, Effort::Medium);
off_diff.line = Some(999);
result.findings = vec![on_diff, off_diff];
attach_inline_comments(&mut result, raw_diff);
assert_eq!(
result.inline_comments.len(),
1,
"only the on-diff finding becomes an inline comment"
);
assert_eq!(result.inline_comments[0].path, "src/db.rs");
assert_eq!(result.inline_comments[0].line, 2);
}