use std::fmt::Write;
use std::sync::Arc;
use super::{Frame, McpObserver, ProgressPump, Receiver};
use crate::levels::recording;
use promptforge_core::execute::{self, ResolutionContext, RunConfig};
use promptforge_core::model::ModelCatalog;
use promptforge_core::observe::{NullObserver, Observation, Observer};
use promptforge_core::parser::Prompt;
use promptforge_core::store::StoreRef;
use promptforge_tool_picker::{Catalog, Config, ToolPicker};
use tracing::Level;
fn drain(frames: &mut Receiver<Frame>) -> Vec<Frame> {
let mut drained = Vec::new();
while let Ok(frame) = frames.try_recv() {
drained.push(frame);
}
drained
}
fn frame(progress: u32, message: &str) -> Frame {
Frame {
progress,
message: message.to_owned(),
}
}
fn long_prompt(sections: usize) -> String {
let mut source = String::from(
"---\nname: long\ndescription: Many sections\npromptforge: 1\n---\n\n# Test prompt\n",
);
for section in 1..sections {
let _written = write!(
source,
"\n## S{section}\n\n```lua\nvar.step = {section}\n```\n"
);
}
let _written = write!(
source,
"\n## S{sections}\n\n```lua\nreturn 'long done'\n```\n"
);
source
}
fn three_section_run() -> Vec<(&'static str, Observation)> {
vec![
("Trio", Observation::RunStarted),
("First", Observation::SectionStarted),
("First", Observation::ModelTurnCompleted),
("First", Observation::ToolCallSucceeded),
("First", Observation::SectionFinished),
("Second", Observation::SectionStarted),
("Second", Observation::SectionFinished),
("Third", Observation::SectionStarted),
("Third", Observation::SectionFinished),
("Trio", Observation::RunSucceeded),
]
}
#[test]
fn a_run_frames_its_start_and_each_section_and_nothing_else() {
let (observer, mut frames) = McpObserver::queued();
for (section, report) in three_section_run() {
observer.observe("test-run", section, report);
}
assert_eq!(
drain(&mut frames),
vec![
frame(0, "Trio"),
frame(1, "First"),
frame(2, "Second"),
frame(3, "Third"),
]
);
assert_eq!(observer.turns(), 1, "the run's own total is what is kept");
assert_eq!(observer.dropped(), 0);
}
#[test]
fn progress_counts_recognized_section_starts() {
let (observer, mut frames) = McpObserver::queued();
for section in ["one", "two", "three"] {
observer.observe("test-run", section, Observation::SectionStarted);
}
let progress: Vec<u32> = drain(&mut frames).iter().map(|f| f.progress).collect();
assert_eq!(progress, vec![1, 2, 3]);
}
#[tokio::test]
async fn a_pump_that_never_drains_still_lets_the_run_finish() {
let sections = super::CAPACITY + 16;
let source = long_prompt(sections);
let prompt = Prompt::parse(&source, "test-run", &NullObserver::default())
.expect("the fixture prompt parses");
let (observer, _frames) = McpObserver::queued();
let observer = Arc::new(observer);
let store = StoreRef::memory();
let models = ModelCatalog::empty();
let picker =
ToolPicker::build(Catalog::default(), Config::default()).expect("empty picker must build");
let value = execute::run(
&prompt,
"",
ResolutionContext::new(&picker, &models),
&[],
&store,
RunConfig::new("test-run").observer(Arc::clone(&observer) as Arc<dyn Observer>),
)
.await
.expect("a Lua-only run reaches no model and finishes");
assert_eq!(value, "long done");
assert!(
observer.dropped() > 0,
"a run past the queue's capacity drops frames rather than stalling"
);
}
#[tokio::test(start_paused = true)]
async fn a_pump_the_peer_never_accepts_is_abandoned() {
let pump = ProgressPump::from_task(tokio::spawn(std::future::pending()));
let started = tokio::time::Instant::now();
pump.finish().await;
assert_eq!(
started.elapsed(),
super::pump::FLUSH_GRACE,
"the flush waits its grace and no longer"
);
}
#[test]
fn the_run_start_and_both_within_run_failures_reach_the_default_level() {
let (levels, _recording) = recording();
let observer = McpObserver::silent();
for (section, report) in three_section_run() {
observer.observe("test-run", section, report);
}
observer.observe("test-run", "First", Observation::ToolCallFailed);
observer.observe("test-run", "First", Observation::ModelTurnFailed);
assert_eq!(
levels.operator_visible(),
vec![Level::INFO, Level::WARN, Level::WARN],
"the run start, then the failed tool call and the failed model turn"
);
}
#[test]
fn a_closed_queue_counts_a_disconnect_apart_from_a_full_drop() {
let (observer, frames) = McpObserver::queued();
drop(frames);
observer.observe("test-run", "First", Observation::SectionStarted);
assert_eq!(observer.disconnected(), 1, "a closed queue is a disconnect");
assert_eq!(
observer.dropped(),
0,
"and never folded into the full-queue drop count"
);
}
#[test]
fn a_silent_observer_counts_turns_without_a_queue() {
let observer = McpObserver::silent();
for (section, report) in three_section_run() {
observer.observe("test-run", section, report);
}
assert_eq!(observer.turns(), 1);
assert_eq!(observer.dropped(), 0, "there is nothing to drop into");
}
#[test]
fn unknown_details_are_tolerated_without_frames_or_counters() {
let (observer, mut frames) = McpObserver::queued();
for report in [
Observation::ToolRegistryValidationStarted,
Observation::ToolRegistryValidationSucceeded,
Observation::ToolRegistryValidationFailed,
Observation::Other("A future detail".to_owned()),
] {
observer.observe("test-run", "First", report);
}
assert!(drain(&mut frames).is_empty());
assert_eq!(observer.turns(), 0);
assert_eq!(observer.dropped(), 0);
}