use crate::brain::agent::error::AgentError;
use crate::brain::agent::service::shutdown;
static FLAG_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn with_clean_flag<T>(f: impl FnOnce() -> T) -> T {
let _guard = FLAG_LOCK.lock().unwrap_or_else(|e| e.into_inner());
shutdown::reset_for_test();
let out = f();
shutdown::reset_for_test();
out
}
#[test]
fn a_turn_cancelled_by_shutdown_keeps_its_row() {
with_clean_flag(|| {
shutdown::mark_shutting_down();
let cancelled: Result<(), AgentError> = Err(AgentError::Cancelled);
assert!(
shutdown::keeps_recovery_row(&cancelled),
"the row must survive a shutdown cancellation, or the next boot has \
nothing to resume — this is the #1462 data loss"
);
});
}
#[test]
fn a_user_stop_still_deletes_the_row() {
with_clean_flag(|| {
let cancelled: Result<(), AgentError> = Err(AgentError::Cancelled);
assert!(
!shutdown::keeps_recovery_row(&cancelled),
"a deliberately stopped turn must NOT be resumed; the user abandoned it"
);
});
}
#[test]
fn a_turn_that_finished_during_shutdown_still_deletes_its_row() {
with_clean_flag(|| {
shutdown::mark_shutting_down();
let finished: Result<(), AgentError> = Ok(());
assert!(
!shutdown::keeps_recovery_row(&finished),
"a turn that completed must never keep its row, even mid-shutdown, or \
the next boot replays work that was already answered"
);
});
}
#[test]
fn other_failures_during_shutdown_do_not_keep_the_row() {
with_clean_flag(|| {
shutdown::mark_shutting_down();
let failed: Result<(), AgentError> = Err(AgentError::SessionNotFound(uuid::Uuid::nil()));
assert!(
!shutdown::keeps_recovery_row(&failed),
"only a cancellation is resumable; other errors would replay a turn \
that failed for its own reasons"
);
});
}
#[test]
fn the_flag_starts_down_and_latches() {
with_clean_flag(|| {
assert!(!shutdown::is_shutting_down(), "must start down");
shutdown::mark_shutting_down();
assert!(shutdown::is_shutting_down(), "must latch once raised");
});
}
const STATE_SRC: &str = include_str!("../tui/app/state.rs");
const TOOL_LOOP_SRC: &str = include_str!("../brain/agent/service/tool_loop.rs");
#[test]
fn the_delete_is_guarded_by_the_shutdown_decision() {
assert!(
TOOL_LOOP_SRC
.contains("let cancelled_by_shutdown = super::shutdown::keeps_recovery_row(&result);"),
"the recovery-row decision must be taken from shutdown::keeps_recovery_row; \
a hardcoded value or an inline match silently reintroduces #1462"
);
let block = TOOL_LOOP_SRC
.split("let cancelled_by_shutdown =")
.nth(1)
.expect("the shutdown decision is gone from the cleanup path");
let block = &block[..block
.find("Failed to clean up pending request")
.unwrap_or(block.len())];
assert!(
block.contains("&& !cancelled_by_shutdown"),
"the delete must be skipped when the turn was cancelled by shutdown; \
without this guard the row is destroyed and the next boot has nothing \
to resume (#1462)"
);
}
#[test]
fn ctrl_c_quit_raises_the_flag_before_cancelling() {
let block = STATE_SRC
.split("// Second Ctrl+C within window — quit")
.nth(1)
.expect("the Ctrl+C quit path is gone");
let block = &block[..block.find("should_quit = true").unwrap_or(block.len())];
let flag_at = block
.find("mark_shutting_down()")
.expect("the Ctrl+C quit path must raise the shutdown flag (#1462)");
let cancel_at = block
.find("token.cancel()")
.expect("the Ctrl+C quit path still cancels the in-flight token");
assert!(
flag_at < cancel_at,
"the flag must be raised BEFORE the cancel; raising it afterwards races \
the unwinding turn, which may already have deleted its row"
);
}