use super::super::background::{Acted, Acting, BgOutcome, Refund, Window};
use super::*;
use tokio_util::sync::CancellationToken;
#[tokio::test]
async fn auto_reflect_advances_watermark_on_spawn() {
let (_d, mut orch, chat_id) = orch_ready_for_reflection();
orch.engines.backend = Some(Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>);
orch.maybe_auto_reflect(chat_id);
let chat = orch.chats.iter().find(|c| c.id == chat_id).unwrap();
assert_eq!(chat.reflected_upto, Some(2));
assert!(chat.reflected_at.is_some());
assert!(orch.bg_running(BackgroundKind::Reflection));
}
#[tokio::test]
async fn auto_reflect_keeps_watermark_when_server_not_ready() {
let (_d, mut orch, chat_id) = orch_ready_for_reflection();
orch.maybe_auto_reflect(chat_id);
let chat = orch.chats.iter().find(|c| c.id == chat_id).unwrap();
assert_eq!(chat.reflected_upto, None); assert!(!orch.bg_running(BackgroundKind::Reflection));
}
#[tokio::test]
async fn reflect_failures_alert_once_then_reset() {
let (_d, mut orch, mut rx) = bare_orch_rx();
let saw_error = |rx: &mut UnboundedReceiver<AppEvent>| {
let mut seen = false;
while let Ok(e) = rx.try_recv() {
if matches!(e, AppEvent::Error(_)) {
seen = true;
}
}
seen
};
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Failed("boom".into()),
None,
);
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Failed("boom".into()),
None,
);
assert!(!saw_error(&mut rx));
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Failed("boom".into()),
None,
);
assert!(saw_error(&mut rx));
assert_eq!(orch.bg_failures(BackgroundKind::Reflection), 3);
orch.handle_bg_done(BackgroundKind::Reflection, BgOutcome::Done, None);
assert_eq!(orch.bg_failures(BackgroundKind::Reflection), 0);
let mut changed = false;
while let Ok(e) = rx.try_recv() {
if matches!(e, AppEvent::SelfModelChanged) {
changed = true;
}
}
assert!(changed);
}
#[test]
fn a_stopped_task_touches_neither_the_streak_nor_the_success_path() {
let (_d, mut orch, mut rx) = bare_orch_rx();
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Failed("boom".into()),
None,
);
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Failed("boom".into()),
None,
);
assert_eq!(orch.bg_failures(BackgroundKind::Reflection), 2);
let token = tokio_util::sync::CancellationToken::new();
orch.begin_bg(BackgroundKind::Reflection, token.clone(), None);
while rx.try_recv().is_ok() {}
orch.handle_stop_background_task(BackgroundKind::Reflection);
assert!(
token.is_cancelled(),
"the slot's token is what the stop cancels"
);
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Cancelled { consumed: false },
None,
);
assert_eq!(orch.bg_failures(BackgroundKind::Reflection), 2, "untouched");
assert!(!orch.bg_running(BackgroundKind::Reflection));
let mut events = Vec::new();
while let Ok(e) = rx.try_recv() {
events.push(e);
}
assert!(events.iter().any(|e| matches!(
e,
AppEvent::BackgroundTask {
kind: BackgroundKind::Reflection,
active: false
}
)));
assert!(events.iter().any(|e| matches!(e, AppEvent::TaskList(_))));
assert!(
events
.iter()
.any(|e| matches!(e, AppEvent::SelfModelChanged))
);
assert!(
!events.iter().any(|e| matches!(e, AppEvent::Error(_))),
"a stop is not a failure: {events:?}"
);
}
#[test]
fn stopping_an_idle_kind_does_nothing() {
let (_d, orch, mut rx) = bare_orch_rx();
orch.handle_stop_background_task(BackgroundKind::Consolidation);
assert!(!orch.bg_running(BackgroundKind::Consolidation));
assert!(rx.try_recv().is_err(), "nothing to announce");
}
fn watermark(orch: &Orchestrator, chat_id: Uuid) -> (Option<usize>, bool) {
let chat = orch.chats.iter().find(|c| c.id == chat_id).unwrap();
(chat.reflected_upto, chat.reflected_at.is_some())
}
fn refund(window: Window) -> Refund {
Refund {
window,
acted: Arc::new(Acted::default()),
}
}
#[tokio::test]
async fn a_reflection_stopped_before_its_first_round_gives_the_window_back() {
let (_d, mut orch, chat_id) = orch_ready_for_reflection();
orch.engines.backend = Some(Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>);
orch.maybe_auto_reflect(chat_id);
assert_eq!(
watermark(&orch, chat_id),
(Some(2), true),
"advanced at spawn"
);
orch.saves.take();
orch.handle_stop_background_task(BackgroundKind::Reflection);
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Cancelled { consumed: false },
None,
);
assert_eq!(
watermark(&orch, chat_id),
(None, false),
"the window is unread again"
);
assert!(
orch.saves.is_dirty(chat_id),
"the refund is saved like the advance"
);
assert!(!orch.bg_running(BackgroundKind::Reflection));
orch.maybe_auto_reflect(chat_id);
assert!(orch.bg_running(BackgroundKind::Reflection));
assert_eq!(watermark(&orch, chat_id), (Some(2), true));
}
#[tokio::test]
async fn a_reflection_stopped_after_a_round_keeps_its_advance() {
let (_d, mut orch, chat_id) = orch_ready_for_reflection();
orch.engines.backend = Some(Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>);
orch.maybe_auto_reflect(chat_id);
orch.saves.take();
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Cancelled { consumed: true },
None,
);
assert_eq!(watermark(&orch, chat_id), (Some(2), true), "kept");
assert!(!orch.saves.is_dirty(chat_id), "nothing to save");
}
#[tokio::test]
async fn a_finished_or_failed_reflection_keeps_its_advance() {
for outcome in [BgOutcome::Done, BgOutcome::Failed("boom".into())] {
let (_d, mut orch, chat_id) = orch_ready_for_reflection();
orch.engines.backend = Some(Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>);
orch.maybe_auto_reflect(chat_id);
orch.handle_bg_done(BackgroundKind::Reflection, outcome.clone(), None);
assert_eq!(watermark(&orch, chat_id), (Some(2), true), "{outcome:?}");
}
}
#[test]
fn a_consolidation_stopped_before_its_first_round_gets_its_count_back() {
let (_d, mut orch, _rx) = bare_orch_rx();
let chat = Uuid::new_v4();
for (outcome, expected) in [
(BgOutcome::Cancelled { consumed: false }, 3 + 5),
(BgOutcome::Cancelled { consumed: true }, 3),
(BgOutcome::Done, 3),
(BgOutcome::Failed("boom".into()), 3),
] {
orch.consolidate_counts.insert(chat, 3);
orch.begin_bg(
BackgroundKind::Consolidation,
CancellationToken::new(),
Some(refund(Window::Counter { chat, count: 5 })),
);
orch.handle_bg_done(BackgroundKind::Consolidation, outcome.clone(), None);
assert_eq!(
orch.consolidate_counts.get(&chat),
Some(&expected),
"{outcome:?}"
);
}
}
#[test]
fn a_refund_for_a_chat_that_is_gone_does_nothing() {
let (_d, mut orch, _rx) = bare_orch_rx();
let chat = Uuid::new_v4();
orch.begin_bg(
BackgroundKind::Reflection,
CancellationToken::new(),
Some(refund(Window::Reflection {
chat,
upto: None,
at: None,
})),
);
orch.handle_bg_done(
BackgroundKind::Reflection,
BgOutcome::Cancelled { consumed: false },
None,
);
assert!(!orch.saves.is_dirty(chat));
}
#[tokio::test]
async fn a_quit_before_the_first_round_gives_the_window_back_and_flushes_it() {
let (dir, mut orch, chat_id) = orch_ready_for_reflection();
orch.engines.backend = Some(Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>);
orch.maybe_auto_reflect(chat_id);
assert_eq!(watermark(&orch, chat_id), (Some(2), true));
orch.flush_saves();
assert_eq!(
super::subagent::load(dir.path(), chat_id).reflected_upto,
Some(2),
"the advance was flushed before the quit"
);
orch.cancel_bg_all();
orch.refund_unlanded();
assert_eq!(watermark(&orch, chat_id), (None, false));
assert!(orch.saves.is_dirty(chat_id));
orch.flush_saves();
let on_disk = super::subagent::load(dir.path(), chat_id);
assert_eq!(
on_disk.reflected_upto, None,
"the next launch reads the window"
);
assert_eq!(on_disk.reflected_at, None);
}
#[test]
fn a_quit_keeps_an_acted_window_and_ignores_the_roll() {
let (_d, mut orch, _rx) = bare_orch_rx();
let chat = Uuid::new_v4();
orch.consolidate_counts.insert(chat, 3);
let token = CancellationToken::new();
orch.begin_bg(
BackgroundKind::Consolidation,
token.clone(),
Some(Refund {
window: Window::Counter { chat, count: 5 },
acted: Arc::new(Acted::at(Acting::Wrote)),
}),
);
let roll = CancellationToken::new();
orch.begin_bg(BackgroundKind::Compaction, roll.clone(), None);
orch.cancel_bg_all();
orch.refund_unlanded();
assert!(
token.is_cancelled() && roll.is_cancelled(),
"every task ended"
);
assert_eq!(orch.consolidate_counts.get(&chat), Some(&3), "kept: acted");
}
#[test]
fn a_quit_during_a_round_of_tools_keeps_the_window() {
let (_d, mut orch, _rx) = bare_orch_rx();
let chat = Uuid::new_v4();
orch.consolidate_counts.insert(chat, 3);
orch.begin_bg(
BackgroundKind::Consolidation,
CancellationToken::new(),
Some(Refund {
window: Window::Counter { chat, count: 5 },
acted: Arc::new(Acted::at(Acting::InTools)),
}),
);
orch.cancel_bg_all();
orch.refund_unlanded();
assert_eq!(
orch.consolidate_counts.get(&chat),
Some(&3),
"kept: mid-tools"
);
}