deer-flow/backend/tests/test_jsonl_event_store_unicode.py
Undermoon1412 f770ecc0b8
fix(events): preserve Unicode separators in JSONL event records (#5429)
Signed-off-by: Undermoon1412 <Undermoon1412@users.noreply.github.com>
Co-authored-by: Undermoon1412 <Undermoon1412@users.noreply.github.com>
2026-09-14 18:24:51 +08:00

112 lines
4.8 KiB
Python

"""JSONL framing must not split Unicode separators inside event values (#5420)."""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from deerflow.runtime.events.store.jsonl import JsonlRunEventStore
SEPARATORS = [chr(0x85), chr(0x2028), chr(0x2029)]
SEPARATOR_IDS = ["next-line", "line-separator", "paragraph-separator"]
@pytest.mark.anyio
@pytest.mark.parametrize("separator", SEPARATORS, ids=SEPARATOR_IDS)
@pytest.mark.parametrize("write_method", ["put", "put_batch", "put_if_absent"])
async def test_unicode_event_values_round_trip(tmp_path: Path, separator: str, write_method: str):
store = JsonlRunEventStore(base_dir=tmp_path)
content = {"type": "ai", "id": "message-1", "content": f"before{separator}after"}
event = {
"thread_id": "thread-1",
"run_id": "run-1",
"event_type": "llm.ai.response",
"category": "message",
"content": content,
"metadata": {"note": f"first{separator}second"},
}
if write_method == "put_batch":
saved = (await store.put_batch([event]))[0]
elif write_method == "put_if_absent":
saved, created = await store.put_if_absent(**event)
assert created
else:
saved = await store.put(**event)
# The writer already emits valid JSON: this must be repaired on the read side.
raw = (tmp_path / "threads/thread-1/runs/run-1.jsonl").read_bytes()
assert separator.encode("utf-8") in raw
assert json.loads(raw) == saved
assert await store.list_messages("thread-1") == [saved]
assert await store.list_events("thread-1", "run-1") == [saved]
assert await store.list_messages_by_run("thread-1", "run-1") == [saved]
assert await store.count_messages("thread-1") == 1
assert await store.get_message_seqs("thread-1", ["message:message-1"]) == {"message:message-1": saved["seq"]}
assert await store.find_latest_ai_message_run_ids("thread-1", {"message-1"}) == {"message-1": "run-1"}
@pytest.mark.anyio
@pytest.mark.parametrize("separator", SEPARATORS, ids=SEPARATOR_IDS)
async def test_reopen_recovers_seq_from_existing_unicode_record(tmp_path: Path, separator: str):
# Seed a pre-existing file directly: changing only the writer cannot fix it.
path = tmp_path / "threads/thread-1/runs/run-1.jsonl"
path.parent.mkdir(parents=True)
record = {
"thread_id": "thread-1",
"run_id": "run-1",
"event_type": "llm.ai.response",
"category": "message",
"content": f"old{separator}message",
"metadata": {},
"seq": 41,
"created_at": "2026-01-01T00:00:00+00:00",
}
path.write_text(json.dumps(record, ensure_ascii=False) + "\n", encoding="utf-8")
reopened = JsonlRunEventStore(base_dir=tmp_path)
following = await reopened.put(thread_id="thread-1", run_id="run-2", event_type="llm.ai.response", category="message", content="next message")
assert following["seq"] == 42
assert await reopened.list_messages("thread-1") == [record, following]
assert await reopened.list_messages("thread-1", after_seq=41) == [following]
@pytest.mark.anyio
@pytest.mark.parametrize("separator", SEPARATORS, ids=SEPARATOR_IDS)
async def test_unicode_metadata_does_not_duplicate_idempotent_event(tmp_path: Path, separator: str):
store = JsonlRunEventStore(base_dir=tmp_path)
event = {
"thread_id": "thread-1",
"run_id": "run-1",
"event_type": "run.end",
"category": "lifecycle",
"content": "completed",
"metadata": {"note": f"first{separator}second"},
}
saved, created = await store.put_if_absent(**event)
assert created
reopened = JsonlRunEventStore(base_dir=tmp_path)
existing, created_again = await reopened.put_if_absent(**event)
assert not created_again
assert existing == saved
assert await reopened.list_events("thread-1", "run-1") == [saved]
@pytest.mark.anyio
@pytest.mark.parametrize("newline", ["\n", "\r\n"], ids=["lf", "crlf"])
async def test_physical_lines_keep_controls_and_skip_malformed_records(tmp_path: Path, newline: str):
store = JsonlRunEventStore(base_dir=tmp_path)
saved = await store.put(thread_id="thread-1", run_id="run-1", event_type="llm.ai.response", category="message", content="ordinary\n你好\r\ntext")
path = tmp_path / "threads/thread-1/runs/run-1.jsonl"
# Blank lines, malformed JSON, and a final record without a trailing newline.
path.write_bytes(newline.join(["", "not-json", " ", json.dumps(saved, ensure_ascii=False)]).encode("utf-8"))
reopened = JsonlRunEventStore(base_dir=tmp_path)
assert await reopened.list_messages("thread-1") == [saved]
assert await reopened.list_events("thread-1", "run-1") == [saved]
following = await reopened.put(thread_id="thread-1", run_id="run-2", event_type="llm.ai.response", category="message", content="next message")
assert following["seq"] == saved["seq"] + 1