use super::*;
#[path = "prompt_test_helpers.rs"]
mod helpers;
use helpers::{empty_context, sample_meta, stock_voice};
#[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",
&stock_voice(),
);
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",
&stock_voice(),
);
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",
&stock_voice(),
);
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![],
coverage_contrib: None,
};
let req = build_review_prompt(
"acme",
"repo",
&sample_meta(),
"+fn foo() {}",
&context,
"",
"openai/gpt-5.4-mini-20260317",
&stock_voice(),
);
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",
&stock_voice(),
);
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",
&stock_voice(),
);
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",
&stock_voice(),
);
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",
&stock_voice(),
);
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",
&stock_voice(),
);
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");
}
use crate::llm::schema::assert_object_nodes_strict as assert_strict;
#[test]
fn review_schema_is_openai_strict_compliant() {
let schema = review_response_schema();
assert_strict(&schema.schema);
let items = &schema.schema["properties"]["findings"]["items"];
assert_eq!(
items["additionalProperties"],
serde_json::json!(false),
"findings.items must set additionalProperties:false"
);
let required: std::collections::BTreeSet<&str> = items["required"]
.as_array()
.expect("findings.items.required array")
.iter()
.filter_map(serde_json::Value::as_str)
.collect();
assert_eq!(
required,
[
"body",
"category",
"confidence",
"consequence",
"file",
"line",
"severity",
"suggested_replacement",
"title"
]
.into_iter()
.collect(),
"findings.items must require every property under strict mode"
);
}
#[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 max_tokens_gemini_is_raised() {
assert_eq!(max_tokens_for_model("google/gemini-2.5-pro"), 8192);
assert_eq!(
max_tokens_for_model("openrouter/google/gemini-2.5-flash"),
8192
);
assert_eq!(
max_tokens_for_model("GOOGLE/GEMINI-2.5-PRO"),
8192,
"match must be case-insensitive"
);
}
#[test]
fn max_tokens_default_for_non_gemini() {
assert_eq!(max_tokens_for_model("openai/gpt-5.4-mini-20260317"), 4096);
assert_eq!(
max_tokens_for_model("bedrock/us.anthropic.claude-sonnet-4-6"),
4096
);
}
#[path = "prompt_tests_apex.rs"]
mod apex_tests;