Files
openworker/tests/test_conversation_jsonl_robustness.py
T
Saidheerajgollu 8c199ffdda Tolerate a corrupt line when loading a conversation .jsonl
`_read_jsonl` parsed every line with a bare `json.loads` inside a list
comprehension, so a single malformed line raised `JSONDecodeError` and
took the whole `load()` down. An append interrupted mid-write (process
crash, full disk) leaves exactly that: one truncated trailing line — and
from then on every surface that opens the session errors on load, with
no way back short of hand-editing the file. The session is effectively
bricked, including its recoverable history.

Skip unparseable lines and keep the good messages. This matches how the
rest of this module already treats JSON (the inline-blob and roots/grants
loaders all swallow `JSONDecodeError` and fall back) — `_read_jsonl` was
the one strict outlier on the hot session-load path.
2026-07-23 14:13:07 -07:00

63 lines
2.1 KiB
Python

"""ConversationStore: a corrupt line in a .jsonl must not brick session load.
An append interrupted mid-write (crash, full disk) leaves one malformed trailing line.
load() must skip it and return the recoverable history, not raise on every open.
"""
from __future__ import annotations
from coworker.conversations import ConversationStore
from coworker.sessions import SessionRecord
def _seed(store: ConversationStore, sid: str, n: int) -> None:
store.save(
SessionRecord(
session_id=sid,
workspace="/tmp",
model="m",
mode="interactive",
messages=[{"role": "user", "content": f"m{i}"} for i in range(n)],
)
)
def test_load_skips_a_corrupt_trailing_line(tmp_path):
store = ConversationStore(tmp_path / "state")
sid = "abc123def456"
_seed(store, sid, 2)
# Simulate a torn write: append a truncated JSON line to the session's log.
jsonl = tmp_path / "state" / "conversations" / f"{sid}.jsonl"
with open(jsonl, "a", encoding="utf-8") as f:
f.write('{"role": "user", "content": "unterm\n') # no closing brace/quote
loaded = store.load(sid) # must not raise
assert loaded is not None
# The two good messages survive; the corrupt line is dropped.
assert [m["content"] for m in loaded.messages] == ["m0", "m1"]
def test_load_skips_a_corrupt_middle_line(tmp_path):
store = ConversationStore(tmp_path / "state")
sid = "def456abc123"
jsonl = tmp_path / "state" / "conversations" / f"{sid}.jsonl"
jsonl.parent.mkdir(parents=True, exist_ok=True)
jsonl.write_text(
'{"role": "user", "content": "first"}\n'
"not json at all\n"
'{"role": "assistant", "content": "third"}\n',
encoding="utf-8",
)
# Register the session in the index so load() reaches the .jsonl.
store._conn.execute(
"INSERT INTO sessions (session_id, workspace, model, mode, title, n_msgs) "
"VALUES (?, '/tmp', 'm', 'interactive', 't', 2)",
(sid,),
)
store._conn.commit()
loaded = store.load(sid)
assert loaded is not None
assert [m["content"] for m in loaded.messages] == ["first", "third"]