1313
1414from utils .jsonl_parser import parse_session
1515
16- FUZZ_SETTINGS = settings (
17- max_examples = 200 ,
18- deadline = 5000 ,
19- suppress_health_check = [HealthCheck .function_scoped_fixture ],
20- )
16+ # Only suppress the tmp_path health check; max_examples and deadline come from
17+ # the active Hypothesis profile (ci/dev) registered in conftest.py.
18+ FUZZ_SETTINGS = settings (suppress_health_check = [HealthCheck .function_scoped_fixture ])
2119
20+ # Structured errors that are acceptable instead of a clean parse. Empty for now —
21+ # the invariant is that parse_session never raises an unhandled exception.
2222ALLOWED_EXCEPTIONS : tuple [type [BaseException ], ...] = ()
2323
2424
25- def _fuzz_jsonl_path (tmp_path : Path , name : str ) -> Path :
26- return tmp_path / name
27-
28-
2925def _parse_file_without_crash (path : str ) -> None :
3026 try :
3127 parse_session (path )
28+ except ALLOWED_EXCEPTIONS :
29+ return
3230 except Exception as exc :
33- if ALLOWED_EXCEPTIONS and isinstance (exc , ALLOWED_EXCEPTIONS ):
34- return
3531 raise AssertionError (f"unhandled { type (exc ).__name__ } : { exc } " ) from exc
3632
3733
@@ -151,17 +147,17 @@ def structured_entry(draw: st.DrawFn) -> dict:
151147@given (st .lists (st .text (min_size = 0 , max_size = 500 ), min_size = 0 , max_size = 30 ))
152148def test_raw_line_soup_does_not_crash (tmp_path : Path , lines : list [str ]) -> None :
153149 """Malformed JSON lines, garbage text, and empty lines."""
154- path = _write_jsonl (_fuzz_jsonl_path ( tmp_path , "soup.jsonl" ) , lines )
150+ path = _write_jsonl (tmp_path / "soup.jsonl" , lines )
155151 _parse_file_without_crash (path )
156152
157153
158154@FUZZ_SETTINGS
159155@given (st .text (min_size = 1 , max_size = 500 ))
160156def test_truncated_json_line (tmp_path : Path , prefix : str ) -> None :
161- """Partial JSON simulating concurrent writes."""
162- half = json .dumps (prefix )[: max ( 1 , len ( prefix ) // 2 )]
163- line = '{"type": "user", "message": {"content": ' + half
164- path = _write_jsonl (_fuzz_jsonl_path ( tmp_path , "trunc.jsonl" ) , [line ])
157+ """Partial JSON simulating concurrent writes (object cut mid-serialization) ."""
158+ full_line = json .dumps ({ "type" : "user" , "message" : { "content" : prefix }})
159+ truncated = full_line [: max ( 1 , len ( full_line ) // 2 )]
160+ path = _write_jsonl (tmp_path / "trunc.jsonl" , [truncated ])
165161 _parse_file_without_crash (path )
166162
167163
@@ -170,7 +166,7 @@ def test_truncated_json_line(tmp_path: Path, prefix: str) -> None:
170166def test_structured_entries_with_fuzzed_fields (tmp_path : Path , entries : list [dict ]) -> None :
171167 """Unknown types, missing/extra fields, wrong-typed nested values."""
172168 lines = [json .dumps (e , default = str ) for e in entries ]
173- path = _write_jsonl (_fuzz_jsonl_path ( tmp_path , "structured.jsonl" ) , lines )
169+ path = _write_jsonl (tmp_path / "structured.jsonl" , lines )
174170 _parse_file_without_crash (path )
175171
176172
@@ -182,7 +178,7 @@ def test_deep_nesting_in_message_content(tmp_path: Path, nested_values: list) ->
182178 "timestamp" : "2026-06-11T00:00:00Z" ,
183179 "message" : {"content" : nested_values },
184180 }
185- path = _write_jsonl (_fuzz_jsonl_path ( tmp_path , "nest.jsonl" ) , [json .dumps (entry , default = str )])
181+ path = _write_jsonl (tmp_path / "nest.jsonl" , [json .dumps (entry , default = str )])
186182 _parse_file_without_crash (path )
187183
188184
@@ -195,7 +191,7 @@ def test_long_line_payload(tmp_path: Path, length: int) -> None:
195191 "timestamp" : "2026-06-11T00:00:00Z" ,
196192 "message" : {"content" : [{"type" : "text" , "text" : payload }]},
197193 }
198- path = _write_jsonl (_fuzz_jsonl_path ( tmp_path , "long.jsonl" ) , [json .dumps (entry )])
194+ path = _write_jsonl (tmp_path / "long.jsonl" , [json .dumps (entry )])
199195 _parse_file_without_crash (path )
200196
201197
@@ -215,7 +211,7 @@ def test_empty_lines_between_records(tmp_path: Path, texts: list[str]) -> None:
215211 )
216212 )
217213 lines .append (" " )
218- path = _write_jsonl (_fuzz_jsonl_path ( tmp_path , "empty.jsonl" ) , lines )
214+ path = _write_jsonl (tmp_path / "empty.jsonl" , lines )
219215 _parse_file_without_crash (path )
220216
221217
@@ -243,4 +239,27 @@ def test_unknown_record_type_is_graceful(tmp_path: Path) -> None:
243239 path = _write_jsonl (tmp_path / "unknown.jsonl" , lines )
244240 session = parse_session (path )
245241 assert session ["metadata" ]["entry_counts" ].get ("totally-new-claude-record" ) == 1
246- assert len (session ["messages" ]) >= 1
242+ # Unknown type produces no message; only the valid user line does.
243+ assert len (session ["messages" ]) == 1
244+
245+
246+ def test_non_numeric_usage_tokens_do_not_crash (tmp_path : Path ) -> None :
247+ """Non-numeric usage fields must coerce to 0, not raise TypeError on +=."""
248+ entry = {
249+ "type" : "assistant" ,
250+ "timestamp" : "2026-06-11T00:00:00Z" ,
251+ "message" : {
252+ "model" : "claude-test" ,
253+ "content" : [{"type" : "text" , "text" : "hi" }],
254+ "usage" : {
255+ "input_tokens" : "five" ,
256+ "output_tokens" : ["not" , "a" , "number" ],
257+ "cache_creation" : {"ephemeral_5m_input_tokens" : "lots" },
258+ },
259+ },
260+ }
261+ path = _write_jsonl (tmp_path / "bad_usage.jsonl" , [json .dumps (entry )])
262+ session = parse_session (path )
263+ assert session ["metadata" ]["total_input_tokens" ] == 0
264+ assert session ["metadata" ]["total_output_tokens" ] == 0
265+ assert session ["metadata" ]["total_ephemeral_5m_tokens" ] == 0
0 commit comments