use std::fmt::Write;
use std::sync::Arc;
use crate::pipeline::board::Ticket;
use crate::prompt::load_prompt;
use crate::tools::shell::{ShellMode, ShellTool};
use crate::{DiagnosticsCommands, Workspace};
use super::{
DIAGNOSTICS_ROLE, TicketPhase, TransitionCtx, bounce_to_development,
comment_and_transition_or_bail, guard_job_phase, reset_phase_attempt, warn,
};
pub(crate) async fn run(ticket: Arc<Ticket>, ws: Workspace, job_id: String) {
if guard_job_phase(&ticket.id, TicketPhase::InDiagnostics, &job_id).await {
return;
}
dispatch_diagnostics(ticket, ws, &job_id).await;
}
async fn run_diagnostics_commands(diag: &DiagnosticsCommands, ws: &Workspace) -> (String, bool) {
let mut comment = String::new();
let mut all_passed = true;
let mut failed_at: &str = "";
for (label, cmd_opt) in diag.commands() {
let Some(cmd) = cmd_opt else {
continue;
};
let mut mark_failed = |comment: &mut String, body: String| {
let _ = write!(comment, "\n\n{label} ({cmd}):\n");
comment.push_str(&body);
all_passed = false;
failed_at = label;
};
let started = std::time::Instant::now();
match ShellTool::new(ShellMode::Full)
.execute_with_status(ws, serde_json::json!({"command": cmd}))
.await
{
Ok((_output, Some(0))) => {
let _ = write!(
comment,
"\n\n{label} ({cmd}): PASSED in {:.1}s",
started.elapsed().as_secs_f64(),
);
}
Ok((output, _exit_code)) => {
let display = if output.is_empty() {
"(no output)".to_string()
} else {
output
};
mark_failed(&mut comment, display);
break;
}
Err(e) => {
mark_failed(&mut comment, e.to_string());
break;
}
}
}
if all_passed {
comment.push_str("\n\n---\n");
comment.push_str(&load_prompt("pipeline/diagnostics_passed.md"));
} else {
let _ = write!(
comment,
"\n\n---\n{} {failed_at}",
load_prompt("pipeline/diagnostics_failed.md"),
);
}
crate::tools::shell::cleanup_agent_spills(DIAGNOSTICS_ROLE);
(comment.trim_start_matches('\n').to_string(), all_passed)
}
async fn conclude_diagnostics_success(
ticket: &Ticket,
job_id: &str,
comment: &str,
log_label: &str,
) {
comment_and_transition_or_bail(
TransitionCtx::buffered(
ticket,
TicketPhase::InDiagnostics,
TicketPhase::Verification,
"Diagnostics",
DIAGNOSTICS_ROLE,
),
DIAGNOSTICS_ROLE,
comment,
log_label,
)
.await;
let _ = crate::jobs::terminalize_job(&crate::session::store().conn, job_id).await;
}
async fn conclude_diagnostics_failure(ticket: &Ticket, job_id: &str, comment: &str) {
bounce_to_development(
ticket,
TicketPhase::InDiagnostics,
"Diagnostics",
DIAGNOSTICS_ROLE,
DIAGNOSTICS_ROLE,
comment,
job_id,
)
.await;
}
async fn dispatch_diagnostics(ticket: Arc<Ticket>, ws: Workspace, job_id: &str) {
let diag_agent_id = format!("ticket_{}_diagnostics", ticket.id);
if let Err(e) = crate::jobs::upsert_job_agent(
&crate::session::store().conn,
job_id,
&diag_agent_id,
crate::jobs::AgentKind::Diagnostics,
crate::jobs::RowStatus::Launched,
)
.await
{
warn!(
ticket = %ticket.id,
error = ?e,
"Failed to register diagnostics in-flight marker — diagnostics may re-dispatch",
);
}
match crate::workspace::store().get_diagnostics(&ws.name).await {
Ok(Some(cmds)) if !cmds.is_empty() => {
let (comment, all_passed) = run_diagnostics_commands(&cmds, &ws).await;
if guard_job_phase(&ticket.id, TicketPhase::InDiagnostics, job_id).await {
return;
}
if all_passed {
conclude_diagnostics_success(
&ticket,
job_id,
&comment,
"Diagnostics finished — transitioned ticket",
)
.await;
} else {
conclude_diagnostics_failure(&ticket, job_id, &comment).await;
}
}
Ok(_) => {
conclude_diagnostics_success(
&ticket,
job_id,
"No diagnostics commands are configured for this workspace \
— diagnostics skipped.",
"Diagnostics skipped — transitioned ticket",
)
.await;
}
Err(e) => {
warn!(
ticket = %ticket.id,
error = %e,
"Failed to load diagnostics for workspace — resetting for a fresh attempt",
);
reset_phase_attempt(
&ticket,
TicketPhase::InDiagnostics,
job_id,
"diagnostics load failure",
&format!("Could not load diagnostics commands due to a database error: {e}"),
)
.await;
}
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use tempfile::TempDir;
const LOCALE_CANDIDATES: &[&str] = &["de_DE.UTF-8", "fr_FR.UTF-8", "es_ES.UTF-8"];
const COMMAND: &str = "sh -c 'printf \"%s\\n\" \"$(date +%A)\"; exit 1'";
async fn run_command(ws: &Workspace, command: &str) -> Option<(String, Option<i32>)> {
ShellTool::new(ShellMode::Full)
.execute_with_status(ws, serde_json::json!({ "command": command }))
.await
.ok()
}
async fn pick_translating_locale(ws: &Workspace) -> Option<&'static str> {
let (plain, _) = run_command(ws, &format!("LC_ALL=C {COMMAND}")).await?;
for locale in LOCALE_CANDIDATES {
let Some((translated, _)) =
run_command(ws, &format!("LC_ALL={locale} {COMMAND}")).await
else {
continue;
};
if translated != plain {
return Some(locale);
}
}
None
}
#[tokio::test]
#[ignore = "manual evidence run: needs a locale that translates a failing command's answer"]
async fn judgement_never_reads_the_answers_language() {
async fn row(ws: &Workspace, label: &str) -> (String, Option<i32>, bool, usize) {
let line = format!("LC_ALL={label} {COMMAND}");
let (answer, status) = run_command(ws, &line).await.expect("the command runs");
let diag = DiagnosticsCommands {
format: Some(line),
..Default::default()
};
let (comment, passed) = run_diagnostics_commands(&diag, ws).await;
(answer, status, passed, comment.lines().count())
}
let tmp = TempDir::new().expect("tempdir");
let ws = crate::workspace::test_ws(tmp.path());
let Some(locale) = pick_translating_locale(&ws).await else {
println!("EVIDENCE SKIPPED: no candidate locale translates the answer on this host");
return;
};
println!("command: <the failing command> | locale: {locale}");
let (c_answer, c_status, c_passed, c_shown) = row(&ws, "C").await;
println!(
"locale C: judged {} | exit status {c_status:?} | shown {c_shown} lines",
if c_passed { "passed" } else { "failed" },
);
let (answer, status, passed, shown) = row(&ws, locale).await;
println!(
"locale {locale}: judged {} | exit status {status:?} | shown {shown} lines | answer differs from the C run: {}",
if passed { "passed" } else { "failed" },
c_answer != answer,
);
let identical = c_passed == passed;
println!("verdict identical across locales: {identical}");
println!("the verdict is the exit status, never the text");
assert!(
identical,
"the judgement must not depend on the language the command answers in"
);
}
}