Files
optimclaw/tests/e2e_attachments.rs
OutBack Dingo 6d9dbbb3b9 fork: rename IronClaw → OptimClaw
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
2026-03-29 06:27:52 +07:00

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();
}
}