use super::*;
use crate::test_utils::TraceCapture;
use serde_json::json;
#[test]
fn embedding_seam_and_vector_search_record_on_the_span() {
use crate::embeddings::Embedding;
use crate::vector_store::VectorStoreIndex as _;
use crate::vector_store::in_memory_store::InMemoryVectorStore;
use crate::vector_store::request::VectorSearchRequest;
const BODY: &str = r#"{
"object": "list",
"model": "text-embedding-3-small",
"usage": { "prompt_tokens": 4, "total_tokens": 4 },
"data": [{ "object": "embedding", "index": 0, "embedding": [0.1, 0.2] }]
}"#;
let capture = TraceCapture::default();
let _isolation = crate::test_utils::scoped_tracing_subscriber_guard_blocking();
tracing::subscriber::with_default(capture.subscriber(), || {
let model = crate::driver::Model::new(
crate::providers::openai::wire::OpenAIConfig::with_key(
&crate::providers::openai::wire::OPENAI,
"test-key",
)
.embedding("text-embedding-3-small", Some(2)),
crate::test_utils::RecordingHttpClient::new(BODY),
);
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
runtime.block_on(async {
let response = model
.call(vec!["hello".to_owned()])
.await
.expect("embedding succeeds");
assert_eq!(response.usage.input_tokens, Some(4));
let store = InMemoryVectorStore::from_documents([(
"doc".to_owned(),
vec![Embedding {
document: "doc".to_owned(),
vec: vec![0.1, 0.2],
}],
)]);
let index = store.index(model);
let request = VectorSearchRequest::builder()
.query("hello")
.samples(1)
.build();
let hits: Vec<crate::vector_store::VectorSearchResult<String>> =
index.top_n(request).await.expect("search succeeds");
assert_eq!(hits.len(), 1);
});
});
let last = |field: &str| capture.values_of(field).last().cloned();
assert_eq!(last("gen_ai.operation.name"), Some(json!("embeddings")));
assert_eq!(last("gen_ai.provider.name"), Some(json!("openai")));
assert_eq!(last("gen_ai.usage.input_tokens"), Some(json!(4)));
assert_eq!(
last("gen_ai.response.model"),
Some(json!("text-embedding-3-small"))
);
let usage_records = capture.values_of("gen_ai.usage.input_tokens").len();
assert_eq!(
usage_records, 2,
"the vector-search query embeds through the instrumented seam"
);
}
#[test]
fn completion_parent_span_macro_honours_its_parent_argument() {
fn captured_parent(capture: &TraceCapture) -> Option<&'static str> {
let Some(span) = capture.last_span() else {
panic!("completion-parent span was not captured");
};
assert_eq!(span.target, "third_party_runtime");
span.parent_name
}
let capture = TraceCapture::default();
let _isolation = crate::test_utils::scoped_tracing_subscriber_guard_blocking();
tracing::subscriber::with_default(capture.subscriber(), || {
let ambient = tracing::info_span!(target: "application", "ambient");
ambient.in_scope(|| {
let _default_arm = completion_parent_span!(
target: "third_party_runtime",
name: "chat",
operation: Empty,
system_instructions: Option::<&str>::None,
);
});
assert_eq!(captured_parent(&capture), Some("ambient"));
ambient.in_scope(|| {
let _explicit_arm = completion_parent_span!(
target: "third_party_runtime",
parent: None,
name: "chat",
operation: Empty,
system_instructions: Option::<&str>::None,
);
});
assert_eq!(captured_parent(&capture), None);
});
}
#[test]
fn completion_parent_required_fields_are_pinned() {
assert_eq!(COMPLETION_PARENT_MARKER_FIELD, "rig.completion_parent");
assert_eq!(
COMPLETION_PARENT_REQUIRED_FIELDS,
&[
"gen_ai.operation.name",
"gen_ai.provider.name",
"gen_ai.request.model",
"gen_ai.system_instructions",
"gen_ai.response.id",
"gen_ai.response.model",
"gen_ai.usage.input_tokens",
"gen_ai.usage.output_tokens",
"gen_ai.usage.cache_read.input_tokens",
"gen_ai.usage.cache_creation.input_tokens",
"gen_ai.usage.tool_use_prompt_tokens",
"gen_ai.usage.reasoning_tokens",
"gen_ai.input.messages",
"gen_ai.output.messages",
]
);
}
#[test]
fn near_miss_completion_parent_warns_once_per_callsite() {
let capture = TraceCapture::default();
let _isolation = crate::test_utils::scoped_tracing_subscriber_guard_blocking();
reset_near_miss_warnings();
tracing::subscriber::with_default(capture.subscriber(), || {
tracing::callsite::rebuild_interest_cache();
let near_miss = tracing::info_span!(
target: "third_party_runtime",
"chat",
rig.completion_parent = true,
gen_ai.operation.name = tracing::field::Empty,
);
let _guard = near_miss.enter();
SpanBuilder::new("openai", "gpt-5", GenAiOperation::Chat).build();
SpanBuilder::new("openai", "gpt-5", GenAiOperation::Chat).build();
});
let captured = capture.warnings();
assert_eq!(
captured.len(),
1,
"a near-miss callsite warns exactly once, got: {captured:?}"
);
let Some(message) = captured.first() else {
panic!("near miss did not warn");
};
assert!(
message.contains("gen_ai.provider.name"),
"warning must name the missing fields, got: {message}"
);
assert!(
message.contains("completion_parent_span!"),
"warning must point at the supported fix, got: {message}"
);
}
#[test]
#[allow(deprecated)]
fn adopted_completion_parents_report_the_standard_operation_and_stream_flag() {
let capture = TraceCapture::default();
let _isolation = crate::test_utils::scoped_tracing_subscriber_guard_blocking();
tracing::subscriber::with_default(capture.subscriber(), || {
for (operation, streaming) in [
(GenAiOperation::Chat, Some(false)),
(GenAiOperation::Chat, Some(true)),
(GenAiOperation::ChatStreaming, None),
] {
let parent = completion_parent_span!(
target: "third_party_runtime",
name: "chat",
operation: Empty,
system_instructions: Option::<&str>::None,
);
let _entered = parent.enter();
let builder = SpanBuilder::new("prov", "model", operation);
match streaming {
Some(streaming) => builder.streaming(streaming),
None => builder,
}
.build();
}
});
let reported = capture
.spans()
.iter()
.map(|span| {
(
span.target,
span.value("gen_ai.operation.name").cloned(),
span.value("gen_ai.request.stream").cloned(),
)
})
.collect::<Vec<_>>();
assert_eq!(
reported,
[
(
"third_party_runtime",
Some(json!("chat")),
Some(json!(false))
),
(
"third_party_runtime",
Some(json!("chat")),
Some(json!(true))
),
(
"third_party_runtime",
Some(json!("chat")),
Some(json!(true))
),
]
);
}