Files
codex/codex-rs/utils/audio/src/audio_preparation_tests.rs
T
rka-oai 763787d061 Support standalone named function call outputs (#39782)
## Why

External tool events may need to enter thread history without a preceding function call and therefore do not have a `call_id`.

## What changed

- Allow `function_call_output` items to omit `call_id` and carry optional `name` and `namespace` fields.
- Preserve named standalone outputs during history normalization and agent forks while retaining existing pairing behavior for outputs with a `call_id`.
- Accept, persist, and forward these outputs through `thread/inject_items`, and update the app-server schemas and documentation.

## Testing

- Cover paired and standalone JSON round trips, history normalization, agent forks, and injected thread history.

GitOrigin-RevId: a3258163a7dc93777c7c3023116fe204819bdbb0
2026-08-20 19:19:41 +00:00

164 lines
5.4 KiB
Rust

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 {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputAudio {
audio_url: "data:audio/x-wav;base64,YXVkaW8=".to_string(),
},
ContentItem::InputAudio {
audio_url: "data:audio/ogg;base64,YXVkaW8=".to_string(),
},
ContentItem::InputAudio {
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 {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputAudio {
audio_url: "data:audio/wav;base64,YXVkaW8=".to_string(),
},
ContentItem::InputAudio {
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 {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputAudio {
audio_url: "data:audio/wav;base64,%%%".to_string(),
},
ContentItem::InputAudio {
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 {
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::InputAudio {
audio_url: "data:audio/wav;base64,YXVkaW8=".to_string(),
},
FunctionCallOutputContentItem::InputAudio {
audio_url: "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::InputAudio {
audio_url: "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);
}
}