Skip to content

Commit 1204d39

Browse files
fix: address PR #89 review feedback on role coercion and fallback messages
1 parent d899603 commit 1204d39

2 files changed

Lines changed: 46 additions & 6 deletions

File tree

tests/test_jsonl_parser.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,16 +988,34 @@ def test_unknown_entry_type_maps_to_system_with_warning(self, caplog):
988988
]
989989
)
990990
try:
991-
with caplog.at_level("WARNING"):
991+
with caplog.at_level("WARNING", logger="utils.jsonl_parser"):
992992
s = parse_session(path)
993993
assert len(s["messages"]) == 1
994994
assert s["messages"][0]["role"] == "system"
995+
assert s["messages"][0]["text"] == "forward-compat payload"
995996
assert s["messages"][0]["content"] == "forward-compat payload"
996997
assert "Unknown message role" in caplog.text
997998
assert "mystery_future_type" in caplog.text
998999
finally:
9991000
os.unlink(path)
10001001

1002+
def test_fallback_message_extracts_text_from_structured_content(self):
1003+
path = _write_jsonl(
1004+
[
1005+
{
1006+
"type": "result",
1007+
"timestamp": "2026-01-01T00:00:00Z",
1008+
"content": [{"type": "text", "text": "block body"}],
1009+
}
1010+
]
1011+
)
1012+
try:
1013+
s = parse_session(path)
1014+
assert s["messages"][0]["text"] == "block body"
1015+
assert s["messages"][0]["content"] == "block body"
1016+
finally:
1017+
os.unlink(path)
1018+
10011019
def test_valid_unhandled_result_type_emits_result_role(self):
10021020
path = _write_jsonl(
10031021
[
@@ -1014,3 +1032,19 @@ def test_valid_unhandled_result_type_emits_result_role(self):
10141032
assert s["messages"][0]["role"] == "result"
10151033
finally:
10161034
os.unlink(path)
1035+
1036+
1037+
class TestCoerceRole:
1038+
def test_known_roles_returned_unchanged(self):
1039+
from utils.jsonl_parser import _coerce_role
1040+
1041+
for role in ("user", "assistant", "system", "result", "progress"):
1042+
assert _coerce_role(role) == role
1043+
1044+
def test_unknown_role_maps_to_system_with_warning(self, caplog):
1045+
from utils.jsonl_parser import _coerce_role
1046+
1047+
with caplog.at_level("WARNING", logger="utils.jsonl_parser"):
1048+
result = _coerce_role("totally_unknown")
1049+
assert result == "system"
1050+
assert "totally_unknown" in caplog.text

utils/jsonl_parser.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,33 @@
2424

2525
__all__ = ["parse_session", "quick_session_info"]
2626

27-
_HANDLED_ENTRY_TYPES = frozenset({"user", "assistant", "system", "progress"})
2827
_SKIP_ENTRY_TYPES = frozenset({"file-history-snapshot"})
2928
_VALID_ROLES = frozenset(get_args(RoleLiteral))
29+
_log = logging.getLogger(__name__)
3030

3131

3232
def _coerce_role(raw: str) -> RoleLiteral:
3333
if raw in _VALID_ROLES:
3434
return cast(RoleLiteral, raw)
35-
logging.warning("Unknown message role %r; mapping to 'system'", raw)
35+
_log.warning("Unknown message role %r; mapping to 'system'", raw)
3636
return "system"
3737

3838

3939
def _fallback_message(entry: dict[str, Any], role: RoleLiteral) -> MessageDict:
4040
"""Minimal message for JSONL entry types without a dedicated processor."""
41-
content = entry.get("content", "")
42-
text = content if isinstance(content, str) else (str(content) if content is not None else "")
41+
raw_content = entry.get("content", "")
42+
if isinstance(raw_content, str):
43+
text = raw_content
44+
elif raw_content is not None:
45+
text = _extract_text(_normalize_content(raw_content))
46+
else:
47+
text = ""
4348
return {
4449
"role": role,
4550
"uuid": entry.get("uuid"),
4651
"parent_uuid": entry.get("parentUuid"),
4752
"timestamp": entry.get("timestamp"),
53+
"text": text,
4854
"content": text,
4955
"is_sidechain": entry.get("isSidechain", False),
5056
}
@@ -155,7 +161,7 @@ def parse_session(filepath: str) -> SessionDict:
155161
_process_progress(entry, messages)
156162
elif entry_type:
157163
type_str = entry_type if isinstance(entry_type, str) else str(entry_type)
158-
if type_str not in _HANDLED_ENTRY_TYPES and type_str not in _SKIP_ENTRY_TYPES:
164+
if type_str not in _SKIP_ENTRY_TYPES:
159165
messages.append(_fallback_message(entry, _coerce_role(type_str)))
160166

161167
metadata["models_used"] = sorted(metadata["models_used"])

0 commit comments

Comments
 (0)