fix: address M3 verifier findings on soak invariants and probe tests
This commit is contained in:
@@ -55,7 +55,7 @@ class TestLiveEndpoints:
|
||||
async def test_recognize_text_returns_content(self):
|
||||
async with OcrClient.from_env("OCR", env=_env({1: _PRIMARY})) as client:
|
||||
result = await client.recognize_text(_TABLE_IMAGE.read_bytes())
|
||||
assert result.text.strip() # 真实护理记录图必有文字
|
||||
assert result.text.strip() # 取证样本已核: 该图含可识别文字
|
||||
assert result.source_name == "monkey_1"
|
||||
assert result.usage.prompt_tokens == 0 and result.latency_ms > 0
|
||||
|
||||
|
||||
@@ -280,6 +280,14 @@ class TestMiddleJsonDefense:
|
||||
elements, _ = _parse_middle_json(_zip_bytes(_middle_bytes([page])))
|
||||
assert elements[0].page_index == 0
|
||||
|
||||
@pytest.mark.parametrize("bad_idx", [True, -3, "2", 1.5])
|
||||
def test_page_idx_non_int_falls_back(self, bad_idx):
|
||||
# bool/负数/非 int 一律回退枚举序(与 _finite_number 拒 bool 的防御口径一致)
|
||||
page = _page([_block("text", (1, 2, 30, 40))])
|
||||
page["page_idx"] = bad_idx
|
||||
elements, _ = _parse_middle_json(_zip_bytes(_middle_bytes([page])))
|
||||
assert elements[0].page_index == 0
|
||||
|
||||
|
||||
class TestErrorTranslation:
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -292,7 +292,49 @@ class TestBackpressure:
|
||||
await permit.release()
|
||||
|
||||
|
||||
class FakeClock:
|
||||
def __init__(self, start=1000.0):
|
||||
self.t = start
|
||||
|
||||
def __call__(self):
|
||||
return self.t
|
||||
|
||||
def advance(self, seconds):
|
||||
self.t += seconds
|
||||
|
||||
|
||||
class TestCancellation:
|
||||
async def test_cancel_during_probe_returns_probe(self):
|
||||
# 探针取消归还(铁律分支;verifier I3): 开路→冷却过后半开探针→
|
||||
# transport 挂起中取消 → release_probe 必须发生,后续可再探
|
||||
clock = FakeClock()
|
||||
inner = InMemoryGate(config=_BREAKER, now=clock) # 门与 client 共用注入钟
|
||||
gate = RecordingGate(inner)
|
||||
client, _, _ = _client([_src()], ["hang"], now=clock, breaker=gate)
|
||||
entry = await inner.try_enter("m1", "setup")
|
||||
await inner.record_failure(entry, "source_dead", True) # force_open → OPEN
|
||||
clock.advance(61.0) # 越过 cooldown_s=60 → HALF_OPEN
|
||||
task = asyncio.create_task(client.recognize_text(b"jpg"))
|
||||
await asyncio.sleep(0.05)
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
assert gate.probe_releases >= 1 # 探针已归还,不悬挂
|
||||
probe = await inner.try_enter("m1", "again")
|
||||
assert probe.allowed and probe.is_probe # 可再探 = 未悬挂的行为证据
|
||||
|
||||
async def test_cancel_during_backoff_sleep_propagates(self):
|
||||
async def cancelling_sleep(delay):
|
||||
raise asyncio.CancelledError()
|
||||
|
||||
client, limiter, _ = _client(
|
||||
[_src()], [TransientError("boom", status_code=500)], sleep=cancelling_sleep
|
||||
)
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await client.recognize_text(b"jpg")
|
||||
stats = await limiter.source_stats("m1")
|
||||
assert stats.inflight == 0 # 退避期取消: permit 已在 finally 释放
|
||||
|
||||
async def test_cancel_during_transport_releases_permit(self):
|
||||
src = _src(max_concurrency=1)
|
||||
client, limiter, _ = _client([src], ["hang"])
|
||||
|
||||
@@ -52,3 +52,22 @@ class TestErrorsClassified:
|
||||
rows = [_row(error="KeyError: 'oops'")]
|
||||
with pytest.raises(AssertionError, match="未分类"):
|
||||
inv_errors_classified(rows, _KNOWN)
|
||||
|
||||
|
||||
class TestRssAbsolute:
|
||||
def test_within_bound(self):
|
||||
from tools.soak.scoreboard import inv_rss_absolute
|
||||
|
||||
inv_rss_absolute([120.0, 180.0, 150.0], max_mb=500.0)
|
||||
|
||||
def test_peak_exceeds(self):
|
||||
from tools.soak.scoreboard import inv_rss_absolute
|
||||
|
||||
with pytest.raises(AssertionError, match="峰值"):
|
||||
inv_rss_absolute([120.0, 690.0, 200.0], max_mb=500.0) # verifier I1 实测形态
|
||||
|
||||
def test_empty_rejected(self):
|
||||
from tools.soak.scoreboard import inv_rss_absolute
|
||||
|
||||
with pytest.raises(AssertionError):
|
||||
inv_rss_absolute([], max_mb=500.0)
|
||||
|
||||
Reference in New Issue
Block a user