use std::time::Duration;
use tokio::sync::{broadcast, mpsc, oneshot};
use super::{EngineConfig, EngineEvent, EngineHandle, Segment, TranscriptionEngine};
#[derive(Debug, Clone)]
pub enum MockEvent {
ModelLoaded(String),
Tentative(String),
Committed {
t_start_ms: u64,
t_end_ms: u64,
text: String,
},
}
pub struct MockEngine {
script: Vec<MockEvent>,
}
impl MockEngine {
pub fn new(script: Vec<MockEvent>) -> Self {
Self { script }
}
}
impl TranscriptionEngine for MockEngine {
fn start(&mut self, cfg: EngineConfig) -> anyhow::Result<EngineHandle> {
match cfg {
EngineConfig::Local { .. } => {}
EngineConfig::Cloud { .. } => {
anyhow::bail!("MockEngine does not support EngineConfig::Cloud");
}
}
let (pcm_tx, mut pcm_rx) = mpsc::channel::<Vec<f32>>(16);
let (events_tx, events_rx) = broadcast::channel::<EngineEvent>(64);
let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
let script = std::mem::take(&mut self.script);
tokio::spawn(async move {
let mut iter = script.into_iter();
loop {
tokio::select! {
_ = &mut shutdown_rx => break,
msg = pcm_rx.recv() => {
if msg.is_none() { break; }
let Some(evt) = iter.next() else { continue; };
let out = match evt {
MockEvent::ModelLoaded(name) => EngineEvent::ModelLoaded { name },
MockEvent::Tentative(text) => EngineEvent::Tentative(text),
MockEvent::Committed { t_start_ms, t_end_ms, text } =>
EngineEvent::Committed(Segment {
t_start: Duration::from_millis(t_start_ms),
t_end: Duration::from_millis(t_end_ms),
text,
tokens: Vec::new(),
}),
};
let _ = events_tx.send(out);
}
}
}
});
Ok(EngineHandle {
pcm_tx,
events_rx,
shutdown: shutdown_tx,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn emits_scripted_events_in_order() {
let script = vec![
MockEvent::ModelLoaded("mock".into()),
MockEvent::Tentative("hel".into()),
MockEvent::Tentative("hello".into()),
MockEvent::Committed {
t_start_ms: 0,
t_end_ms: 1000,
text: "hello".into(),
},
];
let mut engine = MockEngine::new(script);
let handle = engine.start(test_cfg()).unwrap();
let mut rx = handle.events_rx;
for _ in 0..4 {
handle.pcm_tx.send(vec![0.0; 16]).await.unwrap();
}
let e1 = rx.recv().await.unwrap();
assert!(matches!(e1, EngineEvent::ModelLoaded { .. }));
let e2 = rx.recv().await.unwrap();
assert!(matches!(e2, EngineEvent::Tentative(s) if s == "hel"));
let e3 = rx.recv().await.unwrap();
assert!(matches!(e3, EngineEvent::Tentative(s) if s == "hello"));
let e4 = rx.recv().await.unwrap();
assert!(matches!(e4, EngineEvent::Committed(seg) if seg.text == "hello"));
}
fn test_cfg() -> EngineConfig {
EngineConfig::Local {
model_path: std::path::PathBuf::from("/dev/null"),
language: None,
sample_rate: 16_000,
hop_ms: 750,
min_window_ms: 1000,
}
}
#[tokio::test]
async fn mock_engine_rejects_cloud_variant() {
let mut engine = MockEngine::new(vec![]);
let cfg = EngineConfig::Cloud {
api_key: "sk-x".into(),
language: None,
sample_rate: 16_000,
server_url: "wss://example.test/api/audio/stream".into(),
device_name: "mock-device".into(),
app_name: String::new(),
};
let err = engine.start(cfg).err().expect("expected Err on Cloud");
assert!(
err.to_string().to_lowercase().contains("cloud"),
"error should mention cloud variant; got: {err}",
);
}
}