use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use skadoosh::error::Result;
use skadoosh::llm::LlmBackend;
use skadoosh::pipeline::{run_orchestrator, ClipSink, Topology, VadEventMsg};
use skadoosh::stt::MockStt;
use skadoosh::tts::{MockTts, TtsClip};
use skadoosh::AgentEvent;
use tokio::sync::{broadcast, mpsc};
use tokio_util::sync::CancellationToken;
struct FakeLlm {
script: Mutex<VecDeque<Vec<String>>>,
}
impl LlmBackend for FakeLlm {
fn name(&self) -> &str {
"fake-llm"
}
fn stream_reply<'a>(
&'a mut self,
user: &'a str,
clauses: mpsc::Sender<String>,
_cancel: CancellationToken,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
let script = self
.script
.lock()
.expect("script lock")
.pop_front()
.unwrap_or_else(|| vec![format!("(no scripted reply for {user:?})")]);
for clause in script {
clauses
.send(clause)
.await
.map_err(|_| anyhow::anyhow!("clause consumer gone"))?;
}
Ok(())
})
}
fn clear_history(&mut self) {
}
}
#[derive(Clone, Default)]
struct RecordingSink {
clips: Arc<Mutex<Vec<TtsClip>>>,
flushes: Arc<Mutex<u64>>,
}
impl ClipSink for RecordingSink {
async fn queue_clip(&self, clip: TtsClip) -> Result<()> {
self.clips.lock().expect("clips lock").push(clip);
Ok(())
}
fn flush(&self) {
*self.flushes.lock().expect("flushes lock") += 1;
}
fn is_playing(&self) -> bool {
false
}
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
let stt = MockStt::from_replies(["what is the airspeed of an unladen swallow?"]);
let llm = FakeLlm {
script: Mutex::new(VecDeque::from(vec![vec![
"African or European?".to_string(),
" I don't know that.".to_string(),
]])),
};
let sink = RecordingSink::default();
let (vad_tx, vad_rx) = mpsc::channel(8);
let (fatal_tx, fatal_rx) = mpsc::channel(8);
let (events, mut events_rx) = broadcast::channel(64);
let shutdown = CancellationToken::new();
let observer = tokio::spawn(async move {
let mut seen = Vec::new();
while let Ok(event) = events_rx.recv().await {
println!("event: {event:?}");
seen.push(event);
}
seen
});
let orchestrator = tokio::spawn(run_orchestrator(Topology {
vad_events: vad_rx,
fatal_tx,
fatal_rx,
stt: Box::new(stt),
llm: Box::new(llm),
tts_engine: Some(Box::new(MockTts::new())),
sink: sink.clone(),
shutdown: shutdown.clone(),
events,
wake_word: None,
hold_music: None,
watch_rx: None,
}));
vad_tx
.send(VadEventMsg::Segment {
samples: vec![0.0; 16_000],
t_speech_end: Instant::now(),
})
.await
.map_err(|_| anyhow::anyhow!("orchestrator gone"))?;
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let clips = sink.clips.lock().expect("clips lock").len();
if clips >= 2 {
break;
}
assert!(Instant::now() < deadline, "timed out waiting for clips");
tokio::time::sleep(Duration::from_millis(10)).await;
}
shutdown.cancel();
tokio::time::timeout(Duration::from_secs(5), orchestrator)
.await
.expect("orchestrator hung on shutdown")
.expect("orchestrator panicked")?;
drop(vad_tx); let seen = tokio::time::timeout(Duration::from_secs(5), observer)
.await
.expect("observer hung")
.expect("observer panicked");
let clips = sink.clips.lock().expect("clips lock");
assert_eq!(clips.len(), 2, "one MockTts clip per FakeLlm clause");
assert!(clips.iter().all(|c| c.sample_rate == 24_000));
assert!(clips.iter().all(|c| !c.samples.is_empty()));
assert_eq!(
*sink.flushes.lock().expect("flushes lock"),
0,
"no barge-in"
);
assert!(
seen.iter()
.any(|e| matches!(e, AgentEvent::Transcript(t) if t.contains("unladen swallow"))),
"MockStt's scripted transcript flowed through"
);
assert!(
seen.iter()
.any(|e| matches!(e, AgentEvent::Clause(c) if c == "African or European?")),
"FakeLlm's first clause flowed through"
);
let order: Vec<&'static str> = seen
.iter()
.map(|e| match e {
AgentEvent::Listening => "Listening",
AgentEvent::SpeechStart => "SpeechStart",
AgentEvent::Transcript(_) => "Transcript",
AgentEvent::Clause(_) => "Clause",
AgentEvent::ReplyDone => "ReplyDone",
AgentEvent::TurnCancelled => "TurnCancelled",
AgentEvent::ToolCall { .. } => "ToolCall",
AgentEvent::StageLatency { .. } => "StageLatency",
AgentEvent::Error(_) => "Error",
})
.collect();
assert_eq!(
order,
vec![
"Listening",
"Transcript",
"Clause",
"Clause",
"ReplyDone",
"Listening"
],
"full-turn event order (seen: {order:?})"
);
println!("mock_agent: full plugin turn OK (2 clauses → 2 clips)");
Ok(())
}