use super::*;
use crate::util::test::make_ticket;
use crate::util::test::{
create_test_workspace, expect_ticket, expect_ticket_phase, init_management_test_stores,
init_test_stores,
};
use crate::workspace::test_ws_named;
use strum::IntoEnumIterator;
#[test]
fn all_non_general_circuit_breakers_trip_before_general() {
let general = CircuitBreakerKind::General.threshold();
for kind in CircuitBreakerKind::iter() {
if kind == CircuitBreakerKind::General {
continue;
}
assert!(
kind.threshold() < general,
"{kind:?}.threshold() ({}) must be less than General.threshold() ({general})",
kind.threshold(),
);
}
}
#[tokio::test]
async fn circuit_breaker_moves_other_ready_for_development_tickets_to_planning() {
init_management_test_stores().await;
let ws_a = test_ws_named("/ws_a", "ws_a");
let ws_b = test_ws_named("/ws_b", "ws_b");
let trip_id = make_ticket(
board(),
&ws_a,
"Trip Ticket",
TicketPhase::ReadyForDevelopment,
)
.await;
let victim_id = make_ticket(
board(),
&ws_a,
"Victim Ticket",
TicketPhase::ReadyForDevelopment,
)
.await;
let other_ws_id = make_ticket(
board(),
&ws_b,
"Other Workspace Ticket",
TicketPhase::ReadyForDevelopment,
)
.await;
for i in 0..=CircuitBreakerKind::General.threshold() {
board()
.add_comment(&trip_id, SYSTEM_ROLE, &format!("Comment {i}"))
.await
.expect("add_comment to A");
}
let ticket_a = expect_ticket(board(), &trip_id).await;
let tripped = try_trip_circuit_breaker(
&ticket_a,
TicketPhase::ReadyForDevelopment,
CircuitBreakerKind::General,
"test",
)
.await;
assert!(tripped, "circuit breaker should have tripped");
{
let ticket_a = expect_ticket(board(), &trip_id).await;
assert_eq!(
ticket_a.phase,
TicketPhase::Failed,
"tripped ticket A should be Failed"
);
}
{
let ticket_b = expect_ticket(board(), &victim_id).await;
assert_eq!(
ticket_b.phase,
TicketPhase::Planning,
"other ReadyForDevelopment ticket B in same workspace should be Planning"
);
}
{
let ticket_c = expect_ticket(board(), &other_ws_id).await;
assert_eq!(
ticket_c.phase,
TicketPhase::ReadyForDevelopment,
"ticket C in different workspace must not be moved"
);
}
}
#[tokio::test]
async fn record_verdict_comments_filtering() {
init_test_stores().await;
let ticket_id = make_ticket(
board(),
&test_ws_named("/tmp/test", "test"),
"Test",
TicketPhase::Backlog,
)
.await;
let results = vec![pass_result()];
crate::turso::with_tx(
&board().conn,
&ticket_id,
"test verdict comments",
async |tx| {
record_verdict_comments_tx(
tx,
&ticket_id,
&results,
Role::Reviewer.as_str(),
VerdictFilter::FailingOnly,
)
.await
},
)
.await
.expect("record_verdict_comments_tx should succeed");
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
assert_eq!(
comments.len(),
0,
"passing verdicts with FailingOnly filter should produce 0 comments"
);
let results = vec![fail_result()];
crate::turso::with_tx(
&board().conn,
&ticket_id,
"test verdict comments",
async |tx| {
record_verdict_comments_tx(
tx,
&ticket_id,
&results,
Role::Reviewer.as_str(),
VerdictFilter::FailingOnly,
)
.await
},
)
.await
.expect("record_verdict_comments_tx should succeed");
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
assert_eq!(
comments.len(),
1,
"failing verdict should create one comment"
);
assert_eq!(comments[0].role, "reviewer_1");
let results = vec![
analyst_verdict(10, "Excellent analysis.", &[]),
analyst_verdict(4, "Needs more research.", &["Missing citations"]),
];
crate::turso::with_tx(
&board().conn,
&ticket_id,
"test verdict comments",
async |tx| {
record_verdict_comments_tx(
tx,
&ticket_id,
&results,
Role::Analyst.as_str(),
VerdictFilter::All,
)
.await
},
)
.await
.expect("record_verdict_comments_tx should succeed");
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
assert_eq!(
comments.len(),
3,
"All filter should write both verdicts (total 3)"
);
assert_eq!(comments[1].role, "analyst_1");
assert_eq!(comments[2].role, "analyst_2");
}
async fn setup_db_workspace(suffix: &str) -> crate::Workspace {
init_management_test_stores().await;
let ws_name = format!("ws_{suffix}");
let ws_path = format!("/tmp/test_{suffix}");
create_test_workspace(&ws_path, &ws_name).await
}
async fn setup_ticket(
ws_path: &str,
ws_name: &str,
title: &str,
phase: TicketPhase,
) -> (crate::Workspace, String) {
init_management_test_stores().await;
let ws = test_ws_named(ws_path, ws_name);
let ticket_id = make_ticket(board(), &ws, title, phase).await;
(ws, ticket_id)
}
#[tokio::test]
async fn transition_ticket_to_done_buffer_and_notify() {
let ws = setup_db_workspace("drains_buffer").await;
let first_id = make_ticket(board(), &ws, "Ticket A", TicketPhase::QaPassed).await;
let second_id = make_ticket(board(), &ws, "Ticket B", TicketPhase::QaPassed).await;
let ticket_a = expect_ticket(board(), &first_id).await;
transition_ticket_to_done(
&ticket_a,
TicketPhase::QaPassed,
"Test — ticket A done, B still active",
)
.await;
let intermediate = crate::ticket_buffer::drain("ws_drains_buffer");
assert!(
!intermediate.is_empty(),
"After first QaPassed → Done with other active tickets: \
should have buffered the notification (got empty buffer)",
);
let ticket_b = expect_ticket(board(), &second_id).await;
transition_ticket_to_done(
&ticket_b,
TicketPhase::QaPassed,
"Test — ticket B done, last ticket",
)
.await;
for (id, label) in [(&first_id, "A"), (&second_id, "B")] {
let t = expect_ticket(board(), id).await;
assert_eq!(t.phase, TicketPhase::Done, "Ticket {label} should be Done");
let comments = board().get_comments(id).await.expect("get_comments");
assert!(
comments.iter().any(|c| c.role == SYSTEM_ROLE),
"Ticket {label}: expected SYSTEM_ROLE comment from transition_ticket_to_done"
);
}
let drained = crate::ticket_buffer::drain("ws_drains_buffer");
assert!(
drained.is_empty(),
"Buffer should be empty after last ticket's Notify drains it",
);
}
#[tokio::test]
async fn breaker_counts_failures() {
struct BreakerCase {
name: &'static str,
kind: CircuitBreakerKind,
source_phase: TicketPhase,
log_label: &'static str,
ws_suffix: &'static str,
below_threshold_count: usize,
trip_count: usize,
}
init_management_test_stores().await;
let cases = [
BreakerCase {
name: "Sanitation",
kind: CircuitBreakerKind::Sanitation,
source_phase: TicketPhase::InSanitation,
log_label: "Sanitation",
ws_suffix: "san_breaker_test",
below_threshold_count: 2,
trip_count: 4,
},
BreakerCase {
name: "Diagnostics",
kind: CircuitBreakerKind::Diagnostics,
source_phase: TicketPhase::InDiagnostics,
log_label: "Diagnostics",
ws_suffix: "diag_breaker_test",
below_threshold_count: 3,
trip_count: 5,
},
];
for case in &cases {
let ticket_id = make_ticket(
board(),
&test_ws_named("/tmp/test", case.ws_suffix),
&format!("{} Breaker Test", case.log_label),
case.source_phase,
)
.await;
for _ in 0..case.below_threshold_count {
add_breaker_failure(case.kind, &ticket_id).await;
}
let ticket = expect_ticket(board(), &ticket_id).await;
assert!(
!try_trip_circuit_breaker(&ticket, case.source_phase, case.kind, case.log_label,).await,
"case {}: should NOT trip with {} failures (threshold: {})",
case.name,
case.below_threshold_count,
case.kind.threshold(),
);
for _ in case.below_threshold_count..case.trip_count {
add_breaker_failure(case.kind, &ticket_id).await;
}
let ticket = expect_ticket(board(), &ticket_id).await;
let tripped =
try_trip_circuit_breaker(&ticket, case.source_phase, case.kind, case.log_label).await;
assert!(
tripped,
"case {}: should trip with {} failures (threshold: {}, {} > {})",
case.name,
case.trip_count,
case.kind.threshold(),
case.trip_count,
case.kind.threshold(),
);
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase,
TicketPhase::Failed,
"case {}: circuit breaker should transition to Failed",
case.name,
);
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
let has_breaker_comment = comments
.iter()
.any(|c| c.role == SYSTEM_ROLE && c.content.to_lowercase().contains("circuit breaker"));
assert!(
has_breaker_comment,
"case {}: should have a SYSTEM_ROLE comment with the circuit breaker message \
(containing 'circuit breaker')",
case.name,
);
}
}
fn pass_verdict() -> crate::Verdict {
crate::Verdict {
score: REVIEW_QA_THRESHOLD,
critique: Some("Good work.".into()),
issues_detected: vec![],
}
}
fn fail_verdict() -> crate::Verdict {
crate::Verdict {
score: 3,
critique: Some("Missing error handling.".into()),
issues_detected: vec!["No timeout check".into()],
}
}
fn no_verdict() -> ParallelVerdict {
ParallelVerdict::NoResponse
}
async fn add_breaker_failure(kind: CircuitBreakerKind, ticket_id: &str) {
let (role, comment) = match kind {
CircuitBreakerKind::Sanitation => (
SYSTEM_ROLE,
format!("{SANITATION_FAILED_MARKER} — garbage files: 1"),
),
CircuitBreakerKind::Diagnostics => (
DIAGNOSTICS_ROLE,
format!("{DIAGNOSTICS_COMMENT_PREFIX}\n\n---\n{DIAGNOSTICS_FAILED_MARKER} test_step"),
),
CircuitBreakerKind::General => {
unreachable!("General breaker not used in failure-counting tests")
}
};
let _ = board().add_comment(ticket_id, role, &comment).await;
}
fn pass_result() -> ParallelVerdict {
ParallelVerdict::Verdict(pass_verdict())
}
fn fail_result() -> ParallelVerdict {
ParallelVerdict::Verdict(fail_verdict())
}
fn analyst_verdict(score: u8, critique: &str, issues: &[&str]) -> ParallelVerdict {
ParallelVerdict::Verdict(crate::Verdict {
score,
critique: Some(critique.into()),
issues_detected: issues.iter().map(|&s| s.into()).collect(),
})
}
#[tokio::test]
async fn process_verifier_verdicts_cases() {
struct Case {
name: &'static str,
ws_suffix: &'static str,
title: &'static str,
phase: TicketPhase,
results: Vec<ParallelVerdict>,
vi: VerifierInfo,
expected_phase: TicketPhase,
expected_pipeline_reservation: bool,
}
init_management_test_stores().await;
let cases = vec![
Case {
name: "all failed -> Failed",
ws_suffix: "vp_all_fail",
title: "VP All Failed",
phase: TicketPhase::InReview,
results: vec![no_verdict(); 3],
vi: REVIEWER_VI,
expected_phase: TicketPhase::Failed,
expected_pipeline_reservation: false,
},
Case {
name: "any failed -> bounce-back with pipeline reservation",
ws_suffix: "vp_any_fail",
title: "VP Any Failed",
phase: TicketPhase::InReview,
results: vec![pass_result(), fail_result(), pass_result()],
vi: REVIEWER_VI,
expected_phase: TicketPhase::ReadyForDevelopment,
expected_pipeline_reservation: true,
},
Case {
name: "all passed -> Reviewed",
ws_suffix: "vp_all_pass",
title: "VP All Pass",
phase: TicketPhase::InReview,
results: vec![pass_result(), pass_result(), pass_result()],
vi: REVIEWER_VI,
expected_phase: TicketPhase::Reviewed,
expected_pipeline_reservation: false,
},
Case {
name: "all passed (QA) -> QaPassed",
ws_suffix: "vp_qa_pass",
title: "VP QA Pass",
phase: TicketPhase::InQa,
results: vec![pass_result(), pass_result(), pass_result()],
vi: QA_VI,
expected_phase: TicketPhase::QaPassed,
expected_pipeline_reservation: false,
},
];
for case in &cases {
let ticket_id = make_ticket(
board(),
&test_ws_named("/tmp/test", case.ws_suffix),
case.title,
case.phase,
)
.await;
let ticket = expect_ticket(board(), &ticket_id).await;
process_verifier_verdicts(&ticket, &case.results, case.vi).await;
let ticket = expect_ticket(board(), &ticket_id).await;
assert_eq!(
ticket.phase, case.expected_phase,
"case {}: expected phase {:?}, got {:?}",
case.name, case.expected_phase, ticket.phase,
);
assert_eq!(
ticket.pipeline_reservation, case.expected_pipeline_reservation,
"case {}: expected pipeline_reservation={}, got {}",
case.name, case.expected_pipeline_reservation, ticket.pipeline_reservation,
);
}
}
#[tokio::test]
async fn circuit_breaker_comment_boundary() {
struct Case {
name: &'static str,
ws_suffix: &'static str,
title: &'static str,
comment_count: usize,
expected_trip: bool,
expected_phase: TicketPhase,
}
init_management_test_stores().await;
let cases = [
Case {
name: "> threshold trips",
ws_suffix: "cb_thresh",
title: "CB Threshold",
comment_count: CircuitBreakerKind::General.threshold() + 1,
expected_trip: true,
expected_phase: TicketPhase::Failed,
},
Case {
name: "= threshold does not trip",
ws_suffix: "cb_no_trip",
title: "CB No Trip",
comment_count: CircuitBreakerKind::General.threshold(),
expected_trip: false,
expected_phase: TicketPhase::InReview,
},
];
for case in &cases {
let ticket_id = make_ticket(
board(),
&test_ws_named("/tmp/test", case.ws_suffix),
case.title,
TicketPhase::InReview,
)
.await;
for i in 0..case.comment_count {
board()
.add_comment(&ticket_id, "user", &format!("Comment {i}"))
.await
.expect("add_comment");
}
let ticket = expect_ticket(board(), &ticket_id).await;
let tripped = try_trip_circuit_breaker(
&ticket,
TicketPhase::InReview,
CircuitBreakerKind::General,
"test",
)
.await;
assert_eq!(
tripped, case.expected_trip,
"case {}: expected trip={}, got tripped={}",
case.name, case.expected_trip, tripped,
);
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase, case.expected_phase,
"case {}: expected phase {:?}, got {:?}",
case.name, case.expected_phase, phase,
);
if tripped {
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
let has_marker = comments
.iter()
.any(|c| c.content.to_lowercase().contains("circuit breaker"));
assert!(
has_marker,
"case {}: trip comment must contain circuit breaker marker",
case.name,
);
}
}
}
#[tokio::test]
async fn process_analyst_verdicts_cases() {
struct Case {
name: &'static str,
ws_suffix: &'static str,
title: &'static str,
results: Vec<ParallelVerdict>,
expected_comment_substring: &'static str,
}
init_management_test_stores().await;
let cases = vec![
Case {
name: "all pass -> Planning with LGTM",
ws_suffix: "an_all_pass",
title: "Analyst All Pass",
results: vec![
analyst_verdict(10, "Great analysis.", &[]),
analyst_verdict(9, "Solid work.", &[]),
analyst_verdict(8, "Good analysis.", &[]),
],
expected_comment_substring: "All LGTM",
},
Case {
name: "partial fail -> Planning with blockers",
ws_suffix: "an_partial",
title: "Analyst Partial Fail",
results: vec![
analyst_verdict(10, "Great.", &[]),
analyst_verdict(3, "Poor analysis.", &["Missing data"]),
analyst_verdict(8, "Decent.", &["Minor issue"]),
],
expected_comment_substring: "blockers",
},
Case {
name: "no verdicts -> Planning with no analysis",
ws_suffix: "an_no_v",
title: "Analyst No Verdicts",
results: vec![no_verdict(); 3],
expected_comment_substring: "no analysis",
},
];
for case in &cases {
let ticket_id = make_ticket(
board(),
&test_ws_named("/tmp/test", case.ws_suffix),
case.title,
TicketPhase::Analysis,
)
.await;
let ticket = expect_ticket(board(), &ticket_id).await;
process_analyst_verdicts(&ticket, &case.results).await;
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase,
TicketPhase::Planning,
"case {}: expected Planning, got {:?}",
case.name,
phase,
);
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
let system = comments
.iter()
.find(|c| c.role == SYSTEM_ROLE)
.unwrap_or_else(|| panic!("case {}: system summary comment should exist", case.name));
assert!(
system.content.contains(case.expected_comment_substring),
"case {}: system comment should contain {:?}, got: {}",
case.name,
case.expected_comment_substring,
system.content,
);
}
}
#[tokio::test]
async fn handle_qa_passed_no_git_to_done() {
let dir = tempfile::tempdir().expect("create temp dir");
let ws_path = dir.path().to_str().expect("temp path is valid UTF-8");
let (ws, ticket_id) =
setup_ticket(ws_path, "qa_no_git", "QA No Git", TicketPhase::QaPassed).await;
let ticket = expect_ticket(board(), &ticket_id).await;
handle_qa_passed(ticket, ws).await;
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase,
TicketPhase::Done,
"QA passed should eventually transition to Done"
);
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
assert!(
comments
.iter()
.any(|c| c.role == SYSTEM_ROLE && c.content.contains("without commit")),
"Expected a SYSTEM_ROLE comment explaining why no commit was made"
);
}
#[tokio::test]
async fn handle_qa_passed_untracked_files_to_insanitation() {
if !crate::git_commands::git_is_installed().await {
eprintln!("git not installed — skipping git-dependent test");
return;
}
let (_dir, repo_path) = crate::util::test::init_temp_repo();
std::fs::write(repo_path.join("untracked.txt"), b"garbage").expect("write untracked file");
let (ws, ticket_id) = setup_ticket(
repo_path.to_str().unwrap(),
"qa_untracked",
"QA Untracked",
TicketPhase::QaPassed,
)
.await;
let ticket = expect_ticket(board(), &ticket_id).await;
handle_qa_passed(ticket, ws).await;
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase,
TicketPhase::InSanitation,
"QA passed with untracked files should transition to InSanitation"
);
let ticket = expect_ticket(board(), &ticket_id).await;
let expected_key =
crate::session::ticket_session_key(&ticket_id, crate::Role::Sanitation.as_str());
assert_eq!(
ticket.assigned_to.as_deref(),
Some(expected_key.as_str()),
"assigned_to should be set to sanitation session key"
);
}
#[tokio::test]
async fn handle_qa_passed_clean_tree_to_done() {
if !crate::git_commands::git_is_installed().await {
eprintln!("git not installed — skipping git-dependent test");
return;
}
let (_dir, repo_path) = crate::util::test::init_temp_repo();
let (ws, ticket_id) = setup_ticket(
repo_path.to_str().unwrap(),
"qa_clean",
"QA Clean Tree",
TicketPhase::QaPassed,
)
.await;
let ticket = expect_ticket(board(), &ticket_id).await;
handle_qa_passed(ticket, ws).await;
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase,
TicketPhase::Done,
"QA passed with clean tree should transition to Done"
);
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
assert!(
comments
.iter()
.any(|c| c.role == SYSTEM_ROLE && c.content.contains("Clean working tree")),
"Expected a SYSTEM_ROLE comment explaining the clean-tree skip"
);
}
#[tokio::test]
async fn process_sanitation_verdict_cases() {
init_management_test_stores().await;
let ws_pass = test_ws_named("/tmp/test", "sv_pass");
let pass_id = make_ticket(board(), &ws_pass, "SV Pass", TicketPhase::InSanitation).await;
let ticket_pass = expect_ticket(board(), &pass_id).await;
let pass_verdict = crate::SanitationVerdict {
pass: true,
garbage_files: vec![],
rationale: "All files are legitimate project files.".into(),
};
process_sanitation_verdict(&ticket_pass, pass_verdict).await;
let phase = expect_ticket_phase(board(), &pass_id).await;
assert_eq!(
phase,
TicketPhase::SanitationPassed,
"pass=true should transition to SanitationPassed, got {phase:?}",
);
let ticket = expect_ticket(board(), &pass_id).await;
assert!(
ticket.assigned_to.is_none(),
"assigned_to should be cleared after pass=true",
);
let comments = board().get_comments(&pass_id).await.expect("get_comments");
let has_sanitation_comment = comments.iter().any(|c| c.role == Role::Sanitation.as_str());
assert!(
has_sanitation_comment,
"pass=true should add a sanitation comment",
);
let has_system_comment = comments.iter().any(|c| c.role == SYSTEM_ROLE);
assert!(
!has_system_comment,
"pass=true should not add a system comment",
);
let ws_fail = test_ws_named("/tmp/test", "sv_fail");
let fail_id = make_ticket(board(), &ws_fail, "SV Fail", TicketPhase::InSanitation).await;
let ticket_fail = expect_ticket(board(), &fail_id).await;
let fail_verdict = crate::SanitationVerdict {
pass: false,
garbage_files: vec!["node_modules/".into(), "tmp/scratch.js".into()],
rationale: "These are intermediate build artifacts.".into(),
};
process_sanitation_verdict(&ticket_fail, fail_verdict).await;
let ticket = expect_ticket(board(), &fail_id).await;
assert_eq!(
ticket.phase,
TicketPhase::ReadyForDevelopment,
"pass=false should bounce back to ReadyForDevelopment, got {:?}",
ticket.phase,
);
assert!(
ticket.pipeline_reservation,
"pass=false should set pipeline_reservation=true",
);
assert!(
ticket.assigned_to.is_none(),
"assigned_to should be cleared after pass=false transition",
);
let comments = board().get_comments(&fail_id).await.expect("get_comments");
let has_garbage_comment = comments
.iter()
.any(|c| c.role == Role::Sanitation.as_str() && c.content.contains("node_modules/"));
assert!(
has_garbage_comment,
"pass=false should have a sanitation comment mentioning garbage files",
);
let has_system_breaker = comments
.iter()
.any(|c| c.role == SYSTEM_ROLE && c.content.contains(SANITATION_FAILED_MARKER));
assert!(
has_system_breaker,
"pass=false should have a system comment with the circuit breaker prefix",
);
}
#[allow(clippy::too_many_lines)]
#[tokio::test]
async fn dispatch_diagnostics_cases() {
struct Case {
name: &'static str,
ws_suffix: &'static str,
title: &'static str,
commands: Option<DiagnosticsCommands>,
corrupt_diagnostics: bool,
needs_tempdir: bool,
expected_phase: TicketPhase,
expected_pipeline_reservation: bool,
expected_comment_contains: &'static [&'static str],
}
init_management_test_stores().await;
let fail_cmds = DiagnosticsCommands {
format: Some("false".to_string()),
..Default::default()
};
let pass_cmds = DiagnosticsCommands {
format: Some("true".to_string()),
type_check: Some("true".to_string()),
..Default::default()
};
let cases = [
Case {
name: "no diagnostics commands",
ws_suffix: "dc_no_cmds",
title: "No Diagnostics Commands",
commands: None,
corrupt_diagnostics: false,
needs_tempdir: false,
expected_phase: TicketPhase::DiagnosticsDone,
expected_pipeline_reservation: false,
expected_comment_contains: &["No diagnostics commands are configured"],
},
Case {
name: "diagnostics failure",
ws_suffix: "dc_fail",
title: "Diagnostics Failure Test",
commands: Some(fail_cmds),
corrupt_diagnostics: false,
needs_tempdir: true,
expected_phase: TicketPhase::ReadyForDevelopment,
expected_pipeline_reservation: true,
expected_comment_contains: &[DIAGNOSTICS_COMMENT_PREFIX, DIAGNOSTICS_FAILED_MARKER],
},
Case {
name: "diagnostics all pass",
ws_suffix: "dc_pass",
title: "Diagnostics All Pass Test",
commands: Some(pass_cmds),
corrupt_diagnostics: false,
needs_tempdir: true,
expected_phase: TicketPhase::DiagnosticsDone,
expected_pipeline_reservation: false,
expected_comment_contains: &[DIAGNOSTICS_COMMENT_PREFIX, DIAGNOSTICS_PASSED_MARKER],
},
Case {
name: "diagnostics DB error",
ws_suffix: "dc_db_err",
title: "Diagnostics DB Error Test",
commands: None,
corrupt_diagnostics: true,
needs_tempdir: false,
expected_phase: TicketPhase::DiagnosticsDone,
expected_pipeline_reservation: false,
expected_comment_contains: &["database error"],
},
];
for case in &cases {
let (_dir, ws_path): (Option<tempfile::TempDir>, String) = if case.needs_tempdir {
let dir = tempfile::tempdir().expect("create temp dir");
let path = dir.path().to_string_lossy().to_string();
(Some(dir), path)
} else {
(None, format!("/tmp/{}", case.ws_suffix))
};
let ws = create_test_workspace(&ws_path, case.ws_suffix).await;
if let Some(cmds) = &case.commands {
crate::workspace::store()
.set_diagnostics(case.ws_suffix, cmds, &crate::turso::now())
.await
.expect("set diagnostics");
}
if case.corrupt_diagnostics {
crate::workspace::store()
.conn
.execute(
"UPDATE workspaces SET diagnostics = ?1 WHERE name = ?2",
turso::params!["not valid json", case.ws_suffix],
)
.await
.expect("set diagnostics to invalid JSON");
}
let ticket_id = make_ticket(board(), &ws, case.title, TicketPhase::InDiagnostics).await;
let ticket = expect_ticket(board(), &ticket_id).await;
dispatch_diagnostics(Arc::new(ticket), ws).await;
let phase = expect_ticket_phase(board(), &ticket_id).await;
assert_eq!(
phase, case.expected_phase,
"case {}: expected phase {:?}, got {:?}",
case.name, case.expected_phase, phase,
);
let ticket = expect_ticket(board(), &ticket_id).await;
assert_eq!(
ticket.pipeline_reservation, case.expected_pipeline_reservation,
"case {}: pipeline_reservation mismatch",
case.name,
);
assert!(
ticket.assigned_to.is_none(),
"case {}: assigned_to should be cleared after diagnostics dispatch",
case.name,
);
let comments = board()
.get_comments(&ticket_id)
.await
.expect("get_comments");
assert!(
!comments.is_empty(),
"case {}: should have written at least one comment",
case.name,
);
let has_expected = comments.iter().any(|c| {
c.role == DIAGNOSTICS_ROLE
&& case
.expected_comment_contains
.iter()
.all(|&marker| c.content.contains(marker))
});
assert!(
has_expected,
"case {}: should have a DIAGNOSTICS_ROLE comment containing: {:?}",
case.name, case.expected_comment_contains,
);
}
}