blob: db2def3220835f63609009617b5733def17958a3 [file]
#!/usr/bin/env python3
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import importlib.util
from contextlib import redirect_stdout
import io
import json
from pathlib import Path
from types import SimpleNamespace
import sys
import unittest
from unittest import mock
import urllib.error
import urllib.parse
MODULE_PATH = Path(__file__).with_name("emit_litefuse_otel_io.py")
SPEC = importlib.util.spec_from_file_location("emit_litefuse_otel_io", MODULE_PATH)
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)
def attribute_values(span):
values = {}
for attribute in span["attributes"]:
value = attribute["value"]
if "arrayValue" in value:
values[attribute["key"]] = [
next(iter(item.values()))
for item in value["arrayValue"]["values"]
]
else:
values[attribute["key"]] = next(iter(value.values()))
return values
class FakeResponse:
status = 200
def __enter__(self):
return self
def __exit__(self, _exc_type, _exc, _traceback):
return False
def read(self):
return b"{}"
class PartialSuccessResponse(FakeResponse):
def read(self):
return b'{"partialSuccess":{"rejectedSpans":"1","errorMessage":"bad span"}}'
class JsonResponse(FakeResponse):
def __init__(self, payload):
self.payload = payload
def read(self):
return json.dumps(self.payload).encode()
class LitefuseOtelExporterTest(unittest.TestCase):
def verify_single_trace(self, args, public_key, secret_key, trace_id, count):
result = {"trace_id": trace_id, "observation_count": count}
MODULE.verify_traces(
args, public_key, secret_key, [{"result": result, "subagent": False}]
)
return result["verified"]
def verification_args(self, **overrides):
return SimpleNamespace(**{
"base_url": "https://litefuse.example",
"verify_attempts": 3,
"verify_sleep_seconds": 5,
"verify_timeout_seconds": 120,
"min_observations": 3,
"min_step_observations": 1,
**overrides,
})
def verification_fixture(self, *, subagent=False, count=3):
events = [self.span_event(f"{index:032x}") for index in range(1, count + 1)]
events[0]["body"]["name"] = "codex.subagent.review" if subagent else "codex.review"
payload = {"batch": [{"type": "trace-create", "body": self.trace_body()}, *events]}
result = {"trace_id": "subagent" if subagent else "main", "observation_count": count}
target = MODULE.trace_verification_target(result, payload, subagent=subagent)
observations = [
{**event["body"], "id": MODULE.otel_id(event["body"]["id"], 8)}
for event in events
]
return target, observations, payload
def trace_body(self):
return {
"id": "1" * 32,
"name": "doris-ai-review",
"sessionId": "run-123",
"environment": "github-actions",
"metadata": {"repository": "apache/doris", "codex_jsonl": True},
"tags": ["doris-ai-review", "codex-jsonl"],
}
def span_event(self, span_id, parent_id=None, output_size=0):
body = {
"id": span_id,
"traceId": "1" * 32,
"name": "codex.command",
"startTime": "2026-09-01T00:00:00.000Z",
"endTime": "2026-09-01T00:00:01.000Z",
"input": {"command": "git status"},
"output": {"status": "completed", "text": "x" * output_size},
"environment": "github-actions",
"metadata": {"item_type": "command_execution"},
"level": "DEFAULT",
}
if parent_id:
body["parentObservationId"] = parent_id
return {"type": "span-create", "body": body}
def reject_payloads_above(self, server_limit, request_sizes):
def fake_urlopen(request, timeout):
request_sizes.append(len(request.data))
if len(request.data) > server_limit:
raise urllib.error.HTTPError(
request.full_url,
413,
"Payload Too Large",
{},
io.BytesIO(b"payload too large"),
)
return FakeResponse()
return fake_urlopen
def test_converts_legacy_events_to_otlp_hierarchy_and_attributes(self):
root_id = "2" * 32
child_id = "3" * 32
root = self.span_event(root_id)
child = self.span_event(child_id, root_id)
payload = MODULE.otlp_payload(self.trace_body(), [root, child])
spans = payload["resourceSpans"][0]["scopeSpans"][0]["spans"]
self.assertEqual(len(spans), 2)
self.assertEqual(spans[0]["traceId"], "1" * 32)
self.assertEqual(len(spans[0]["spanId"]), 16)
self.assertNotIn("parentSpanId", spans[0])
self.assertEqual(spans[1]["parentSpanId"], spans[0]["spanId"])
attributes = attribute_values(spans[0])
self.assertEqual(attributes["langfuse.trace.name"], "doris-ai-review")
self.assertEqual(attributes["session.id"], "run-123")
self.assertEqual(attributes["langfuse.observation.type"], "span")
self.assertTrue(attributes["langfuse.internal.is_app_root"])
self.assertEqual(
attributes["langfuse.trace.tags"],
["doris-ai-review", "codex-jsonl"],
)
def test_bounds_and_shrinks_failed_turn_status_message(self):
args = SimpleNamespace(
repository="apache/doris",
workflow="Code Review",
run_id="run-123",
pr_number="67413",
head_sha="a" * 40,
base_sha="b" * 40,
reasoning_effort="xhigh",
max_json_chars=20_000,
max_context_json_chars=0,
trace_name="doris-ai-review",
session_id="run-123",
environment="github-actions",
model="gpt-5.6-sol",
)
events = [
{"type": "turn.failed", "error": {"message": "e" * 50_000}}
]
_trace_id, payload, _observation_count = MODULE.build_ingestion_payload(
args, "review task", "", events
)
turn_event = next(
event
for event in payload["batch"]
if (event.get("body") or {}).get("name") == "codex.turn"
)
source_status = turn_event["body"]["statusMessage"]
self.assertLess(len(source_status), 20_100)
self.assertIn("[truncated to first 20000 chars]", source_status)
trace_body = MODULE.trace_body_from_payload(payload)
self.assertGreater(
MODULE.json_payload_bytes(MODULE.otlp_payload(trace_body, [turn_event])),
20_000,
)
chunks = MODULE.otlp_chunks(payload, max_payload_bytes=20_000)
spans = [
span
for _legacy, otel, _size in chunks
for resource in otel["resourceSpans"]
for scope in resource["scopeSpans"]
for span in scope["spans"]
]
turn_span = next(span for span in spans if span["name"] == "codex.turn")
attributes = attribute_values(turn_span)
self.assertEqual(turn_span["status"], {"code": 2})
self.assertIn(
"[truncated to first", attributes["langfuse.observation.status_message"]
)
self.assertTrue(all(size <= 20_000 for _legacy, _otel, size in chunks))
def test_subagent_root_observation_preserves_task_input(self):
args = SimpleNamespace(
max_input_chars=200_000,
max_output_chars=200_000,
max_json_chars=40_000,
repository="apache/doris",
workflow="Code Review",
run_id="run-123",
pr_number="67413",
head_sha="a" * 40,
base_sha="b" * 40,
reasoning_effort="xhigh",
session_id="run-123",
subagent_trace_name="doris-ai-review-subagent",
environment="github-actions",
model="gpt-5.6-sol",
)
session_path = "/tmp/thread-123.jsonl"
events = [
{
"type": "session_meta",
"timestamp": "2026-09-01T00:00:00.000Z",
"payload": {"id": "thread-123"},
},
{
"type": "response_item",
"timestamp": "2026-09-01T00:00:01.000Z",
"payload": {
"type": "message",
"role": "user",
"content": [{"type": "input_text", "text": "review task"}],
},
},
{
"type": "response_item",
"timestamp": "2026-09-01T00:00:02.000Z",
"payload": {
"type": "message",
"role": "assistant",
"content": [{"type": "output_text", "text": "review result"}],
},
},
]
result = MODULE.build_subagent_session_payload(args, session_path, events)
chunks = MODULE.otlp_chunks(result["payload"], max_payload_bytes=20_000)
spans = [
span
for _legacy, otel, _size in chunks
for resource in otel["resourceSpans"]
for scope in resource["scopeSpans"]
for span in scope["spans"]
]
root = next(span for span in spans if span["name"] == "codex.subagent.review")
attributes = attribute_values(root)
self.assertEqual(
json.loads(attributes["langfuse.observation.input"]),
{"prompt": "review task"},
)
self.assertEqual(
attributes["langfuse.observation.metadata.session_file"], session_path
)
self.assertEqual(
attributes["langfuse.observation.metadata.thread_id"], "thread-123"
)
def test_chunks_encoded_otlp_payloads_to_requested_size(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
span_events = [
self.span_event(f"{index:032x}", output_size=2_000)
for index in range(1, 9)
]
chunks = MODULE.otlp_chunks(
{"batch": [trace_event, *span_events]}, max_payload_bytes=6_000
)
self.assertGreater(len(chunks), 1)
self.assertEqual(
sum(MODULE.otlp_span_count(otel) for _legacy, otel, _size in chunks),
len(span_events),
)
self.assertTrue(all(size <= 6_000 for _legacy, _otel, size in chunks))
def test_truncates_one_oversized_span(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
span_event = self.span_event("2" * 32, output_size=50_000)
chunks = MODULE.otlp_chunks(
{"batch": [trace_event, span_event]}, max_payload_bytes=6_000
)
self.assertEqual(len(chunks), 1)
_legacy, otel, size = chunks[0]
self.assertLessEqual(size, 6_000)
output = attribute_values(
otel["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
)["langfuse.observation.output"]
self.assertIn("truncated_json", output)
def test_does_not_truncate_span_below_full_otlp_limit(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
span_event = self.span_event("2" * 32, output_size=3_500)
original = MODULE.otlp_payload(self.trace_body(), [span_event])
original_size = MODULE.json_payload_bytes(original)
self.assertGreater(original_size, 3_000)
self.assertLessEqual(original_size, 6_000)
chunks = MODULE.otlp_chunks(
{"batch": [trace_event, span_event]}, max_payload_bytes=6_000
)
self.assertEqual(len(chunks), 1)
_legacy, otel, size = chunks[0]
self.assertEqual(size, original_size)
output = attribute_values(
otel["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
)["langfuse.observation.output"]
self.assertNotIn("truncated_json", output)
self.assertEqual(len(json.loads(output)["text"]), 3_500)
def test_preserves_near_limit_single_span_payload(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
span_event = self.span_event("2" * 32, output_size=4_509)
original = MODULE.otlp_payload(self.trace_body(), [span_event])
original_size = MODULE.json_payload_bytes(original)
self.assertGreater(original_size, 6_000)
self.assertLess(original_size, 6_100)
chunks = MODULE.otlp_chunks(
{"batch": [trace_event, span_event]}, max_payload_bytes=6_000
)
self.assertEqual(len(chunks), 1)
_legacy, otel, size = chunks[0]
self.assertGreater(size, 5_500)
self.assertLessEqual(size, 6_000)
output = attribute_values(
otel["resourceSpans"][0]["scopeSpans"][0]["spans"][0]
)["langfuse.observation.output"]
self.assertGreater(len(json.loads(output)["truncated_json"]), 4_000)
def test_prechunks_before_otlp_encoding(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
span_events = [
self.span_event(f"{index:032x}", output_size=1_000)
for index in range(1, 21)
]
encoded_batch_sizes = []
original_otlp_payload = MODULE.otlp_payload
def recording_otlp_payload(trace_body, events):
encoded_batch_sizes.append(len(events))
return original_otlp_payload(trace_body, events)
with mock.patch.object(
MODULE, "otlp_payload", side_effect=recording_otlp_payload
):
MODULE.otlp_chunks(
{"batch": [trace_event, *span_events]}, max_payload_bytes=6_000
)
self.assertLess(max(encoded_batch_sizes), len(span_events))
def test_posts_otlp_v4_headers(self):
payload = MODULE.otlp_payload(
self.trace_body(), [self.span_event("2" * 32)]
)
captured = {}
def fake_urlopen(request, timeout):
captured["request"] = request
captured["timeout"] = timeout
return FakeResponse()
with mock.patch.object(MODULE.urllib.request, "urlopen", fake_urlopen):
status = MODULE.post_payload_once(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
payload,
30,
)
headers = {key.lower(): value for key, value in captured["request"].header_items()}
self.assertEqual(headers["content-type"], "application/json")
self.assertEqual(headers["x-langfuse-ingestion-version"], "4")
self.assertEqual(headers["x-langfuse-sdk-name"], "doris-code-review")
self.assertEqual(captured["timeout"], 30)
self.assertEqual(status["success_count"], 1)
def test_paginates_v2_observations_until_root_is_visible(self):
first_page = [{"id": f"child-{index}"} for index in range(1_000)]
responses = [
JsonResponse({"data": first_page, "meta": {"cursor": "next"}}),
JsonResponse({"data": [{"id": "root"}], "meta": {}}),
]
requests = []
def fake_urlopen(request, timeout):
requests.append((request, timeout))
return responses.pop(0)
with mock.patch.object(MODULE.urllib.request, "urlopen", fake_urlopen):
payload = MODULE.fetch_observations_v2(
"https://litefuse.example", "public", "secret", "trace-id"
)
self.assertEqual(len(payload["data"]), 1_001)
self.assertEqual(payload["data"][-1], {"id": "root"})
first_query = urllib.parse.parse_qs(
urllib.parse.urlparse(requests[0][0].full_url).query
)
second_query = urllib.parse.parse_qs(
urllib.parse.urlparse(requests[1][0].full_url).query
)
self.assertEqual(first_query["limit"], ["1000"])
self.assertNotIn("cursor", first_query)
self.assertEqual(second_query["cursor"], ["next"])
def test_rejects_incomplete_v2_observation_pagination(self):
response = JsonResponse(
{"data": [{"id": "newest"}], "meta": {"cursor": "still-more"}}
)
with mock.patch.object(
MODULE.urllib.request, "urlopen", return_value=response
):
with self.assertRaisesRegex(RuntimeError, "remained paginated"):
MODULE.fetch_observations_v2(
"https://litefuse.example",
"public",
"secret",
"trace-id",
max_pages=1,
)
def test_rejects_otlp_partial_success(self):
payload = MODULE.otlp_payload(
self.trace_body(), [self.span_event("2" * 32)]
)
with mock.patch.object(
MODULE.urllib.request, "urlopen", return_value=PartialSuccessResponse()
):
with self.assertRaisesRegex(RuntimeError, "partially rejected 1 spans"):
MODULE.post_payload_once(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
payload,
30,
)
def test_splits_multi_span_chunk_after_transport_error(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
payload = {
"batch": [
trace_event,
self.span_event("2" * 32),
self.span_event("3" * 32, "2" * 32),
]
}
with mock.patch.object(
MODULE.urllib.request,
"urlopen",
side_effect=[
urllib.error.URLError("connection reset"),
FakeResponse(),
FakeResponse(),
],
):
status = MODULE.post_payload(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
payload,
10_000,
30,
3,
0,
)
self.assertEqual(status["transport_retries"], 1)
self.assertEqual(status["request_count"], 2)
self.assertEqual(status["success_count"], 2)
def test_splits_multi_span_chunk_after_http_413(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
payload = {
"batch": [
trace_event,
self.span_event("2" * 32),
self.span_event("3" * 32, "2" * 32),
]
}
payload_too_large = urllib.error.HTTPError(
"https://litefuse.example/api/public/otel/v1/traces",
413,
"Payload Too Large",
{},
io.BytesIO(b"payload too large"),
)
with mock.patch.object(
MODULE.urllib.request,
"urlopen",
side_effect=[payload_too_large, FakeResponse(), FakeResponse()],
):
status = MODULE.post_payload(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
payload,
10_000,
30,
3,
0,
)
self.assertEqual(status["payload_too_large_retries"], 1)
self.assertEqual(status["request_count"], 2)
self.assertEqual(status["success_count"], 2)
def test_retries_single_span_413_without_half_size_ceiling(self):
trace_body = {
**self.trace_body(),
"metadata": {"repository": "apache/doris", "fixed": "m" * 3_000},
}
trace_event = {"type": "trace-create", "body": trace_body}
span_event = self.span_event("2" * 32, output_size=1_500)
server_limit = (
MODULE.json_payload_bytes(MODULE.otlp_payload(trace_body, [span_event])) - 1
)
request_sizes = []
with mock.patch.object(
MODULE.urllib.request,
"urlopen",
side_effect=self.reject_payloads_above(server_limit, request_sizes),
):
status = MODULE.post_payload(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
{"batch": [trace_event, span_event]},
10_000,
30,
3,
0,
)
self.assertEqual(status["payload_too_large_retries"], 1)
self.assertEqual(status["request_count"], 1)
self.assertEqual(status["success_count"], 1)
self.assertEqual(len(request_sizes), 2)
self.assertLessEqual(request_sizes[1], server_limit)
self.assertLess(request_sizes[1], request_sizes[0])
self.assertGreater(request_sizes[1], request_sizes[0] // 2)
def test_adapts_single_span_413_for_lower_server_limit(self):
trace_body = {
**self.trace_body(),
"metadata": {"repository": "apache/doris", "fixed": "m" * 3_000},
}
trace_event = {"type": "trace-create", "body": trace_body}
span_event = self.span_event("2" * 32, output_size=5_000)
initial_size = MODULE.json_payload_bytes(
MODULE.otlp_payload(trace_body, [span_event])
)
server_limit = initial_size - 1_000
request_sizes = []
with mock.patch.object(
MODULE.urllib.request,
"urlopen",
side_effect=self.reject_payloads_above(server_limit, request_sizes),
):
status = MODULE.post_payload(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
{"batch": [trace_event, span_event]},
10_000,
30,
5,
0,
)
self.assertEqual(initial_size, 9_486)
self.assertEqual(len(request_sizes), 3)
self.assertTrue(
all(
current > following
for current, following in zip(request_sizes, request_sizes[1:])
)
)
self.assertLessEqual(request_sizes[-1], server_limit)
self.assertEqual(status["payload_too_large_retries"], 2)
self.assertEqual(status["post_attempt_count"], len(request_sizes))
self.assertEqual(status["success_count"], 1)
def test_stops_single_span_413_after_retry_budget(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
span_event = self.span_event("2" * 32, output_size=5_000)
request_sizes = []
with mock.patch.object(
MODULE.urllib.request,
"urlopen",
side_effect=self.reject_payloads_above(-1, request_sizes),
):
with self.assertRaisesRegex(RuntimeError, "after 3 retries"):
MODULE.post_payload(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
{"batch": [trace_event, span_event]},
10_000,
30,
3,
0,
)
self.assertEqual(len(request_sizes), 4)
self.assertTrue(
all(
current > following
for current, following in zip(request_sizes, request_sizes[1:])
)
)
def test_prechunking_encodes_each_legacy_event_once(self):
span_events = [
self.span_event(f"{index:032x}", output_size=200)
for index in range(1, 101)
]
original_compact_json_bytes = MODULE.compact_json_bytes
encode_count = 0
def recording_compact_json_bytes(value):
nonlocal encode_count
encode_count += 1
return original_compact_json_bytes(value)
with mock.patch.object(
MODULE,
"compact_json_bytes",
side_effect=recording_compact_json_bytes,
):
chunks = MODULE.chunk_payload(
{"batch": span_events}, max_payload_bytes=5_000
)
self.assertEqual(encode_count, len(span_events))
self.assertEqual(
[event for chunk, _size in chunks for event in chunk["batch"]],
span_events,
)
self.assertTrue(
all(
size == MODULE.json_payload_bytes(chunk)
for chunk, size in chunks
)
)
def test_retries_retryable_otlp_http_status(self):
trace_event = {"type": "trace-create", "body": self.trace_body()}
payload = {"batch": [trace_event, self.span_event("2" * 32)]}
unavailable = urllib.error.HTTPError(
"https://litefuse.example/api/public/otel/v1/traces",
503,
"Service Unavailable",
{},
io.BytesIO(b'{"message":"temporarily unavailable"}'),
)
with mock.patch.object(
MODULE.urllib.request,
"urlopen",
side_effect=[unavailable, FakeResponse()],
):
status = MODULE.post_payload(
"https://litefuse.example/api/public/otel/v1/traces",
"public",
"secret",
payload,
10_000,
30,
3,
0,
)
self.assertEqual(status["http_retries"], 1)
self.assertEqual(status["request_count"], 1)
self.assertEqual(status["success_count"], 1)
def test_verifies_pending_subagent_without_polling_complete_main_again(self):
main, main_rows, _ = self.verification_fixture()
child, child_rows, _ = self.verification_fixture(subagent=True)
replies = [main_rows, child_rows[:1], child_rows]
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy",
side_effect=[{"data": rows} for rows in replies],
) as fetch, mock.patch.object(MODULE.time, "sleep") as sleep:
MODULE.verify_traces(self.verification_args(), "public", "secret", [main, child])
self.assertEqual([call.args[3] for call in fetch.call_args_list], ["main", "subagent", "subagent"])
sleep.assert_called_once_with(5)
for target in (main, child):
self.assertEqual(target["result"]["verified"]["observation_count"], 3)
self.assertNotIn("verification_diagnostic", target["result"])
def test_reports_all_missing_subagents_and_preserves_main_success(self):
main, main_rows, _ = self.verification_fixture()
children = [self.verification_fixture(subagent=True)[0] for _ in range(2)]
for index, child in enumerate(children):
child["result"]["trace_id"] = f"child-{index}"
replies = [main_rows, [], [], [], []]
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy",
side_effect=[{"data": rows} for rows in replies],
), mock.patch.object(MODULE.time, "sleep") as sleep:
with self.assertRaisesRegex(RuntimeError, "child-0.*child-1"):
MODULE.verify_traces(
self.verification_args(verify_attempts=2), "public", "secret",
[main, *children],
)
self.assertIn("verified", main["result"])
for child in children:
self.assertNotIn("verified", child["result"])
self.assertEqual(
child["result"]["verification_diagnostic"]["missing_expected_observation_count"], 3
)
sleep.assert_called_once_with(5)
def test_subagent_profile_accepts_root_only_and_session_message_shapes(self):
for count in (1, 2):
with self.subTest(count=count):
target, rows, _ = self.verification_fixture(subagent=True, count=count)
if count == 2:
rows[1]["name"] = "codex.subagent.message.assistant"
rows[1]["input"] = {"role": "assistant"}
rows[1]["output"] = {"text": "done"}
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": rows}
):
MODULE.verify_traces(self.verification_args(), "public", "secret", [target])
self.assertEqual(target["result"]["verified"]["required_observation_count"], count)
def test_subagent_rejects_missing_root_id_or_io_even_when_count_is_sufficient(self):
for defect in ("root", "wrong_id", "duplicate", "empty_id", "input", "output"):
with self.subTest(defect=defect):
target, rows, _ = self.verification_fixture(subagent=True)
if defect == "root":
rows[0]["name"] = "codex.command"
elif defect == "wrong_id":
rows[1]["id"] = "unrelated-observation"
elif defect == "duplicate":
rows[1]["id"] = rows[2]["id"]
elif defect == "empty_id":
rows[1]["id"] = ""
else:
rows[1][defect] = None
with mock.patch.object(
MODULE, "fetch_trace", return_value={"input": "p", "output": "o"}
), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": rows}
):
with self.assertRaisesRegex(RuntimeError, "subagent"):
MODULE.verify_traces(
self.verification_args(verify_attempts=1), "public", "secret", [target]
)
def test_subagent_accepts_exported_empty_io_and_unexported_none(self):
for value in ({}, [], "", False, 0, None):
with self.subTest(value=value):
_, rows, payload = self.verification_fixture(subagent=True, count=2)
payload["batch"][-1]["body"]["output"] = value
result = {"trace_id": "subagent", "observation_count": 2}
target = MODULE.trace_verification_target(result, payload, subagent=True)
rows[1]["output"] = value
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": rows}
):
MODULE.verify_traces(self.verification_args(), "public", "secret", [target])
self.assertIn("verified", result)
def test_v2_fallback_uses_exported_session_time_window(self):
target, rows, payload = self.verification_fixture(subagent=True, count=2)
payload["batch"][-1]["body"]["startTime"] = "2026-09-01T02:00:00+00:00"
target = MODULE.trace_verification_target(target["result"], payload, subagent=True)
requests = []
def fake_urlopen(request, timeout):
requests.append(request)
if "/v2/observations?" not in request.full_url:
raise urllib.error.URLError("legacy API unavailable")
return JsonResponse({"data": rows, "meta": {}})
with mock.patch.object(MODULE.urllib.request, "urlopen", side_effect=fake_urlopen):
MODULE.verify_traces(self.verification_args(), "public", "secret", [target])
query = urllib.parse.parse_qs(urllib.parse.urlparse(requests[-1].full_url).query)
self.assertEqual(query["fromStartTime"], ["2026-08-31T23:59:59+00:00"])
self.assertEqual(query["toStartTime"], ["2026-09-01T02:00:01+00:00"])
self.assertEqual(target["result"]["verified"]["read_source"], "v2_observations")
def test_subagent_trace_detail_fallback_still_requires_all_exported_ids(self):
for complete in (False, True):
with self.subTest(complete=complete):
target, rows, _ = self.verification_fixture(subagent=True)
with mock.patch.object(
MODULE, "fetch_trace",
return_value={"observations": rows if complete else rows[:1]},
), mock.patch.object(
MODULE, "fetch_observations_legacy", side_effect=RuntimeError("unavailable")
), mock.patch.object(
MODULE, "fetch_observations_v2", side_effect=RuntimeError("unavailable")
):
if complete:
MODULE.verify_traces(self.verification_args(), "public", "secret", [target])
self.assertIn("verified", target["result"])
else:
with self.assertRaisesRegex(RuntimeError, "subagent"):
MODULE.verify_traces(
self.verification_args(verify_attempts=1), "public", "secret", [target]
)
def test_verifies_subagent_with_more_than_ten_legacy_pages(self):
target, rows, _ = self.verification_fixture(subagent=True, count=1001)
rows = list(reversed(rows)) # The oldest root is on page 11.
requests = []
def fake_urlopen(request, timeout):
query = urllib.parse.parse_qs(urllib.parse.urlparse(request.full_url).query)
page = int(query["page"][0])
requests.append(page)
return JsonResponse({"data": rows[(page - 1) * 100:page * 100]})
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE.urllib.request, "urlopen", side_effect=fake_urlopen
):
MODULE.verify_traces(self.verification_args(), "public", "secret", [target])
self.assertEqual(requests, list(range(1, 12)))
self.assertEqual(target["result"]["verified"]["unique_observation_count"], 1001)
def test_shared_deadline_bounds_all_traces_and_http_fallbacks(self):
targets = [self.verification_fixture()[0]]
targets.extend(self.verification_fixture(subagent=True)[0] for _ in range(100))
for index, target in enumerate(targets):
target["result"]["trace_id"] = f"trace-{index}"
now = [0]
timeouts = []
def fake_urlopen(request, timeout):
timeouts.append(timeout)
now[0] += timeout
raise TimeoutError("slow read")
with mock.patch.object(MODULE.time, "monotonic", side_effect=lambda: now[0]), mock.patch.object(
MODULE.urllib.request, "urlopen", side_effect=fake_urlopen
), mock.patch.object(MODULE.time, "sleep") as sleep:
with self.assertRaisesRegex(RuntimeError, "trace-100"):
MODULE.verify_traces(
self.verification_args(verify_timeout_seconds=35), "public", "secret", targets
)
self.assertEqual(timeouts, [30, 5])
self.assertEqual(now[0], 35)
sleep.assert_not_called()
self.assertEqual(targets[-1]["result"]["verification_diagnostic"]["read_source"], "not_polled")
def test_shared_deadline_clips_sleep_and_stops_before_next_round(self):
targets = [self.verification_fixture()[0], self.verification_fixture(subagent=True)[0]]
now = [0]
def fake_sleep(seconds):
now[0] += seconds
with mock.patch.object(MODULE.time, "monotonic", side_effect=lambda: now[0]), mock.patch.object(
MODULE, "fetch_trace", return_value={}
), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": []}
) as fetch, mock.patch.object(MODULE.time, "sleep", side_effect=fake_sleep) as sleep:
with self.assertRaises(RuntimeError):
MODULE.verify_traces(
self.verification_args(verify_timeout_seconds=2), "public", "secret", targets
)
self.assertEqual(fetch.call_count, 2)
sleep.assert_called_once_with(2)
def test_pagination_checks_remaining_deadline_before_every_request(self):
for reader in (MODULE.fetch_observations_legacy, MODULE.fetch_observations_v2):
with self.subTest(reader=reader.__name__):
now = [0]
timeouts = []
def fake_urlopen(request, timeout):
timeouts.append(timeout)
now[0] += 3
return JsonResponse({"data": [{}] * 100, "meta": {"cursor": "next"}})
with mock.patch.object(
MODULE.time, "monotonic", side_effect=lambda: now[0]
), mock.patch.object(MODULE.urllib.request, "urlopen", side_effect=fake_urlopen):
with self.assertRaisesRegex(TimeoutError, "deadline exhausted"):
reader("https://litefuse.example", "public", "secret", "trace", deadline=5)
self.assertEqual(timeouts, [5, 2])
def test_main_exports_and_verifies_all_traces_with_failure_diagnostics(self):
for mode in ("success", "missing_subagent", "main_only", "no_verify", "dry_run"):
with self.subTest(mode=mode):
argv = [
"exporter", "--input-file", "prompt", "--events-file", "events",
"--session-id", "run-test", "--verify-attempts", "1",
"--verify-sleep-seconds", "0",
]
if mode != "main_only":
argv.extend(["--subagent-sessions-dir", "/unused"])
if mode != "no_verify":
argv.append("--verify")
if mode == "dry_run":
argv.append("--dry-run")
with mock.patch.object(sys, "argv", argv):
args = MODULE.parse_args()
events = [
{
"type": "item.completed", "_line_number": index + 1,
"item": {"type": "agent_message", "text": text, "id": str(index)},
}
for index, text in enumerate(("reviewing", "done"))
]
session_events = [
{"type": "session_meta", "payload": {"id": "child-thread"}},
{
"type": "response_item",
"payload": {"type": "message", "role": "assistant", "content": "done"},
},
]
child = MODULE.build_subagent_session_payload(args, "child.jsonl", session_events)
stored_rows = {}
requests = []
def fake_urlopen(request, timeout):
requests.append(request)
if request.method == "POST":
payload = json.loads(request.data)
for resource in payload["resourceSpans"]:
for scope in resource["scopeSpans"]:
for span in scope["spans"]:
attrs = attribute_values(span)
row = {"id": span["spanId"], "name": span["name"]}
for field in ("input", "output"):
value = attrs.get(f"langfuse.observation.{field}")
if value is not None:
row[field] = json.loads(value)
stored_rows.setdefault(span["traceId"], []).append(row)
return FakeResponse()
if "/api/public/traces/" in request.full_url:
return JsonResponse({})
query = urllib.parse.parse_qs(urllib.parse.urlparse(request.full_url).query)
trace_id = query["traceId"][0]
rows = stored_rows[trace_id]
if mode == "missing_subagent" and trace_id == child["trace_id"]:
rows = rows[:-1]
return JsonResponse({"data": rows})
stdout = io.StringIO()
with mock.patch.object(MODULE, "parse_args", return_value=args), mock.patch.object(
MODULE, "read_text", return_value="review task"
), mock.patch.object(MODULE, "load_jsonl", return_value=events), mock.patch.object(
MODULE, "build_subagent_session_payloads", return_value=[child]
) as build_children, mock.patch.object(
MODULE.urllib.request, "urlopen", side_effect=fake_urlopen
), mock.patch.dict(
MODULE.os.environ,
{} if mode == "dry_run" else {"LANGFUSE_PUBLIC_KEY": "public", "LANGFUSE_SECRET_KEY": "secret"},
clear=True,
), redirect_stdout(stdout):
if mode == "missing_subagent":
with self.assertRaisesRegex(RuntimeError, child["trace_id"]):
MODULE.main()
else:
MODULE.main()
result = json.loads(stdout.getvalue())
if mode == "dry_run":
self.assertEqual(requests, [])
self.assertEqual(result["subagent_trace_count"], 1)
elif mode == "no_verify":
self.assertTrue(requests)
self.assertTrue(all(request.method == "POST" for request in requests))
self.assertNotIn("verified", result)
self.assertNotIn("verified", result["subagent_traces"][0])
else:
self.assertEqual(result["verified"]["agent_message_with_turn_input_count"], 1)
self.assertEqual(result["verified"]["agent_message_with_previous_count"], 1)
if mode == "main_only":
build_children.assert_not_called()
self.assertEqual(result["subagent_traces"], [])
elif mode == "missing_subagent":
self.assertIn("verification_error", result)
diagnostic = result["subagent_traces"][0]["verification_diagnostic"]
self.assertEqual(diagnostic["missing_expected_observation_count"], 1)
self.assertNotIn("verified", result["subagent_traces"][0])
else:
self.assertEqual(result["subagent_traces"][0]["verified"]["observation_count"], 3)
def test_main_still_rejects_missing_agent_message_context(self):
target, rows, _ = self.verification_fixture()
rows[-1]["name"] = "codex.agent_message"
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": rows}
):
with self.assertRaisesRegex(RuntimeError, '"agent_message_all_have_context_window": false'):
MODULE.verify_traces(
self.verification_args(verify_attempts=1), "public", "secret", [target]
)
def test_verify_complete_main_trace_uses_legacy_observations(self):
args = mock.Mock(
base_url="https://litefuse.example",
verify_attempts=1,
verify_timeout_seconds=120,
verify_sleep_seconds=0,
min_observations=3,
min_step_observations=1,
)
observations = [
{
"id": "review",
"name": "codex.review",
"input": {"prompt": "p"},
"output": {"text": "o"},
},
{
"id": "turn",
"name": "codex.turn",
"input": {"prompt": "p"},
"output": {"text": "o"},
},
{
"id": "command",
"name": "codex.command",
"input": {"command": "pwd"},
"output": {"status": "ok"},
},
]
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": observations}
), mock.patch.object(MODULE, "fetch_observations_v2") as v2_fetch:
result = self.verify_single_trace(
args, "public", "secret", "trace-id", len(observations)
)
self.assertEqual(result["read_source"], "legacy_observations")
self.assertEqual(result["required_observation_count"], len(observations))
v2_fetch.assert_not_called()
def test_verify_rejects_partially_visible_trace(self):
args = mock.Mock(
base_url="https://litefuse.example",
verify_attempts=1,
verify_timeout_seconds=120,
verify_sleep_seconds=0,
min_observations=1,
min_step_observations=1,
)
observations = [
{
"id": "review",
"name": "codex.review",
"input": {"prompt": "p"},
"output": {"text": "o"},
},
{
"id": "command",
"name": "codex.command",
"input": {"command": "pwd"},
"output": {"status": "ok"},
},
]
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": observations}
), mock.patch.object(MODULE, "fetch_observations_v2"):
with self.assertRaisesRegex(
RuntimeError, '"required_observation_count": 3'
):
self.verify_single_trace(
args, "public", "secret", "trace-id", 3
)
def test_verify_rejects_duplicate_observation_ids(self):
args = mock.Mock(
base_url="https://litefuse.example",
verify_attempts=1,
verify_timeout_seconds=120,
verify_sleep_seconds=0,
min_observations=1,
min_step_observations=1,
)
observations = [
{
"id": "review",
"name": "codex.review",
"input": {"prompt": "p"},
"output": {"text": "o"},
},
{
"id": "command",
"name": "codex.command",
"input": {"command": "pwd"},
"output": {"status": "ok"},
},
{
"id": "command",
"name": "codex.command",
"input": {"command": "pwd"},
"output": {"status": "ok"},
},
]
with mock.patch.object(MODULE, "fetch_trace", return_value={}), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": observations}
), mock.patch.object(MODULE, "fetch_observations_v2"):
with self.assertRaisesRegex(
RuntimeError, '"duplicate_observation_count": 1'
):
self.verify_single_trace(
args, "public", "secret", "trace-id", 2
)
def test_verify_rejects_missing_or_empty_observation_ids(self):
args = SimpleNamespace(
base_url="https://litefuse.example",
verify_attempts=1,
verify_timeout_seconds=120,
verify_sleep_seconds=0,
min_observations=1,
min_step_observations=1,
)
complete_observations = [
{
"id": "review",
"name": "codex.review",
"input": {"prompt": "p"},
"output": {"text": "o"},
},
{
"id": "command",
"name": "codex.command",
"input": {"command": "pwd"},
"output": {"status": "ok"},
},
]
for id_fields in ({}, {"id": None}, {"id": ""}):
with self.subTest(id_fields=id_fields):
observations = [
*complete_observations,
{
**id_fields,
"name": "codex.command",
"input": {"command": "git status"},
"output": {"status": "ok"},
},
]
with mock.patch.object(
MODULE, "fetch_trace", return_value={}
), mock.patch.object(
MODULE, "fetch_observations_legacy", return_value={"data": observations}
), mock.patch.object(MODULE, "fetch_observations_v2"):
# The two valid IDs already satisfy the count requirement;
# the additional ID-less row must independently fail verification.
with self.assertRaisesRegex(
RuntimeError, '"observations_missing_id_count": 1'
):
self.verify_single_trace(args, "public", "secret", "trace-id", 2)
if __name__ == "__main__":
unittest.main()