Skip to content

Commit 0289d3e

Browse files
committed
fix: test_traverse_blobs_never_raises skips the meta-parse error path
1 parent 6290213 commit 0289d3e

4 files changed

Lines changed: 58 additions & 19 deletions

File tree

tests/test_blob_parsing_fuzz.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
from models import Bubble, SchemaError
2525
from utils.cli_chat_reader import (
2626
classify_blob_data,
27+
extract_blob_refs,
2728
messages_to_bubbles,
2829
traverse_blobs,
29-
_extract_blob_refs, # internal helper; covered directly alongside classify_blob_data
3030
)
3131
from utils.text_extract import extract_text_from_bubble
3232

@@ -132,17 +132,39 @@ def _make_meta_value(meta: dict) -> str:
132132

133133

134134
def _build_store_db_raw(path: str, meta: dict, blobs: dict[str, bytes]) -> None:
135-
"""Minimal store.db with arbitrary blob payloads (for traverse_blobs fuzz)."""
135+
"""Minimal store.db with well-formed meta dict and arbitrary blob payloads."""
136+
_build_store_db_meta_row(path, _make_meta_value(meta), blobs)
137+
138+
139+
def _build_store_db_meta_row(
140+
path: str, meta_row: str | None, blobs: dict[str, bytes]
141+
) -> None:
142+
"""Minimal store.db; *meta_row* is the raw ``meta.value`` (hex JSON or adversarial)."""
136143
conn = sqlite3.connect(path)
137144
conn.execute("CREATE TABLE meta (key TEXT PRIMARY KEY, value TEXT)")
138145
conn.execute("CREATE TABLE blobs (id TEXT PRIMARY KEY, data BLOB)")
139-
conn.execute("INSERT INTO meta VALUES ('0', ?)", (_make_meta_value(meta),))
146+
if meta_row is not None:
147+
conn.execute("INSERT INTO meta VALUES ('0', ?)", (meta_row,))
140148
for blob_id, data in blobs.items():
141149
conn.execute("INSERT INTO blobs VALUES (?, ?)", (blob_id, data))
142150
conn.commit()
143151
conn.close()
144152

145153

154+
_FUZZ_META_ROW = st.one_of(
155+
st.none(),
156+
st.just(""),
157+
st.text(min_size=0, max_size=200),
158+
st.dictionaries(st.text(max_size=20), _JSON_VALUES, max_size=6).map(
159+
lambda d: json.dumps(d).encode("utf-8").hex()
160+
),
161+
st.builds(
162+
lambda root: _make_meta_value({"latestRootBlobId": root, "createdAt": 1}),
163+
_BLOB_ID_HEX,
164+
),
165+
)
166+
167+
146168
def _assemble_workspace_bubble(bubble_id: object, value: object) -> dict | None:
147169
"""Mirror workspace_tabs KV bubble load (json.loads → Bubble.from_dict).
148170
@@ -219,7 +241,7 @@ class TestBlobChainParsingFuzz(unittest.TestCase):
219241
@settings(max_examples=120, deadline=None)
220242
def test_extract_blob_refs_never_raises(self, data: bytes) -> None:
221243
try:
222-
refs = _extract_blob_refs(data)
244+
refs = extract_blob_refs(data)
223245
except Exception as exc:
224246
self.fail(f"unexpected {type(exc).__name__}: {exc}")
225247
self.assertIsInstance(refs, list)
@@ -230,7 +252,7 @@ def test_extract_blob_refs_never_raises(self, data: bytes) -> None:
230252
@given(data=st.binary(max_size=4096))
231253
@settings(max_examples=80, deadline=None)
232254
def test_extract_blob_refs_is_idempotent(self, data: bytes) -> None:
233-
self.assertEqual(_extract_blob_refs(data), _extract_blob_refs(data))
255+
self.assertEqual(extract_blob_refs(data), extract_blob_refs(data))
234256

235257
@given(data=st.binary(max_size=4096))
236258
@settings(max_examples=80, deadline=None)
@@ -273,6 +295,23 @@ def test_traverse_blobs_never_raises(
273295
self.fail(f"traverse_blobs raised {type(exc).__name__}: {exc}")
274296
self.assertIsInstance(messages, list)
275297

298+
@given(meta_row=_FUZZ_META_ROW)
299+
@settings(
300+
max_examples=30,
301+
deadline=None,
302+
suppress_health_check=[HealthCheck.too_slow],
303+
)
304+
def test_traverse_blobs_meta_parse_never_raises(self, meta_row: str | None) -> None:
305+
"""Covers meta decode / CliSessionMeta.from_dict failure → return [] (no crash)."""
306+
with tempfile.TemporaryDirectory() as td:
307+
db_path = os.path.join(td, "store.db")
308+
_build_store_db_meta_row(db_path, meta_row, {})
309+
try:
310+
messages = traverse_blobs(db_path)
311+
except Exception as exc:
312+
self.fail(f"traverse_blobs raised {type(exc).__name__}: {exc}")
313+
self.assertIsInstance(messages, list)
314+
276315

277316
class TestTextExtractionFuzz(unittest.TestCase):
278317
@given(bubble=_BUBBLE_LIKE)

tests/test_cli_chat_reader.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from utils.cli_chat_reader import (
2222
_content_to_text,
23-
_extract_blob_refs,
23+
extract_blob_refs,
2424
_extract_tool_calls,
2525
_strip_user_info,
2626
aggregate_session_stats,
@@ -75,34 +75,34 @@ def _build_store_db(path: str, meta: dict, json_blobs: dict[str, dict], chain: d
7575

7676

7777
# ---------------------------------------------------------------------------
78-
# _extract_blob_refs
78+
# extract_blob_refs
7979
# ---------------------------------------------------------------------------
8080

8181
class TestExtractBlobRefs(unittest.TestCase):
8282
def test_empty_bytes_returns_empty(self):
83-
self.assertEqual(_extract_blob_refs(b""), [])
83+
self.assertEqual(extract_blob_refs(b""), [])
8484

8585
def test_single_ref(self):
8686
ref = "a" * 64 # 32 bytes as hex
8787
raw = b"\x0a\x20" + bytes.fromhex(ref)
88-
self.assertEqual(_extract_blob_refs(raw), [ref])
88+
self.assertEqual(extract_blob_refs(raw), [ref])
8989

9090
def test_two_refs(self):
9191
ref1 = "a" * 64
9292
ref2 = "b" * 64
9393
raw = b"\x0a\x20" + bytes.fromhex(ref1) + b"\x0a\x20" + bytes.fromhex(ref2)
94-
self.assertEqual(_extract_blob_refs(raw), [ref1, ref2])
94+
self.assertEqual(extract_blob_refs(raw), [ref1, ref2])
9595

9696
def test_noise_bytes_ignored(self):
9797
ref = "c" * 64
9898
noise = b"\x00\xff\x01\x02\x03\x04"
9999
raw = noise + b"\x0a\x20" + bytes.fromhex(ref) + b"\xde\xad"
100-
self.assertIn(ref, _extract_blob_refs(raw))
100+
self.assertIn(ref, extract_blob_refs(raw))
101101

102102
def test_partial_tag_at_end_ignored(self):
103103
# Only 0x0a without 0x20 immediately following should not produce a ref.
104104
raw = b"\x0a" + b"\x00" * 32
105-
self.assertEqual(_extract_blob_refs(raw), [])
105+
self.assertEqual(extract_blob_refs(raw), [])
106106

107107

108108
# ---------------------------------------------------------------------------

tests/test_models.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
Workspace,
1818
WorkspaceLocalComposer,
1919
)
20-
from utils.cli_chat_reader import _extract_blob_refs
20+
from utils.cli_chat_reader import extract_blob_refs
2121

2222

2323
GOOD_COMPOSER_RAW: dict = {
@@ -252,7 +252,7 @@ def test_meta_parses_then_blob_chain_extracts_refs(self) -> None:
252252
self.assertEqual(meta.latest_root_blob_id, ref1)
253253

254254
chain_blob = _make_blob_chain(ref1, ref2, ref3)
255-
refs = _extract_blob_refs(chain_blob)
255+
refs = extract_blob_refs(chain_blob)
256256
self.assertEqual(refs, [ref1, ref2, ref3])
257257

258258
def test_blob_chain_skips_non_marker_bytes(self) -> None:
@@ -261,10 +261,10 @@ def test_blob_chain_skips_non_marker_bytes(self) -> None:
261261
garbage_after = b"\xff\xfe"
262262
raw = garbage_before + bytes([0x0A, 0x20]) + bytes.fromhex(ref) + garbage_after
263263

264-
self.assertEqual(_extract_blob_refs(raw), [ref])
264+
self.assertEqual(extract_blob_refs(raw), [ref])
265265

266266
def test_blob_chain_empty_returns_empty_list(self) -> None:
267-
self.assertEqual(_extract_blob_refs(b""), [])
267+
self.assertEqual(extract_blob_refs(b""), [])
268268

269269

270270
if __name__ == "__main__":

utils/cli_chat_reader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _read_meta(db_path: str) -> dict:
6262
return {}
6363

6464

65-
def _extract_blob_refs(data: bytes) -> list[str]:
65+
def extract_blob_refs(data: bytes) -> list[str]:
6666
"""Extract all 32-byte (SHA-256) blob references from a binary chain node.
6767
6868
The encoding is: tag ``0x0a`` (field 1, length-delimited) followed by
@@ -84,7 +84,7 @@ def classify_blob_data(data: bytes) -> tuple[dict | None, list[str]]:
8484
8585
Returns ``(message_dict, [])`` when *data* decodes to a dict with a
8686
``role`` field; otherwise ``(None, refs)`` where *refs* are SHA-256 hex
87-
ids from :func:`_extract_blob_refs`. Used by :func:`traverse_blobs` and
87+
ids from :func:`extract_blob_refs`. Used by :func:`traverse_blobs` and
8888
property tests — keep in sync when the load loop changes.
8989
"""
9090
try:
@@ -93,7 +93,7 @@ def classify_blob_data(data: bytes) -> tuple[dict | None, list[str]]:
9393
return msg, []
9494
except (UnicodeDecodeError, json.JSONDecodeError):
9595
pass
96-
return None, _extract_blob_refs(data)
96+
return None, extract_blob_refs(data)
9797

9898

9999
def traverse_blobs(db_path: str) -> list[dict]:

0 commit comments

Comments
 (0)