mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
Full rename of all identifiers, filenames, and references: ironclaw → optimclaw IronClaw → OptimClaw IRONCLAW → OPTIMCLAW ironclaw_common → optimclaw_common ironclaw_safety → optimclaw_safety Upstream: nearai/ironclaw
211 lines
7.4 KiB
Rust
211 lines
7.4 KiB
Rust
//! E2E tests for attachment processing in the LLM pipeline.
|
|
//!
|
|
//! Verifies that attachments on incoming messages are augmented into the user
|
|
//! text and (for images) passed as multimodal content parts to the LLM.
|
|
|
|
#[cfg(feature = "libsql")]
|
|
mod support;
|
|
|
|
#[cfg(feature = "libsql")]
|
|
mod attachment_tests {
|
|
use std::time::Duration;
|
|
|
|
use crate::support::test_rig::TestRigBuilder;
|
|
use crate::support::trace_llm::LlmTrace;
|
|
|
|
use optimclaw::channels::{AttachmentKind, IncomingAttachment, IncomingMessage};
|
|
use optimclaw::llm::ContentPart;
|
|
|
|
const FIXTURES: &str = concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/tests/fixtures/llm_traces/spot"
|
|
);
|
|
const TIMEOUT: Duration = Duration::from_secs(15);
|
|
|
|
fn make_attachment(kind: AttachmentKind) -> IncomingAttachment {
|
|
IncomingAttachment {
|
|
id: "att-1".to_string(),
|
|
kind,
|
|
mime_type: "application/octet-stream".to_string(),
|
|
filename: None,
|
|
size_bytes: None,
|
|
source_url: None,
|
|
storage_key: None,
|
|
extracted_text: None,
|
|
data: vec![],
|
|
duration_secs: None,
|
|
}
|
|
}
|
|
|
|
/// Audio attachment with transcript reaches the LLM as augmented text.
|
|
#[tokio::test]
|
|
async fn attachment_audio_transcript_reaches_llm() {
|
|
let trace =
|
|
LlmTrace::from_file(format!("{FIXTURES}/attachment_audio_transcript.json")).unwrap();
|
|
let rig = TestRigBuilder::new()
|
|
.with_trace(trace.clone())
|
|
.build()
|
|
.await;
|
|
|
|
// Build a message with an audio attachment containing a transcript
|
|
let mut att = make_attachment(AttachmentKind::Audio);
|
|
att.filename = Some("voice.ogg".to_string());
|
|
att.mime_type = "audio/ogg".to_string();
|
|
att.extracted_text = Some("Hello, can you help me with my project?".to_string());
|
|
att.duration_secs = Some(5);
|
|
|
|
let mut msg = IncomingMessage::new("test", "test-user", "Check this voice note");
|
|
msg.attachments.push(att);
|
|
|
|
rig.send_incoming(msg).await;
|
|
let responses = rig.wait_for_responses(1, TIMEOUT).await;
|
|
|
|
// Verify the response was received
|
|
assert!(
|
|
!responses.is_empty(),
|
|
"should receive at least one response"
|
|
);
|
|
|
|
// Verify the augmented content reached the LLM
|
|
let requests = rig.captured_llm_requests();
|
|
assert!(!requests.is_empty(), "LLM should have been called");
|
|
|
|
let last_request = &requests[requests.len() - 1];
|
|
let last_user_msg = last_request
|
|
.iter()
|
|
.rev()
|
|
.find(|m| matches!(m.role, optimclaw::llm::Role::User))
|
|
.expect("should have a user message");
|
|
|
|
// The augmented text should contain the attachment tags and transcript
|
|
assert!(
|
|
last_user_msg.content.contains("<attachments>"),
|
|
"user message should contain <attachments> block, got: {}",
|
|
last_user_msg.content.chars().take(200).collect::<String>()
|
|
);
|
|
assert!(
|
|
last_user_msg
|
|
.content
|
|
.contains("Hello, can you help me with my project?"),
|
|
"user message should contain the transcript"
|
|
);
|
|
assert!(
|
|
last_user_msg.content.contains("duration=\"5s\""),
|
|
"user message should contain duration"
|
|
);
|
|
|
|
// Audio attachments should NOT produce image content parts
|
|
assert!(
|
|
last_user_msg.content_parts.is_empty(),
|
|
"audio attachments should not produce image content parts"
|
|
);
|
|
|
|
rig.verify_trace_expects(&trace, &responses);
|
|
rig.shutdown();
|
|
}
|
|
|
|
/// Image attachment with data reaches the LLM with multimodal content parts.
|
|
#[tokio::test]
|
|
async fn attachment_image_produces_content_parts() {
|
|
let trace = LlmTrace::from_file(format!("{FIXTURES}/attachment_image.json")).unwrap();
|
|
let rig = TestRigBuilder::new()
|
|
.with_trace(trace.clone())
|
|
.build()
|
|
.await;
|
|
|
|
// Build a message with an image attachment that has raw data
|
|
let mut att = make_attachment(AttachmentKind::Image);
|
|
att.filename = Some("screenshot.png".to_string());
|
|
att.mime_type = "image/png".to_string();
|
|
att.size_bytes = Some(1024);
|
|
att.data = vec![0x89, 0x50, 0x4E, 0x47]; // PNG magic bytes (fake)
|
|
|
|
let mut msg =
|
|
IncomingMessage::new("test", "test-user", "What do you see in this screenshot?");
|
|
msg.attachments.push(att);
|
|
|
|
rig.send_incoming(msg).await;
|
|
let responses = rig.wait_for_responses(1, TIMEOUT).await;
|
|
|
|
assert!(
|
|
!responses.is_empty(),
|
|
"should receive at least one response"
|
|
);
|
|
|
|
// Verify multimodal content parts reached the LLM
|
|
let requests = rig.captured_llm_requests();
|
|
assert!(!requests.is_empty(), "LLM should have been called");
|
|
|
|
let last_request = &requests[requests.len() - 1];
|
|
let last_user_msg = last_request
|
|
.iter()
|
|
.rev()
|
|
.find(|m| matches!(m.role, optimclaw::llm::Role::User))
|
|
.expect("should have a user message");
|
|
|
|
// Should have image content parts
|
|
assert_eq!(
|
|
last_user_msg.content_parts.len(),
|
|
1,
|
|
"should have exactly one image content part"
|
|
);
|
|
|
|
// Verify the content part is an ImageUrl with a data: URI
|
|
match &last_user_msg.content_parts[0] {
|
|
ContentPart::ImageUrl { image_url } => {
|
|
assert!(
|
|
image_url.url.starts_with("data:image/png;base64,"),
|
|
"image URL should be a base64 data URI, got: {}",
|
|
&image_url.url[..image_url.url.len().min(40)]
|
|
);
|
|
}
|
|
other => panic!("expected ImageUrl content part, got: {:?}", other),
|
|
}
|
|
|
|
// The text should note the image is sent as visual content
|
|
assert!(
|
|
last_user_msg
|
|
.content
|
|
.contains("[Image attached — sent as visual content]"),
|
|
"augmented text should note image sent as visual content"
|
|
);
|
|
|
|
rig.verify_trace_expects(&trace, &responses);
|
|
rig.shutdown();
|
|
}
|
|
|
|
/// Message without attachments should have no content_parts and no augmentation.
|
|
#[tokio::test]
|
|
async fn no_attachments_no_augmentation() {
|
|
let trace = LlmTrace::from_file(format!("{FIXTURES}/smoke_greeting.json")).unwrap();
|
|
let rig = TestRigBuilder::new()
|
|
.with_trace(trace.clone())
|
|
.build()
|
|
.await;
|
|
|
|
rig.send_message("Hello! Introduce yourself briefly.").await;
|
|
let responses = rig.wait_for_responses(1, TIMEOUT).await;
|
|
|
|
let requests = rig.captured_llm_requests();
|
|
let last_request = &requests[requests.len() - 1];
|
|
let last_user_msg = last_request
|
|
.iter()
|
|
.rev()
|
|
.find(|m| matches!(m.role, optimclaw::llm::Role::User))
|
|
.expect("should have a user message");
|
|
|
|
// No attachments → no augmentation tags, no content parts
|
|
assert!(
|
|
!last_user_msg.content.contains("<attachments>"),
|
|
"plain message should NOT contain <attachments>"
|
|
);
|
|
assert!(
|
|
last_user_msg.content_parts.is_empty(),
|
|
"plain message should have no content parts"
|
|
);
|
|
|
|
rig.verify_trace_expects(&trace, &responses);
|
|
rig.shutdown();
|
|
}
|
|
}
|