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:
@@ -670,6 +670,15 @@ class TestParseDocument:
|
||||
assert r.markdown == "# SYNTH-MD"
|
||||
assert client._selector.outcomes == [("m1", False), ("m1", True)]
|
||||
|
||||
async def test_source_dead_switches_to_the_second_source_immediately(self):
|
||||
s1, s2 = _src(name="m1"), _src(name="m2", base_url="http://10.77.0.20:7867")
|
||||
client, _, _ = _client(
|
||||
[s1, s2], [SourceDeadError("401", status_code=401), "document"], sleep=_no_sleep
|
||||
)
|
||||
r = await client.parse_document(b"%PDF-1.4")
|
||||
assert r.source_name == "m2"
|
||||
assert client._selector.outcomes == [("m1", False), ("m2", True)]
|
||||
|
||||
async def test_result_invalid_counts_no_attempt_and_no_feed(self):
|
||||
client, _, gate = _client([_src()], [ResultInvalidError("bad package")])
|
||||
with pytest.raises(ResultInvalidError):
|
||||
@@ -742,6 +751,10 @@ class TestParseDocument:
|
||||
assert (await limiter.source_stats("m1")).inflight == 0
|
||||
|
||||
|
||||
async def _no_sleep(_delay):
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
class TestNoLatencyJudgement:
|
||||
"""长文档不会被判慢(设计 §7): 库内没有任何基于延迟的熔断/选源判定。
|
||||
|
||||
@@ -893,6 +906,52 @@ class TestDocumentThroughTheRealTransport:
|
||||
assert after.allowed and not after.is_probe # 探针已按"坏结果≠坏服务"结算,没有悬挂
|
||||
assert inner._gates["m1"].fails == 0
|
||||
|
||||
@staticmethod
|
||||
def _slow_download_handler():
|
||||
async def slow_body():
|
||||
for _ in range(200):
|
||||
await asyncio.sleep(0.01)
|
||||
yield b"x" * 8
|
||||
|
||||
return _doc_handler(download=lambda: httpx.Response(200, content=slow_body()))
|
||||
|
||||
async def test_per_call_deadline_overrides_the_assembly_value(self):
|
||||
client, _, _ = _client(
|
||||
[_src(timeout_s=120.0)], [], transport=_real_transport(self._slow_download_handler())
|
||||
) # 装配值未设期限
|
||||
with pytest.raises(CallDeadlineExceeded):
|
||||
await client.parse_document(b"%PDF-1.4 synthetic", call_deadline_s=0.1)
|
||||
|
||||
async def test_deadline_while_holding_the_half_open_probe_returns_it_and_writes_a_terminal_row(
|
||||
self,
|
||||
):
|
||||
"""期限到期取消在飞请求时: 探针必须归还,且终态遥测行必须写出(遥测必录)。"""
|
||||
clock = FakeClock()
|
||||
inner = InMemoryGate(
|
||||
config=BreakerConfig(fail_threshold=3, cooldown_s=60.0, probe_ttl_s=120.0), now=clock
|
||||
)
|
||||
entry = await inner.try_enter("m1", "setup")
|
||||
await inner.record_failure(entry, "source_dead", True)
|
||||
clock.advance(61.0) # 半开: 本次调用持有探针
|
||||
gate = RecordingGate(inner)
|
||||
recorder = _MemoryRecorder()
|
||||
client, _, _ = _client(
|
||||
[_src(timeout_s=120.0)],
|
||||
[],
|
||||
now=clock,
|
||||
breaker=gate,
|
||||
telemetry=recorder,
|
||||
transport=_real_transport(self._slow_download_handler()),
|
||||
call_deadline_s=0.1,
|
||||
)
|
||||
with pytest.raises(CallDeadlineExceeded):
|
||||
await client.parse_document(b"%PDF-1.4 synthetic")
|
||||
assert gate.probe_releases >= 1
|
||||
again = await inner.try_enter("m1", "check")
|
||||
assert again.allowed and again.is_probe # 探针没有悬挂,下一位调用方可以再探
|
||||
terminals = [r for r in recorder.rows if r["event_kind"] == "terminal_failure"]
|
||||
assert len(terminals) == 1 and terminals[0]["operation"] == "parse_document"
|
||||
|
||||
async def test_sustained_slow_download_hits_the_call_deadline_not_a_transient_error(self):
|
||||
"""每个分块都在 read 超时内、总时长超过 call_deadline_s: 是期限,不是瞬时故障。"""
|
||||
closed = []
|
||||
@@ -958,10 +1017,14 @@ class TestAssembly:
|
||||
|
||||
|
||||
class TestReasonlessTelemetryContract:
|
||||
"""text/layout 两个入口分别验证错误行不受源级推理配置污染。"""
|
||||
"""text/layout/document 三个入口分别验证错误行不受源级推理配置污染。
|
||||
|
||||
独立验证发现文档入口原先不在矩阵里: 让 OCR 一律按 `reasoning_applies=True` 记账,
|
||||
15 个旧用例转红,但没有一个是 `parse_document` 的。"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"method,action", [("recognize_text", "text"), ("parse_layout", "layout")]
|
||||
"method,action",
|
||||
[("recognize_text", "text"), ("parse_layout", "layout"), ("parse_document", "document")],
|
||||
)
|
||||
@pytest.mark.parametrize("config", [{"enable_thinking": True}, {"reasoning_effort": "high"}])
|
||||
@pytest.mark.parametrize("backend", ["memory", "sqlite"])
|
||||
@@ -994,7 +1057,7 @@ class TestReasonlessTelemetryContract:
|
||||
if backend == "sqlite":
|
||||
recorder.close()
|
||||
|
||||
@pytest.mark.parametrize("method", ["recognize_text", "parse_layout"])
|
||||
@pytest.mark.parametrize("method", ["recognize_text", "parse_layout", "parse_document"])
|
||||
@pytest.mark.parametrize("exhausted", [False, True])
|
||||
async def test_failed_attempts_still_have_null_effort(self, method, exhausted):
|
||||
script = (
|
||||
|
||||
Reference in New Issue
Block a user