use async_trait::async_trait;
use serde::Deserialize;
use std::time::Duration;
use tokio::process::Command;
use super::base::{CliExecutor, ExecutorResponse};
use crate::types::config::ExecutorConfig;
use crate::types::requests::EvaluationRequest;
use crate::types::responses::{ModelVote, Vote};
use crate::{TetradError, TetradResult};
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct GeminiWrapper {
#[serde(default)]
session_id: String,
#[serde(default)]
response: String,
#[serde(default)]
stats: serde_json::Value,
}
pub struct GeminiExecutor {
command_name: String,
args: Vec<String>,
timeout: Duration,
}
impl GeminiExecutor {
pub fn new() -> Self {
Self {
command_name: "gemini".to_string(),
args: vec!["-o".to_string(), "json".to_string()],
timeout: Duration::from_secs(60),
}
}
pub fn from_config(config: &ExecutorConfig) -> Self {
Self {
command_name: config.command.clone(),
args: config.args.clone(),
timeout: Duration::from_secs(config.timeout_secs),
}
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
fn parse_gemini_output(output: &str) -> TetradResult<ExecutorResponse> {
let json_start = output.find('{');
let output = if let Some(start) = json_start {
&output[start..]
} else {
output
};
if let Ok(wrapper) = serde_json::from_str::<GeminiWrapper>(output) {
if let Ok(response) = ExecutorResponse::parse_from_output(&wrapper.response, "Gemini") {
return Ok(response);
}
return Ok(Self::analyze_text_response(&wrapper.response));
}
if let Ok(response) = ExecutorResponse::parse_from_output(output, "Gemini") {
return Ok(response);
}
Err(TetradError::ExecutorFailed(
"Gemini".to_string(),
"Não foi possível parsear resposta do Gemini".to_string(),
))
}
fn analyze_text_response(text: &str) -> ExecutorResponse {
let lower = text.to_lowercase();
let vote = if lower.contains("erro crítico")
|| lower.contains("bug grave")
|| lower.contains("vulnerabilidade")
|| lower.contains("falha de segurança")
|| lower.contains("critical error")
|| lower.contains("security vulnerability")
{
"FAIL"
} else if lower.contains("problema")
|| lower.contains("issue")
|| lower.contains("considere")
|| lower.contains("sugestão")
|| lower.contains("atenção")
|| lower.contains("melhoria")
|| lower.contains("overflow")
|| lower.contains("observação")
|| lower.contains("consider")
|| lower.contains("suggestion")
{
"WARN"
} else {
"PASS"
};
let score = if vote == "PASS" {
if lower.contains("perfeito")
|| lower.contains("excelente")
|| lower.contains("perfect")
{
95
} else if lower.contains("bom")
|| lower.contains("correto")
|| lower.contains("idiomático")
{
85
} else {
80
}
} else if vote == "WARN" {
if lower.contains("menor") || lower.contains("minor") {
70
} else {
60
}
} else {
35
};
let issues: Vec<String> = text
.lines()
.filter(|line| {
let trimmed = line.trim();
trimmed.starts_with("- ") || trimmed.starts_with("* ") || trimmed.starts_with("• ")
})
.map(|line| {
line.trim()
.trim_start_matches("- ")
.trim_start_matches("* ")
.trim_start_matches("• ")
.to_string()
})
.take(5)
.collect();
let suggestions: Vec<String> = text
.lines()
.filter(|line| {
let lower_line = line.to_lowercase();
lower_line.contains("sugest") || lower_line.contains("consider")
})
.map(|line| line.trim().to_string())
.take(3)
.collect();
ExecutorResponse {
vote: vote.to_string(),
score,
reasoning: text.chars().take(500).collect(),
issues,
suggestions,
}
}
}
impl Default for GeminiExecutor {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl CliExecutor for GeminiExecutor {
fn name(&self) -> &str {
"Gemini"
}
fn command(&self) -> &str {
&self.command_name
}
fn specialization(&self) -> &str {
"architecture"
}
async fn evaluate(&self, request: &EvaluationRequest) -> TetradResult<ModelVote> {
let prompt = self.build_prompt(request);
let mut cmd = Command::new(&self.command_name);
for arg in &self.args {
cmd.arg(arg);
}
cmd.arg(&prompt);
let result = tokio::time::timeout(self.timeout, cmd.output()).await;
match result {
Ok(Ok(output)) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if !stdout.is_empty() {
match Self::parse_gemini_output(&stdout) {
Ok(response) => return Ok(response.into_vote(self.name())),
Err(e) => {
tracing::debug!(
"Falha ao parsear output do Gemini: {}. Tentando stderr...",
e
);
}
}
}
if !stderr.is_empty() && (stderr.contains("Error") || stderr.contains("error")) {
if !stderr.contains("Loaded cached credentials") {
return Err(TetradError::ExecutorFailed(
self.name().to_string(),
stderr.to_string(),
));
}
}
if stdout.is_empty() && !stderr.is_empty() {
if let Ok(response) = Self::parse_gemini_output(&stderr) {
return Ok(response.into_vote(self.name()));
}
}
Err(TetradError::ExecutorFailed(
self.name().to_string(),
"Não foi possível parsear resposta do Gemini".to_string(),
))
}
Ok(Err(e)) => {
if e.kind() == std::io::ErrorKind::NotFound {
Ok(ModelVote::new(self.name(), Vote::Warn, 50)
.with_reasoning("Gemini CLI não disponível"))
} else {
Err(TetradError::ExecutorFailed(
self.name().to_string(),
e.to_string(),
))
}
}
Err(_) => Err(TetradError::ExecutorTimeout(self.name().to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_response_valid() {
let output = r#"{"vote": "WARN", "score": 70, "reasoning": "Some issues", "issues": ["issue1"], "suggestions": []}"#;
let response = ExecutorResponse::parse_from_output(output, "Gemini");
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "WARN");
assert_eq!(response.score, 70);
assert_eq!(response.issues.len(), 1);
}
#[test]
fn test_specialization() {
let executor = GeminiExecutor::new();
assert_eq!(executor.specialization(), "architecture");
}
#[test]
fn test_parse_gemini_wrapper() {
let output = r#"{
"session_id": "test-123",
"response": "A função está correta e bem estruturada.",
"stats": {}
}"#;
let response = GeminiExecutor::parse_gemini_output(output);
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "PASS");
assert!(response.score >= 80);
}
#[test]
fn test_parse_gemini_wrapper_with_json_response() {
let output = r#"{
"session_id": "test-123",
"response": "{\"vote\": \"PASS\", \"score\": 95, \"reasoning\": \"Excelente!\", \"issues\": [], \"suggestions\": []}",
"stats": {}
}"#;
let response = GeminiExecutor::parse_gemini_output(output);
assert!(response.is_ok());
let response = response.unwrap();
assert_eq!(response.vote, "PASS");
assert_eq!(response.score, 95);
}
#[test]
fn test_parse_gemini_with_log_prefix() {
let output = r#"Loaded cached credentials.
{
"session_id": "test-123",
"response": "Código aprovado.",
"stats": {}
}"#;
let response = GeminiExecutor::parse_gemini_output(output);
assert!(response.is_ok());
}
#[test]
fn test_analyze_text_response_pass() {
let text = "A função está correta e bem estruturada. Código idiomático.";
let response = GeminiExecutor::analyze_text_response(text);
assert_eq!(response.vote, "PASS");
assert!(response.score >= 80);
}
#[test]
fn test_analyze_text_response_warn() {
let text = "O código funciona, mas considere adicionar tratamento de overflow para maior segurança.";
let response = GeminiExecutor::analyze_text_response(text);
assert_eq!(response.vote, "WARN");
assert!(response.score >= 50 && response.score < 80);
}
#[test]
fn test_analyze_text_response_fail() {
let text = "Erro crítico: vulnerabilidade de segurança detectada no código.";
let response = GeminiExecutor::analyze_text_response(text);
assert_eq!(response.vote, "FAIL");
assert!(response.score < 50);
}
#[test]
fn test_analyze_text_extracts_issues() {
let text = "Problemas encontrados:\n- Falta documentação\n- Nomes de variáveis pouco claros\n* Ausência de testes";
let response = GeminiExecutor::analyze_text_response(text);
assert_eq!(response.issues.len(), 3);
}
#[test]
fn test_default_args() {
let executor = GeminiExecutor::new();
assert_eq!(executor.args, vec!["-o", "json"]);
}
}