use roder_api::review::{ReviewFinding, ReviewOutput};
pub const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
fn format_location(finding: &ReviewFinding) -> String {
let path = finding.code_location.absolute_file_path.display();
let start = finding.code_location.line_range.start;
let end = finding.code_location.line_range.end;
format!("{path}:{start}-{end}")
}
pub fn render_findings(output: &ReviewOutput) -> String {
if output.findings.is_empty() {
return String::new();
}
let mut lines = Vec::new();
lines.push(if output.findings.len() > 1 {
"Full review comments:".to_string()
} else {
"Review comment:".to_string()
});
for finding in &output.findings {
lines.push(String::new());
lines.push(format!(
"- [{}] {} — {}",
finding.priority.to_string().to_uppercase(),
finding.display_title(),
format_location(finding)
));
for body_line in finding.body.lines() {
lines.push(format!(" {body_line}"));
}
}
lines.join("\n")
}
pub fn render_review_output_text(output: &ReviewOutput) -> String {
let mut sections = Vec::new();
if let Some(explanation) = output.overall_explanation.as_deref() {
let explanation = explanation.trim();
if !explanation.is_empty() {
sections.push(explanation.to_string());
}
}
let findings = render_findings(output);
if !findings.is_empty() {
sections.push(findings);
}
if sections.is_empty() {
REVIEW_FALLBACK_MESSAGE.to_string()
} else {
sections.join("\n\n")
}
}
pub fn synthetic_user_action(output: &ReviewOutput) -> String {
let results = indent_results(&render_review_output_text(output));
format!(
"<user_action>\n <context>User initiated a review task. Here's the full review output from reviewer model. User may select one or more comments to resolve.</context>\n <action>review</action>\n <results>\n{results}\n </results>\n</user_action>"
)
}
pub fn synthetic_user_action_interrupted() -> String {
"<user_action>\n <context>User initiated a review task, but it was interrupted. If the user asks about this, tell them to re-run `/review` and wait for it to complete.</context>\n <action>review</action>\n <results>\n None.\n </results>\n</user_action>".to_string()
}
fn indent_results(results: &str) -> String {
results
.lines()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use roder_api::review::{ReviewCodeLocation, ReviewLineRange, ReviewPriority};
use super::*;
fn finding(title: &str, priority: ReviewPriority, body: &str) -> ReviewFinding {
ReviewFinding {
title: title.to_string(),
body: body.to_string(),
confidence_score: Some(0.5),
priority,
code_location: ReviewCodeLocation {
absolute_file_path: PathBuf::from("/repo/src/lib.rs"),
line_range: ReviewLineRange { start: 10, end: 12 },
},
}
}
#[test]
fn renders_a_single_finding_with_an_indented_body() {
let output = ReviewOutput {
findings: vec![finding(
"Off-by-one",
ReviewPriority::P1,
"Overruns.\nTwice.",
)],
..ReviewOutput::default()
};
assert_eq!(
render_findings(&output),
"Review comment:\n\n- [P1] Off-by-one — /repo/src/lib.rs:10-12\n Overruns.\n Twice."
);
}
#[test]
fn renders_multiple_findings_with_a_plural_header() {
let output = ReviewOutput {
findings: vec![
finding("First", ReviewPriority::P0, "a"),
finding("Second", ReviewPriority::P3, "b"),
],
..ReviewOutput::default()
};
let rendered = render_findings(&output);
assert!(rendered.starts_with("Full review comments:"));
assert!(rendered.contains("- [P0] First — /repo/src/lib.rs:10-12"));
assert!(rendered.contains("- [P3] Second — /repo/src/lib.rs:10-12"));
}
#[test]
fn renders_nothing_for_an_empty_output() {
assert_eq!(render_findings(&ReviewOutput::default()), "");
assert_eq!(
render_review_output_text(&ReviewOutput::default()),
REVIEW_FALLBACK_MESSAGE
);
}
#[test]
fn summary_joins_the_explanation_and_the_findings() {
let output = ReviewOutput {
findings: vec![finding("Off-by-one", ReviewPriority::P1, "Overruns.")],
overall_explanation: Some(" One blocking bug. ".to_string()),
..ReviewOutput::default()
};
assert_eq!(
render_review_output_text(&output),
"One blocking bug.\n\nReview comment:\n\n- [P1] Off-by-one — /repo/src/lib.rs:10-12\n Overruns."
);
}
#[test]
fn synthetic_user_action_wraps_and_indents_the_results() {
let output = ReviewOutput {
overall_explanation: Some("Looks fine.".to_string()),
..ReviewOutput::default()
};
let rendered = synthetic_user_action(&output);
assert!(rendered.starts_with("<user_action>\n"));
assert!(rendered.contains("<action>review</action>"));
assert!(rendered.contains("\n <results>\n Looks fine.\n </results>\n"));
assert!(rendered.ends_with("</user_action>"));
}
#[test]
fn interrupted_user_action_reports_no_results() {
let rendered = synthetic_user_action_interrupted();
assert!(rendered.contains("interrupted"));
assert!(rendered.contains("None."));
}
}