Files
codex/codex-rs/utils/audio/src/audio_preparation_tests.rs
T
Hung 9018f3a3b0 Preserve file-backed audio in tool outputs (#60735)
## Why

Converting message content to tool output previously reduced file-backed audio to a text file ID, losing its audio type.

## What changed

- Allow `input_audio` tool output items to carry either `audio_url` or `file_id`, preserving both forms during content conversion.
- Account for file-backed tool audio in history estimates and output truncation, and prepare only inline audio data.
- Reject uploaded audio in raw function and custom tool output injection, extending the existing requirement to submit uploaded audio through typed turn input.
- Regenerate app-server schemas and TypeScript/Python types while preserving the Python inline audio class name.

## Testing

Add coverage for tool audio wire-format round trips, repeated file audio sharing text and audio output budgets, and rejection of raw uploaded audio injection across messages and tool outputs. Extend Python SDK coverage for the inline tool audio shape.

GitOrigin-RevId: f4704466e6151662d2c8b8dc3f5ca2ab2f5710a2
2026-11-11 06:41:15 +00:00

213 lines
7.0 KiB
Rust

use codex_protocol::models::AudioReference;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputPayload;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn preparation_canonicalizes_data_urls_and_rejects_remote_urls() {
let mut items = vec![ResponseItem::Message {
status: None,
encrypted_content: None,
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputAudio {
audio: AudioReference::File {
file_id: "file_audio".into(),
},
},
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "data:audio/x-wav;base64,YXVkaW8=".to_string(),
},
},
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "data:audio/ogg;base64,YXVkaW8=".to_string(),
},
},
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "https://example.com/audio.mp3".to_string(),
},
},
],
phase: None,
internal_chat_message_metadata_passthrough: None,
}];
prepare_response_items(&mut items);
assert_eq!(
items,
vec![ResponseItem::Message {
encrypted_content: None,
status: None,
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputAudio {
audio: AudioReference::File {
file_id: "file_audio".into(),
}
},
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "data:audio/wav;base64,YXVkaW8=".to_string()
}
},
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "data:audio/ogg;base64,YXVkaW8=".to_string()
}
},
ContentItem::InputText {
text: "audio content omitted because it could not be processed".to_string(),
},
],
phase: None,
internal_chat_message_metadata_passthrough: None,
}]
);
}
#[test]
fn preparation_replaces_invalid_message_audio_with_placeholders() {
let mut items = vec![ResponseItem::Message {
status: None,
encrypted_content: None,
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "data:audio/wav;base64,%%%".to_string(),
},
},
ContentItem::InputAudio {
audio: AudioReference::Inline {
audio_url: "data:audio/flac;base64,YXVkaW8=".to_string(),
},
},
],
phase: None,
internal_chat_message_metadata_passthrough: None,
}];
prepare_response_items(&mut items);
assert_eq!(
items,
vec![ResponseItem::Message {
encrypted_content: None,
status: None,
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "audio content omitted because it could not be processed".to_string(),
},
ContentItem::InputText {
text: "audio content omitted because its format is not supported; use wav, mp3, m4a, webm, or ogg".to_string(),
},
],
phase: None,
internal_chat_message_metadata_passthrough: None,
}]
);
}
#[test]
fn preparation_replaces_only_failed_tool_audio_and_preserves_metadata() {
let mut items = vec![ResponseItem::FunctionCallOutput {
id: None,
call_id: Some("call-1".to_string()),
name: None,
namespace: None,
output: FunctionCallOutputPayload {
body: FunctionCallOutputBody::ContentItems(vec![
FunctionCallOutputContentItem::InputText {
text: "before".to_string(),
},
FunctionCallOutputContentItem::inline_audio(
"data:audio/wav;base64,YXVkaW8=".to_string(),
),
FunctionCallOutputContentItem::inline_audio(
"data:audio/wav,not-base64".to_string(),
),
]),
success: Some(true),
},
internal_chat_message_metadata_passthrough: None,
}];
prepare_response_items(&mut items);
assert_eq!(
items,
vec![ResponseItem::FunctionCallOutput {
id: None,
call_id: Some("call-1".to_string()),
name: None,
namespace: None,
output: FunctionCallOutputPayload {
body: FunctionCallOutputBody::ContentItems(vec![
FunctionCallOutputContentItem::InputText {
text: "before".to_string(),
},
FunctionCallOutputContentItem::inline_audio(
"data:audio/wav;base64,YXVkaW8=".to_string()
),
FunctionCallOutputContentItem::InputText {
text: "audio content omitted because it could not be processed".to_string(),
},
]),
success: Some(true),
},
internal_chat_message_metadata_passthrough: None,
}]
);
}
#[test]
fn preparation_errors_map_to_expected_placeholders() {
let cases = [
(
AudioPreparationError::InvalidDataUrl {
reason: "details remain in logs",
},
"audio content omitted because it could not be processed",
),
(
AudioPreparationError::UnsupportedFormat,
"audio content omitted because its format is not supported; use wav, mp3, m4a, webm, or ogg",
),
(
AudioPreparationError::AudioTooLarge { size: usize::MAX },
"audio content omitted because it exceeded the supported size limit; use a smaller audio file",
),
];
for (error, expected) in cases {
assert_eq!(error.placeholder(), expected);
}
}
#[test]
fn file_audio_uses_the_measured_length_or_the_full_file_budget() {
use std::time::Duration;
for (duration, expected) in [
(None, 12_000),
(Some(Duration::ZERO), 12_000),
(Some(Duration::from_nanos(1)), 1),
(Some(Duration::from_secs(2)), 20),
(Some(Duration::from_millis(2_001)), 21),
(Some(Duration::from_secs(20 * 60)), 12_000),
(Some(Duration::MAX), 12_000),
] {
assert_eq!(super::estimate_file_audio_tokens(duration), expected);
}
}