Test agent TTS routing in settings layer

Signed-off-by: John Tennant <jtennant@squareup.com>
This commit is contained in:
John Tennant
2026-07-29 12:20:50 -04:00
committed by John Tennant
parent 145e051fff
commit c08885c2d9
2 changed files with 50 additions and 9 deletions
@@ -0,0 +1,26 @@
use super::{enqueue_agent_tts_text, normalize_agent_tts_text, MAX_TTS_TEXT_LEN};
#[tokio::test]
async fn assistant_plain_text_routes_unchanged_into_voice_pipeline_boundary() {
let (sender, receiver) = std::sync::mpsc::channel();
let text = "A newly submitted assistant reply.".to_string();
enqueue_agent_tts_text(text.clone(), move |queued| {
sender.send(queued).map_err(|error| error.to_string())
})
.await
.expect("route assistant text");
assert_eq!(receiver.recv().expect("queued text"), text);
}
#[test]
fn assistant_text_truncation_is_unicode_safe_before_voice_routing() {
let input = "🦀".repeat(MAX_TTS_TEXT_LEN + 1);
let output = normalize_agent_tts_text(input);
assert_eq!(
output.chars().count(),
MAX_TTS_TEXT_LEN + "... message truncated.".chars().count()
);
assert!(output.ends_with("... message truncated."));
}
+24 -9
View File
@@ -825,6 +825,25 @@ pub fn get_model_status(_state: State<'_, AppState>) -> Result<models::VoiceMode
/// ~2000 chars ≈ 1–2 minutes of speech. Longer messages are truncated.
const MAX_TTS_TEXT_LEN: usize = 2000;
fn normalize_agent_tts_text(text: String) -> String {
if text.chars().count() > MAX_TTS_TEXT_LEN {
let mut truncated: String = text.chars().take(MAX_TTS_TEXT_LEN).collect();
truncated.push_str("... message truncated.");
truncated
} else {
text
}
}
async fn enqueue_agent_tts_text<F>(text: String, enqueue: F) -> Result<(), String>
where
F: FnOnce(String) -> Result<(), String> + Send + 'static,
{
tokio::task::spawn_blocking(move || enqueue(text))
.await
.map_err(|error| format!("TTS enqueue task failed: {error}"))?
}
/// Called by the WebView when it receives an incoming agent kind:9 message.
/// Lazily starts the TTS pipeline if models are ready but the pipeline hasn't
/// been created yet (e.g. models finished downloading after huddle started).
@@ -834,13 +853,7 @@ const MAX_TTS_TEXT_LEN: usize = 2000;
pub async fn speak_agent_message(text: String, state: State<'_, AppState>) -> Result<(), String> {
// Truncate oversized messages — agents shouldn't monologue in a voice huddle.
// Use char count (not byte length) to avoid panicking on multi-byte UTF-8.
let text = if text.chars().count() > MAX_TTS_TEXT_LEN {
let mut truncated: String = text.chars().take(MAX_TTS_TEXT_LEN).collect();
truncated.push_str("... message truncated.");
truncated
} else {
text
};
let text = normalize_agent_tts_text(text);
let needs_pipeline = {
let hs = state.huddle()?;
@@ -869,15 +882,17 @@ pub async fn speak_agent_message(text: String, state: State<'_, AppState>) -> Re
let Some(sender) = sender else {
return Ok(());
};
tokio::task::spawn_blocking(move || {
enqueue_agent_tts_text(text, move |text| {
sender
.send(text)
.map_err(|error| format!("TTS queue closed while waiting to enqueue: {error}"))
})
.await
.map_err(|error| format!("TTS enqueue task failed: {error}"))?
}
#[cfg(test)]
mod agent_tts_routing_tests;
/// Add an agent to the active huddle.
///
/// Steps: