fix: close independent verification findings on document parsing

Zip construction and member reads now share one narrow boundary so corrupt BZIP2, LZMA and version fields cannot leak native exceptions. Non-success statuses are read before classification, text-bearing blocks require lines, the size cap and reasoning contract are pinned by tests that fail under mutation, and the per-page timing claim is corrected to the observed range.
This commit is contained in:
2026-10-22 01:12:27 -04:00
parent d80feafb93
commit bb621cad4d
9 changed files with 314 additions and 68 deletions
+70 -14
View File
@@ -704,27 +704,83 @@ class TestParseDocument:
document=_PDF_BODY, source=_source(), call_id="c1"
)
async def test_oversized_declared_content_length_is_rejected(self, monkeypatch):
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 100)
resp = httpx.Response(200, content=b"x" * 10, headers={"content-length": "101"})
with pytest.raises(ResultInvalidError):
@staticmethod
def _oversized_valid_package():
"""**合法**的 ZIP 但整体超过上限: 去掉上限检查时它能被正常解析,用例才会转红。
(独立验证发现: 旧用例用非 ZIP 垃圾字节,去掉上限后同样抛 ResultInvalidError——
走的是 BadZipFile 路径,上限从未被测到。)
"""
return _doc_package(
[_doc_page([_text_block("SYNTH-A")])], extra=[("images/big.jpg", b"x" * 5000)]
)
async def _assert_cap_rejects_before_parsing(self, monkeypatch, resp):
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 2000)
parsed = []
def spy(package):
parsed.append(len(package))
return [], [(1.0, 1.0)], ""
monkeypatch.setattr(monkey_ocr, "parse_document_package", spy)
with pytest.raises(ResultInvalidError, match="体积超过上限"):
await self._transport(zip_resp=resp).parse_document(
document=_PDF_BODY, source=_source(), call_id="c1"
)
assert parsed == [] # 解析函数根本没被调用
async def test_oversized_streamed_body_is_rejected_by_cumulative_count(self, monkeypatch):
"""没有 Content-Length(分块传输)时靠累计字节兜底: 多个小块合起来超限。"""
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 100)
async def test_oversized_valid_package_is_rejected_by_declared_content_length(
self, monkeypatch
):
resp = httpx.Response(200, content=self._oversized_valid_package()) # 自动带 Content-Length
await self._assert_cap_rejects_before_parsing(monkeypatch, resp)
async def test_oversized_valid_package_is_rejected_by_cumulative_count(self, monkeypatch):
"""没有 Content-Length(分块传输)时靠累计字节兜底。"""
package = self._oversized_valid_package()
async def chunks():
for _ in range(3):
yield b"x" * 60
for offset in range(0, len(package), 700):
yield package[offset : offset + 700]
resp = httpx.Response(200, content=chunks())
with pytest.raises(ResultInvalidError):
await self._transport(zip_resp=resp).parse_document(
document=_PDF_BODY, source=_source(), call_id="c1"
)
await self._assert_cap_rejects_before_parsing(
monkeypatch, httpx.Response(200, content=chunks())
)
async def test_package_under_the_cap_still_parses(self, monkeypatch):
"""对照: 上限只拦超限的,不误伤正常包。"""
monkeypatch.setattr(monkey_ocr, "_MAX_PACKAGE_BYTES", 10_000)
result = await self._transport(
zip_resp=httpx.Response(200, content=self._oversized_valid_package())
).parse_document(document=_PDF_BODY, source=_source(), call_id="c1")
assert [e.text for e in result.elements] == ["SYNTH-A"]
@pytest.mark.parametrize(
("status", "exc_type"),
[
(401, SourceDeadError),
(404, RequestRejectedError),
(500, TransientError),
(302, RequestRejectedError), # 3xx 不是 `is_error`,但 raise_for_status 会抛
],
)
async def test_streamed_error_body_is_read_before_classification(self, status, exc_type):
"""真正的流式响应(异步迭代器 body)不会被预缓冲: 不先 `aread()`,摘要读到的是空串。
(独立验证发现: `httpx.Response(content=bytes)` 已预缓冲,删掉 `aread()` 后旧用例仍绿;
且旧条件 `is_error` 漏掉 3xx。)
"""
body = '{"detail": "synthetic upstream reason"}'
async def streamed():
yield body.encode()
t = self._transport(zip_resp=httpx.Response(status, content=streamed()))
with pytest.raises(exc_type) as ei:
await t.parse_document(document=_PDF_BODY, source=_source(), call_id="c1")
assert ei.value.status_code == status
assert ei.value.body_text == body
async def test_bad_package_is_result_invalid(self):
t = self._transport(zip_resp=httpx.Response(200, content=b"not a zip"))