# piker: trading gear for hackers # Copyright (C) 2018-present Tyler Goodlet (in stewardship of pikers) # SPDX-License-Identifier: AGPL-3.0-or-later # This program is free software: you can redistribute it and/or # modify it under the terms of the GNU Affero General Public License # as published by the Free Software Foundation, either version 3 # of the License, or (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public # License along with this program. If not, see # . ''' Offline TypeSafe protocol and controlled Trio scheduling regressions. ''' from dataclasses import replace import json import httpx import numpy as np import pytest import trio from trio.testing import MockClock, wait_all_tasks_blocked from piker.fsp._jev import ( API_URL, DEFAULT_MODEL, JevClassification, JevClient, JevProtocolError, JevServiceError, RegimeClassifier, build_regime_request, ) from piker.fsp._regime import ( MultiTimeframeTrend, RegimeSnapshot, TimeframeRegime, ) def _snapshots(count: int = 12) -> list[RegimeSnapshot]: ''' Produce complete synthetic evidence without a broker or network. ''' tracker: MultiTimeframeTrend = MultiTimeframeTrend( 'synthetic.test', periods=(1, 2), windows=(2,), ) snapshots: list[RegimeSnapshot] = [] second: int for second in range(count): snapshots.append(tracker.push(second, 100 + second)) return snapshots def _answer_payload(request: httpx.Request) -> dict: ''' Simulate the documented Choice response for every sent question. ''' payload: dict = json.loads(request.content) answers: dict[str, dict] = {} name: str question: dict for name, question in payload['questions'].items(): labels: list[str] = list(question['criteria']) probabilities: dict[str, float] = {} label: str for label in labels: probabilities[label] = ( .8 if label == labels[0] else .2 / (len(labels) - 1) ) answers[name] = { 'type': 'choice', 'choice': labels[0], 'confidence': .8, 'probabilities': probabilities, } return { 'model': DEFAULT_MODEL, 'answers': answers, 'usage': { 'input_tokens': 250, 'output_tokens': 32, }, } def test_request_batches_only_ready_timeframes() -> None: ''' Question IDs are not model input, so a timeframe-only identifier leaves the judgment ambiguous. Verify explicit state paths in each ready-frame question, one batched alignment question, and no request before warm-up. The provider never supplies TC state. ''' snapshots: list[RegimeSnapshot] = _snapshots() with pytest.raises(ValueError, match='warm-up'): build_regime_request(snapshots[0]) early: dict = build_regime_request(snapshots[3]) assert set(early['questions']) == {'timeframe_0'} ready: dict = build_regime_request(snapshots[-1]) assert set(ready['questions']) == { 'timeframe_0', 'timeframe_1', 'alignment', } assert ready['model'] == DEFAULT_MODEL assert 'timeframes[1]' in ( ready['questions']['timeframe_1']['instructions'] ) assert 'unknown' in ready['questions']['timeframe_0']['criteria'] assert ready['state']['algorithm'] == 'trend_calculus' def test_graphics_snapshot_projects_to_bounded_model_evidence( ) -> None: ''' Graphics retains 256 observations for visible pivots, exceeding the default HTTP request budget if sent directly to Jev. Build an actual 4500-bar, four-order tracker with that rendering tail; model preparation must keep only its configured evidence tails while preserving source provenance, readiness and total counts. The original rendering snapshot must remain unchanged, and the projected request must pass through the ordinary HTTP adapter. ''' tracker: MultiTimeframeTrend = MultiTimeframeTrend( 'test.market', periods=(1, 5, 15), windows=(2,) * 4, history_limit=256, ) second: int for second in range(4500): snapshot: RegimeSnapshot = tracker.push( second, 100 + float(np.sin(second / 3)), ) full: dict = build_regime_request(snapshot, evidence_limit=256) projected: dict = build_regime_request(snapshot) assert len(json.dumps(full).encode()) > 65_536 assert len(json.dumps(projected).encode()) < 65_536 assert len(snapshot.timeframes[0].recent_closes) == 256 assert projected['state']['revision'] == snapshot.revision assert projected['state']['sequence'] == snapshot.sequence frame: dict source: TimeframeRegime for frame, source in zip( projected['state']['timeframes'], snapshot.timeframes, ): assert len(frame['recent_closes']) <= 16 assert len(frame['reversals']) <= 16 assert frame['close_count'] == source.close_count assert frame['ready'] == source.ready calls: int = 0 def handler(request: httpx.Request) -> httpx.Response: ''' Prove projection precedes the client's byte-budget gate. ''' nonlocal calls calls += 1 assert len(request.content) <= 65_536 return httpx.Response(200, json=_answer_payload(request)) async def scenario() -> None: ''' Exercise normal client admission with the larger UI snapshot. ''' http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: client: JevClient = JevClient(http, 'offline-test-key') result: JevClassification = await client.classify( snapshot, ) assert result.sequence == snapshot.sequence assert len(result.answers) == 4 trio.run(scenario) assert calls == 1 def test_http_contract_retains_probabilities_and_source_times( ) -> None: ''' Collapsing a provider result to a label loses uncertainty and the source revision needed to reject stale UI updates. MockTransport inspects the actual HTTP body and bearer header, then responds using the documented schema. Every probability, source identity, model version and local availability time must survive parsing. ''' snapshot: RegimeSnapshot = _snapshots()[-1] requests: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: ''' Capture the request without permitting network I/O. ''' requests.append(request) return httpx.Response(200, json=_answer_payload(request)) async def scenario() -> None: ''' Exercise the same Trio-compatible HTTP path as live calls. ''' http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: client: JevClient = JevClient(http, 'offline-test-key') result: JevClassification = await client.classify( snapshot, ) assert result.market == snapshot.market assert result.revision == snapshot.revision assert result.source_as_of == snapshot.as_of assert result.sequence == snapshot.sequence assert result.requested_at <= result.received_at assert result.latency >= 0 assert result.model == DEFAULT_MODEL assert result.input_tokens == 250 assert result.answers[0].period == 1 assert result.answers[0].probability == .8 assert result.answers[0].confidence == .8 assert result.answers[-1].period is None distribution: dict = dict(result.answers[0].probabilities) assert sum(distribution.values()) == pytest.approx(1) trio.run(scenario) assert len(requests) == 1 assert str(requests[0].url) == API_URL assert requests[0].headers['authorization'] == ( 'Bearer offline-test-key' ) body: dict = json.loads(requests[0].content) assert body['state']['as_of'] == snapshot.as_of @pytest.mark.parametrize('fault', [ 'missing_question', 'unknown_label', 'missing_probability', 'nonfinite_probability', 'negative_probability', 'wrong_sum', 'not_maximum', 'invalid_confidence', 'wrong_model', 'negative_usage', 'boolean_usage', ]) def test_malformed_response_cannot_publish(fault: str) -> None: ''' A typed API can still fail at the network/schema boundary. Deliberately corrupt one otherwise valid response component and require a protocol error. This prevents invalid probabilities, incomplete classifications or unexpected model versions from entering the same display path as validated semantic judgments. ''' def handler(request: httpx.Request) -> httpx.Response: ''' Inject one isolated contract violation into a valid answer. ''' body: dict = _answer_payload(request) answer: dict = body['answers']['timeframe_0'] probabilities: dict = answer['probabilities'] if fault == 'missing_question': del body['answers']['timeframe_0'] elif fault == 'unknown_label': answer['choice'] = 'buy_now' elif fault == 'missing_probability': del probabilities['unknown'] elif fault == 'nonfinite_probability': probabilities['uptrend'] = 'NaN' elif fault == 'negative_probability': probabilities['uptrend'] = -.8 elif fault == 'wrong_sum': probabilities['uptrend'] = .5 elif fault == 'not_maximum': answer['choice'] = 'downtrend' elif fault == 'invalid_confidence': answer['confidence'] = True elif fault == 'wrong_model': body['model'] = 'unexpected-model' elif fault == 'negative_usage': body['usage']['input_tokens'] = -1 elif fault == 'boolean_usage': body['usage']['input_tokens'] = True return httpx.Response(200, json=body) async def scenario() -> None: ''' Verify protocol rejection at the HTTP adapter boundary. ''' http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: client: JevClient = JevClient(http, 'offline-test-key') with pytest.raises(JevProtocolError): await client.classify(_snapshots()[-1]) trio.run(scenario) def test_request_and_response_budgets_prevent_unbounded_payloads( ) -> None: ''' Bounded history alone does not bound JSON bytes when parameters or provider output changes. A tiny request limit must fail before transport invocation; a tiny response limit must reject received data instead of decoding or publishing an oversized answer. ''' calls: list[httpx.Request] = [] def handler(request: httpx.Request) -> httpx.Response: ''' Count transport calls and return an ordinary valid answer. ''' calls.append(request) return httpx.Response(200, json=_answer_payload(request)) async def scenario() -> None: ''' Verify the two independent budgets in one owned HTTP session. ''' http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: client: JevClient = JevClient( http, 'offline-test-key', max_request_bytes=10, ) with pytest.raises(ValueError, match='byte budget'): await client.classify(_snapshots()[-1]) assert not calls client = JevClient( http, 'offline-test-key', max_response_bytes=10, ) with pytest.raises( JevProtocolError, match='byte budget', ): await client.classify(_snapshots()[-1]) assert len(calls) == 1 trio.run(scenario) def test_worker_coalesces_bursts_and_discards_old_revision() -> None: ''' One request per quote can create an unbounded queue, while a response from before a backfill can repaint corrected history. The ``handler()`` task sets ``entered`` before waiting on ``release``. While that request is suspended, ``scenario()`` replaces ``RegimeClassifier._pending`` with a burst and revision. Releasing the first request must discard its old evidence and publish only the replacement. Request count and ``peak`` prove coalescing and single-flight transport without scheduler timing. ''' requests: list[dict] = [] published: list[JevClassification] = [] active: int = 0 peak: int = 0 async def scenario() -> None: ''' Control response order with events instead of scheduler luck. ''' entered: trio.Event = trio.Event() release: trio.Event = trio.Event() async def handler(request: httpx.Request) -> httpx.Response: ''' Keep the first network operation in flight during submit. ''' nonlocal active, peak active += 1 peak = max(peak, active) requests.append(json.loads(request.content)) try: if len(requests) == 1: entered.set() await release.wait() return httpx.Response( 200, json=_answer_payload(request), ) finally: active -= 1 http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=0, max_source_lag=100, ) snapshots: list[RegimeSnapshot] = _snapshots() worker.submit(snapshots[7]) nursery: trio.Nursery async with trio.open_nursery() as nursery: async def publish(result: JevClassification) -> None: ''' Stop after the fresh result proves publication. ''' published.append(result) nursery.cancel_scope.cancel() nursery.start_soon(worker.run, publish) await entered.wait() snapshot: RegimeSnapshot for snapshot in snapshots[8:]: worker.submit(snapshot) newest: RegimeSnapshot = replace( snapshots[-1], revision=1, sequence=1, ) worker.submit(newest) release.set() assert worker.discarded == 1 assert worker.requests == 2 assert worker.published == 1 trio.run(scenario) assert len(requests) == 2 assert peak == 1 assert len(published) == 1 assert published[0].revision == 1 assert requests[1]['state']['revision'] == 1 assert requests[1]['state']['sequence'] == 1 @pytest.mark.parametrize('invalidation', ['lag', 'segment', 'age']) def test_worker_rejects_stale_results(invalidation: str) -> None: ''' Matching ``RegimeSnapshot.revision`` alone does not prove freshness: a gap, advancing event clock, or slow response can invalidate evidence. ``handler()`` sets ``entered`` and waits on ``release`` while ``scenario()`` changes exactly one of those conditions. Only the second response may publish. ``MockClock`` controls elapsed age; the two events prove the first response cannot finish before its evidence becomes obsolete. ''' published: list[JevClassification] = [] async def scenario() -> None: ''' Delay the first response while replacing source evidence. ''' entered: trio.Event = trio.Event() release: trio.Event = trio.Event() calls: int = 0 async def handler(request: httpx.Request) -> httpx.Response: ''' Suspend only the first request at a cancellable event. ''' nonlocal calls calls += 1 if calls == 1: entered.set() await release.wait() return httpx.Response(200, json=_answer_payload(request)) http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key', timeout=20), min_interval=0, max_age=5, max_source_lag=1, ) snapshots: list[RegimeSnapshot] = _snapshots() worker.submit(snapshots[7]) nursery: trio.Nursery async with trio.open_nursery() as nursery: async def publish(result: JevClassification) -> None: ''' Exit only after an admissible response arrives. ''' published.append(result) nursery.cancel_scope.cancel() nursery.start_soon(worker.run, publish) await entered.wait() newest: RegimeSnapshot = snapshots[8] if invalidation == 'lag': newest = snapshots[-1] elif invalidation == 'segment': newest = replace(newest, segment=1) else: await trio.sleep(6) worker.submit(newest) release.set() assert worker.discarded == 1 assert published[0].sequence == newest.sequence assert worker.published == 1 trio.run(scenario, clock=MockClock(autojump_threshold=0)) def test_worker_throttles_and_honors_retry_after() -> None: ''' Immediate retries amplify rate limits and may resend old market evidence. Return HTTP 429 with a three-second Retry-After, submit newer evidence while the worker waits, and use MockClock to prove the retry respects that delay and contains newest state. ''' sent: list[tuple[float, int]] = [] errors: list[Exception] = [] async def scenario() -> None: ''' Advance simulated time only after tasks reach their waits. ''' failed: trio.Event = trio.Event() async def handler(request: httpx.Request) -> httpx.Response: ''' Rate-limit the initial request and accept its successor. ''' body: dict = json.loads(request.content) sent.append(( trio.current_time(), body['state']['sequence'], )) if len(sent) == 1: return httpx.Response( 429, headers={'retry-after': '3'}, ) return httpx.Response(200, json=_answer_payload(request)) async def on_error(error: Exception) -> None: ''' Signal the exact rate-limit failure before new evidence. ''' errors.append(error) failed.set() http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=2, max_age=10, max_source_lag=10, ) snapshots: list[RegimeSnapshot] = _snapshots() worker.submit(snapshots[7]) nursery: trio.Nursery async with trio.open_nursery() as nursery: async def publish(result: JevClassification) -> None: ''' End the test after successful recovery. ''' nursery.cancel_scope.cancel() async def run() -> None: ''' Bind the worker's optional failure callback. ''' await worker.run(publish, on_error=on_error) nursery.start_soon(run) await failed.wait() worker.submit(snapshots[-1]) assert worker.requests == 2 assert worker.failures == 1 assert worker.last_error is None trio.run(scenario, clock=MockClock(autojump_threshold=0)) assert sent[1][0] - sent[0][0] >= 3 assert sent[1][1] == 12 assert isinstance(errors[0], JevServiceError) def test_cancellation_closes_inflight_work() -> None: ''' An HTTP operation detached from its nursery can outlive a chart and use credentials after shutdown. ``handler()`` signals ``entered`` before ``trio.sleep_forever()``; ``scenario()`` waits for that signal before cancelling the worker nursery. The ``closed`` event proves the transport's finalizer runs before nursery exit, and ``publish()`` rejects any late output. This proves cooperative cancellation, not an uninterruptible syscall. ''' async def scenario() -> None: ''' Cancel only once the mock request is known to be in flight. ''' entered: trio.Event = trio.Event() closed: trio.Event = trio.Event() async def handler(request: httpx.Request) -> httpx.Response: ''' Model a cancellable stalled network operation. ''' entered.set() try: await trio.sleep_forever() finally: closed.set() async def publish(result: JevClassification) -> None: ''' Reject any publication after cancellation begins. ''' pytest.fail('cancelled request published a result') http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=0, ) worker.submit(_snapshots()[-1]) nursery: trio.Nursery async with trio.open_nursery() as nursery: nursery.start_soon(worker.run, publish) await entered.wait() nursery.cancel_scope.cancel() assert closed.is_set() assert worker.published == 0 trio.run(scenario) def test_submit_rejects_wrong_market_and_nonmonotonic_source( ) -> None: ''' Sharing a worker across markets or submitting old source evidence can overwrite its sole pending slot. Feed valid then invalid identities without starting the worker and prove invalid updates are refused before any HTTP call can be made. ''' async def scenario() -> None: ''' Exercise submission validation on an otherwise idle worker. ''' http: httpx.AsyncClient async with httpx.AsyncClient() as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), ) snapshot: RegimeSnapshot = _snapshots()[-1] assert worker.submit(snapshot) assert not worker.submit(snapshot) with pytest.raises(ValueError, match='market'): worker.submit(replace(snapshot, market='other.test')) with pytest.raises(ValueError, match='as_of'): worker.submit(replace(snapshot, sequence=13)) assert worker.requests == 0 trio.run(scenario) def test_worker_publishes_bounded_lag_with_original_provenance( ) -> None: ''' Rejecting every response after a faster input update starves live inference. Permit one second of lag, hold the first response, then submit its successor. Both may publish, but the older result must retain its own source sequence and as_of rather than being relabeled with the newer evidence's coordinates. ''' published: list[JevClassification] = [] async def scenario() -> None: ''' Force a new source update before the first response returns. ''' entered: trio.Event = trio.Event() release: trio.Event = trio.Event() calls: int = 0 async def handler(request: httpx.Request) -> httpx.Response: ''' Hold one response while leaving later requests immediate. ''' nonlocal calls calls += 1 if calls == 1: entered.set() await release.wait() return httpx.Response(200, json=_answer_payload(request)) http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=0, max_source_lag=1, ) snapshots: list[RegimeSnapshot] = _snapshots() worker.submit(snapshots[7]) nursery: trio.Nursery async with trio.open_nursery() as nursery: async def publish(result: JevClassification) -> None: ''' Stop after observing both ordered outputs. ''' published.append(result) if len(published) == 2: nursery.cancel_scope.cancel() nursery.start_soon(worker.run, publish) await entered.wait() worker.submit(snapshots[8]) release.set() assert worker.discarded == 0 assert worker.published == 2 trio.run(scenario) result: JevClassification # noqa: F842 assert [result.sequence for result in published] == [8, 9] assert [result.source_as_of for result in published] == [8, 9] def test_worker_transient_retry_count_is_bounded() -> None: ''' An unavailable service must not generate an endless retry loop for one snapshot. Return 503 on every call, allow two retries, and use MockClock to reach the third failure. Once all tasks are blocked, the worker must be idle until new evidence arrives; cancelling it must not reveal another scheduled request. ''' calls: list[float] = [] async def scenario() -> None: ''' Observe exhausted retries before cancelling the idle worker. ''' exhausted: trio.Event = trio.Event() errors: int = 0 async def handler(request: httpx.Request) -> httpx.Response: ''' Keep the service unavailable for the whole scenario. ''' calls.append(trio.current_time()) return httpx.Response(503) async def on_error(error: Exception) -> None: ''' Signal when the final allowed attempt has failed. ''' nonlocal errors errors += 1 if errors == 3: exhausted.set() async def publish(result: JevClassification) -> None: ''' Reject fabricated results during provider unavailability. ''' pytest.fail('a failed request must not publish') http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=0, max_age=100, max_retries=2, ) worker.submit(_snapshots()[-1]) async def run() -> None: ''' Attach the failure observer to the worker lifetime. ''' await worker.run(publish, on_error=on_error) nursery: trio.Nursery async with trio.open_nursery() as nursery: nursery.start_soon(run) await exhausted.wait() await wait_all_tasks_blocked() assert worker.requests == 3 assert worker.failures == 3 nursery.cancel_scope.cancel() trio.run(scenario, clock=MockClock(autojump_threshold=0)) assert calls == [0, 1, 3] def test_worker_drops_expired_pending_work_before_network() -> None: ''' Throttling can make a queued snapshot expire even if no request is in flight. Publish one result, enqueue its successor, then advance MockClock beyond its five-second age limit but to the next ten-second request slot. The expired successor must be dropped before transport invocation, not merely after a reply. ''' clock: MockClock = MockClock() calls: int = 0 async def scenario() -> None: ''' Move time only after the worker reaches its throttle wait. ''' first: trio.Event = trio.Event() async def handler(request: httpx.Request) -> httpx.Response: ''' Count requests independently of worker statistics. ''' nonlocal calls calls += 1 return httpx.Response(200, json=_answer_payload(request)) http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=10, max_age=5, ) snapshots: list[RegimeSnapshot] = _snapshots() worker.submit(snapshots[7]) async def publish(result: JevClassification) -> None: ''' Submit fresh evidence at the initial publication. ''' assert worker.published == 0 worker.submit(snapshots[8]) first.set() nursery: trio.Nursery async with trio.open_nursery() as nursery: nursery.start_soon(worker.run, publish) await first.wait() await wait_all_tasks_blocked() clock.jump(10) await wait_all_tasks_blocked() assert worker.requests == 1 assert worker.discarded == 1 nursery.cancel_scope.cancel() trio.run(scenario, clock=clock) assert calls == 1 def test_authentication_failure_propagates_without_retry() -> None: ''' Repeatedly retrying an invalid credential consumes requests and hides configuration failures. A 401 response must escape the worker immediately with a sanitized status-only error; neither the key nor the server body should appear in its exception text. ''' async def scenario() -> None: ''' Return an authentication failure through the real HTTP path. ''' def handler(request: httpx.Request) -> httpx.Response: ''' Supply a body that should never be retained in the error. ''' return httpx.Response(401, text='private server details') async def publish(result: JevClassification) -> None: ''' Reject any result produced without authentication. ''' pytest.fail('unauthenticated request published') http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), ) worker.submit(_snapshots()[-1]) caught: pytest.ExceptionInfo[JevServiceError] with pytest.raises(JevServiceError) as caught: await worker.run(publish) assert caught.value.status == 401 assert 'offline-test-key' not in str(caught.value) assert 'private server details' not in str(caught.value) assert worker.requests == 1 trio.run(scenario) @pytest.mark.parametrize( 'invalidation', ['revision', 'segment', 'age'], ) def test_blocked_publication_is_cancelled_when_evidence_changes( invalidation: str, ) -> None: ''' Checking freshness before awaiting an output consumer is not enough: a blocked send can deliver obsolete results after source revision, gap or expiry. ``publish()`` sets ``entered`` before awaiting ``released``. The ``scenario()`` task then changes the revision/segment or advances ``MockClock`` past expiry and waits for ``cancelled`` before releasing the consumer. An empty sink proves the obsolete delivery was revoked; final provenance and counters prove only the replacement reaches ``published``. The event wait models cooperative backpressure, not a stuck syscall. ''' published: list[JevClassification] = [] async def scenario() -> None: ''' Distinguish a blocked delivery from one already delivered. ''' entered: trio.Event = trio.Event() released: trio.Event = trio.Event() cancelled: trio.Event = trio.Event() attempts: int = 0 def handler(request: httpx.Request) -> httpx.Response: ''' Complete HTTP immediately; delay the output consumer. ''' return httpx.Response(200, json=_answer_payload(request)) http: httpx.AsyncClient async with httpx.AsyncClient( transport=httpx.MockTransport(handler), ) as http: worker: RegimeClassifier = RegimeClassifier( JevClient(http, 'offline-test-key'), min_interval=0, max_age=5, ) snapshots: list[RegimeSnapshot] = _snapshots() worker.submit(snapshots[7]) nursery: trio.Nursery async with trio.open_nursery() as nursery: async def publish(result: JevClassification) -> None: ''' Delay the consumer's first result acceptance. ''' nonlocal attempts attempts += 1 if attempts == 1: entered.set() try: await released.wait() except trio.Cancelled: cancelled.set() raise else: await released.wait() published.append(result) nursery.cancel_scope.cancel() nursery.start_soon(worker.run, publish) await entered.wait() newest: RegimeSnapshot = snapshots[8] if invalidation == 'revision': worker.invalidate(1) newest = replace(newest, revision=1, sequence=1) assert not worker.submit(snapshots[-1]) elif invalidation == 'segment': newest = replace(newest, segment=1) worker.submit(newest) else: await trio.sleep(6) await cancelled.wait() assert not published released.set() worker.submit(newest) assert worker.discarded == 1 assert worker.published == 1 assert published[0].sequence == newest.sequence assert published[0].revision == newest.revision assert published[0].segment == newest.segment trio.run(scenario, clock=MockClock(autojump_threshold=0))