use super::*;
fn sample_meta() -> ReviewPrMeta {
ReviewPrMeta {
title: "Add authentication".to_string(),
body: String::new(),
author: "alice".to_string(),
url: "https://github.com/acme/backend/pull/42".to_string(),
}
}
fn empty_context() -> ReviewContext {
ReviewContext::default()
}
#[test]
fn system_prompt_contains_policy() {
let prompt = reviewer_system_prompt();
assert!(
prompt.contains("default verdict is APPROVE"),
"system prompt must state APPROVE-default policy"
);
assert!(
prompt.contains("REQUEST_CHANGES requires ALL THREE"),
"system prompt must specify the REQUEST_CHANGES gate"
);
assert!(
prompt.contains("BLOCK"),
"system prompt must describe the BLOCK tier"
);
assert!(
prompt.contains("verdict"),
"system prompt must mention the verdict field"
);
}
#[test]
fn system_prompt_contains_severity_anchors() {
let prompt = reviewer_system_prompt();
assert!(
prompt.contains("critical"),
"system prompt must define the 'critical' severity anchor"
);
assert!(
prompt.contains("severity=critical"),
"system prompt must instruct model to assign severity=critical for BLOCK issues"
);
assert!(
prompt.contains("Compile-break rule"),
"system prompt must contain the compile-break BLOCK rule"
);
assert!(
prompt.contains("under-rate"),
"system prompt must warn against under-rating blocking issues"
);
}
#[test]
fn system_prompt_contains_compile_break_rule() {
let prompt = reviewer_system_prompt();
assert!(
prompt.contains("REMOVES a symbol"),
"system prompt must describe the removed-symbol compile-break pattern"
);
assert!(
prompt.contains("compile-time regression"),
"system prompt must name it a compile-time regression"
);
}
#[test]
fn build_review_prompt_strips_bedrock_prefix() {
let req = build_review_prompt(
"acme",
"backend",
&sample_meta(),
"+fn x() {}",
&empty_context(),
"",
"bedrock/us.anthropic.claude-sonnet-4-6",
);
assert_eq!(
req.model, "us.anthropic.claude-sonnet-4-6",
"bedrock/ prefix must be stripped from LlmRequest.model"
);
}
#[test]
fn build_review_prompt_strips_openrouter_prefix() {
let req = build_review_prompt(
"acme",
"backend",
&sample_meta(),
"+fn x() {}",
&empty_context(),
"",
"openrouter/openai/gpt-5.4-mini-20260317",
);
assert_eq!(
req.model, "openai/gpt-5.4-mini-20260317",
"openrouter/ prefix must be stripped from LlmRequest.model"
);
}
#[test]
fn build_review_prompt_includes_diff() {
let diff = "+fn hello() { println!(\"hi\"); }\n";
let req = build_review_prompt(
"acme",
"backend",
&sample_meta(),
diff,
&empty_context(),
"",
"openai/gpt-5.4-mini-20260317",
);
assert_eq!(req.model, "openai/gpt-5.4-mini-20260317");
assert_eq!(req.messages.len(), 1);
let content = &req.messages[0].content;
assert!(
content.contains("fn hello"),
"user message must include the diff"
);
assert!(
content.contains("acme/backend"),
"user message must include owner/repo"
);
assert!(
content.contains("Add authentication"),
"user message must include PR title"
);
assert!((req.temperature - REVIEWER_TEMPERATURE).abs() < f32::EPSILON);
}
#[test]
fn prompt_includes_context_blocks() {
use crate::integrations::search_client::SearchResult;
let context = ReviewContext {
search_results: vec![SearchResult {
file: "src/auth.rs".to_string(),
snippet: Some("pub fn verify() {}".to_string()),
score: 0.9,
start_line: Some(10),
end_line: Some(12),
}],
complexity_hotspots: vec![ComplexityHotspot {
file: "src/auth.rs".to_string(),
function_name: Some("verify".to_string()),
cyclomatic: 12,
cognitive: 8,
}],
smells: vec![Smell {
file: "src/auth.rs".to_string(),
category: "long_method".to_string(),
severity: "medium".to_string(),
line: Some(20),
}],
apex_results: vec![],
};
let req = build_review_prompt(
"acme",
"repo",
&sample_meta(),
"+fn foo() {}",
&context,
"",
"openai/gpt-5.4-mini-20260317",
);
let content = &req.messages[0].content;
assert!(
content.contains("Related code"),
"user message must include search context section"
);
assert!(
content.contains("pub fn verify"),
"user message must include search snippet"
);
assert!(
content.contains("Complexity hotspots"),
"user message must include hotspot section"
);
assert!(
content.contains("Code smells"),
"user message must include smells section"
);
}
#[test]
fn prompt_includes_external_context() {
let external = "## Related JIRA tickets\n\n- **PROJ-1 — Add auth** — In Progress\n";
let req = build_review_prompt(
"acme",
"backend",
&sample_meta(),
"+fn x() {}",
&empty_context(),
external,
"openai/gpt-5.4-mini-20260317",
);
let content = &req.messages[0].content;
assert!(
content.contains("## Related JIRA tickets"),
"external context heading must be embedded"
);
assert!(
content.contains("PROJ-1 — Add auth"),
"external context bullet must be embedded"
);
let ext_pos = content.find("Related JIRA tickets").unwrap();
let instr_pos = content.find("populate the structured response").unwrap();
assert!(
ext_pos < instr_pos,
"external context must precede the closing instruction"
);
}
#[test]
fn prompt_empty_external_context_adds_nothing() {
let req = build_review_prompt(
"o",
"r",
&sample_meta(),
"+fn x() {}",
&empty_context(),
" \n",
"openai/gpt-5.4-mini-20260317",
);
let content = &req.messages[0].content;
assert!(!content.contains("Related JIRA"));
assert!(!content.contains("Related Confluence"));
assert!(!content.contains("Related GitHub issues"));
}
#[test]
fn prompt_empty_context_omits_sections() {
let req = build_review_prompt(
"o",
"r",
&sample_meta(),
"+fn x() {}",
&empty_context(),
"",
"openai/gpt-5.4-nano-20260317",
);
let content = &req.messages[0].content;
assert!(
!content.contains("Related code"),
"empty context must not include search section"
);
assert!(
!content.contains("Complexity hotspots"),
"empty context must not include hotspot section"
);
}
#[test]
fn build_review_prompt_includes_response_schema() {
let req = build_review_prompt(
"acme",
"backend",
&sample_meta(),
"+fn x() {}",
&empty_context(),
"",
"us.anthropic.claude-sonnet-4-6",
);
let schema = req
.response_schema
.expect("response_schema must be set on every review prompt");
assert_eq!(
schema.name, "review_output",
"schema name must be review_output"
);
assert!(schema.schema.is_object(), "schema must be a JSON object");
let props = &schema.schema["properties"];
assert!(
props["verdict"].is_object(),
"schema must have verdict property"
);
assert!(
props["findings"].is_object(),
"schema must have findings property"
);
}
#[test]
fn system_prompt_uses_structured_output_language() {
let prompt = reviewer_system_prompt();
assert!(
!prompt.contains("```json"),
"system prompt must not contain the old fenced JSON block instruction"
);
assert!(
prompt.contains("structured response"),
"system prompt must use structured-response language"
);
}
#[test]
fn prompt_local_diff_mode_no_pr_metadata() {
let meta = ReviewPrMeta::default();
let req = build_review_prompt(
"local",
"local",
&meta,
"+fn local_fn() {}",
&empty_context(),
"",
"openai/gpt-5.4-mini-20260317",
);
let content = &req.messages[0].content;
assert!(content.contains("local_fn"));
}
#[test]
fn review_output_schema_enum_matches_board_grades() {
let schema = review_response_schema();
let verdict_enum = &schema.schema["properties"]["verdict"]["enum"];
let values: Vec<&str> = verdict_enum
.as_array()
.expect("verdict enum must be an array")
.iter()
.map(|v| v.as_str().expect("enum value must be a string"))
.collect();
assert!(values.contains(&"APPROVE"), "schema must have APPROVE");
assert!(values.contains(&"APPROVE*"), "schema must have APPROVE*");
assert!(
values.contains(&"REQUEST_CHANGES"),
"schema must have REQUEST_CHANGES"
);
assert!(values.contains(&"BLOCK"), "schema must have BLOCK");
assert!(
values.contains(&"UNKNOWN"),
"schema must have UNKNOWN (not N/A)"
);
assert!(
!values.contains(&"N/A"),
"schema must NOT have N/A (not a board grade)"
);
assert_eq!(values.len(), 5, "schema must have exactly 5 board grades");
}
#[test]
fn system_prompt_describes_unknown_grade() {
let prompt = reviewer_system_prompt();
assert!(
prompt.contains("UNKNOWN"),
"system prompt must describe the UNKNOWN grade"
);
assert!(
!prompt.contains("N/A"),
"system prompt must not list N/A as a verdict option"
);
}
#[test]
fn prompt_includes_apex_context_when_present() {
use crate::integrations::apex_context::ApexContextResult;
let ctx = ReviewContext {
apex_results: vec![ApexContextResult {
file: "apex/auth-spec.md".to_string(),
snippet: "Token expiry must be checked.".to_string(),
score: 0.88,
start_line: Some(42),
}],
..Default::default()
};
let content = build_review_prompt(
"acme",
"backend",
&sample_meta(),
"+fn x() {}",
&ctx,
"",
"openai/gpt-5.4-mini-20260317",
)
.messages[0]
.content
.clone();
assert!(content.contains("Related APEX product specs"));
assert!(content.contains("apex/auth-spec.md"));
assert!(content.contains("Token expiry must be checked"));
assert!(content.contains("[apex:"));
let apex_pos = content.find("Related APEX product specs").unwrap();
let instr_pos = content.find("populate the structured response").unwrap();
assert!(
apex_pos < instr_pos,
"APEX section must precede closing instruction"
);
}
#[test]
fn prompt_no_apex_section_when_empty() {
let content = build_review_prompt(
"acme",
"backend",
&sample_meta(),
"+fn x() {}",
&empty_context(),
"",
"openai/gpt-5.4-mini-20260317",
)
.messages[0]
.content
.clone();
assert!(!content.contains("Related APEX product specs"));
assert!(!content.contains("[apex:"));
}