From d3fa848c99c716dfbd2aa53000e6aa82d07ec5e6 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sun, 17 May 2026 08:47:35 -0500 Subject: [PATCH 1/2] test(integration): poll for query-exchange visibility after POST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Demo's query-exchange replica lags writes by ~10 seconds. Orders and order_groups created via POST aren't immediately readable via their GET-by-id endpoints — the failing tests asserted GET success immediately after CREATE and reliably hit 404 ("not_found" / service: query-exchange). Probe evidence (orders): create -> order_id X poll 1-3 (~1.5s): 404 not_found ... poll @ 10s: GET works, status=resting Same pattern for order_groups (~9.5s). - Added wait_for_resource (sync) and await_resource (async) helpers in tests/integration/helpers.py — bounded poll loop that swallows KalshiNotFoundError until timeout (default 15s, interval 0.5s) and re-raises if the resource never propagates. - Wrapped GET-after-POST sites in test_orders.py (test_order_fill_lifecycle, test_create_get_cancel sync + async) and test_order_groups.py (test_create_and_get, test_update_limit, TestOrderGroupsAsync.test_create_get_delete). - Dropped the now-redundant `await asyncio.sleep(0.5)` workaround + unused asyncio import from test_order_groups.py. Verified locally: tests/integration/test_orders.py + tests/integration/test_order_groups.py — 22/22 passed. Closes 5 of 11 integration-test failures (#124). Running tally: A (#126=4) + B (#127=1) + E (#128=1) + C (this=5) = 11/11. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/helpers.py | 49 ++++++++++++++++++++++++-- tests/integration/test_order_groups.py | 18 ++++++---- tests/integration/test_orders.py | 17 +++++---- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 0340a81..5c2243f 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -5,7 +5,8 @@ import asyncio import functools import logging -from collections.abc import Callable +import time +from collections.abc import Awaitable, Callable from decimal import ROUND_HALF_UP, Decimal from typing import Any, TypeVar @@ -13,7 +14,51 @@ from websockets.exceptions import ConnectionClosed from kalshi.client import KalshiClient -from kalshi.errors import KalshiConnectionError, KalshiError +from kalshi.errors import KalshiConnectionError, KalshiError, KalshiNotFoundError + +T = TypeVar("T") + + +def wait_for_resource( + fetch: Callable[[], T], + *, + timeout: float = 15.0, + interval: float = 0.5, +) -> T: + """Poll ``fetch()`` until it stops raising KalshiNotFoundError. + + Demo's ``query-exchange`` replica lags writes by ~10 seconds: orders + and order_groups created via POST are not immediately readable via + GET-by-id. Use this anywhere a test writes a resource and then + immediately reads it back. + """ + deadline = time.monotonic() + timeout + last_error: KalshiNotFoundError | None = None + while True: + try: + return fetch() + except KalshiNotFoundError as exc: + last_error = exc + if time.monotonic() >= deadline: + raise + time.sleep(interval) + + +async def await_resource( + fetch: Callable[[], Awaitable[T]], + *, + timeout: float = 15.0, + interval: float = 0.5, +) -> T: + """Async counterpart of :func:`wait_for_resource`.""" + deadline = time.monotonic() + timeout + while True: + try: + return await fetch() + except KalshiNotFoundError: + if time.monotonic() >= deadline: + raise + await asyncio.sleep(interval) logger = logging.getLogger(__name__) diff --git a/tests/integration/test_order_groups.py b/tests/integration/test_order_groups.py index d3390ed..ee02f20 100644 --- a/tests/integration/test_order_groups.py +++ b/tests/integration/test_order_groups.py @@ -2,7 +2,6 @@ from __future__ import annotations -import asyncio import logging from collections.abc import Iterator @@ -19,6 +18,7 @@ from tests.integration.assertions import assert_model_fields from tests.integration.conftest import skip_if_low_balance from tests.integration.coverage_harness import register +from tests.integration.helpers import await_resource, wait_for_resource logger = logging.getLogger(__name__) @@ -69,7 +69,10 @@ def test_list(self, sync_client: KalshiClient) -> None: def test_create_and_get( self, sync_client: KalshiClient, ephemeral_group: str, ) -> None: - resp = sync_client.order_groups.get(ephemeral_group) + # Demo query-exchange lags writes by ~10s after create. + resp = wait_for_resource( + lambda: sync_client.order_groups.get(ephemeral_group), + ) assert isinstance(resp, GetOrderGroupResponse) assert_model_fields(resp) @@ -77,7 +80,9 @@ def test_update_limit( self, sync_client: KalshiClient, ephemeral_group: str, ) -> None: sync_client.order_groups.update_limit(ephemeral_group, contracts_limit=5) - resp = sync_client.order_groups.get(ephemeral_group) + resp = wait_for_resource( + lambda: sync_client.order_groups.get(ephemeral_group), + ) # Server may normalize the limit — just assert round-trip works assert resp.contracts_limit is not None @@ -117,9 +122,10 @@ async def test_create_get_delete( resp = await async_client.order_groups.create(contracts_limit=1) assert isinstance(resp, CreateOrderGroupResponse) try: - # Demo server may need a moment to propagate the new group. - await asyncio.sleep(0.5) - got = await async_client.order_groups.get(resp.order_group_id) + # Demo query-exchange lags writes by ~10s after create. + got = await await_resource( + lambda: async_client.order_groups.get(resp.order_group_id), + ) assert isinstance(got, GetOrderGroupResponse) finally: await async_client.order_groups.delete(resp.order_group_id) diff --git a/tests/integration/test_orders.py b/tests/integration/test_orders.py index 5e2f933..4483736 100644 --- a/tests/integration/test_orders.py +++ b/tests/integration/test_orders.py @@ -15,7 +15,7 @@ from tests.integration.assertions import assert_model_fields from tests.integration.conftest import skip_if_low_balance from tests.integration.coverage_harness import register -from tests.integration.helpers import fill_guarantee +from tests.integration.helpers import await_resource, fill_guarantee, wait_for_resource logger = logging.getLogger(__name__) @@ -92,9 +92,10 @@ def test_order_fill_lifecycle( ) # Check order statuses — on demo, self-trade prevention may - # cancel one side immediately - buy_order = sync_client.orders.get(buy_id) - sell_order = sync_client.orders.get(sell_id) + # cancel one side immediately. Demo's query-exchange replica + # lags writes by ~10s, so poll for visibility. + buy_order = wait_for_resource(lambda: sync_client.orders.get(buy_id)) + sell_order = wait_for_resource(lambda: sync_client.orders.get(sell_id)) assert_model_fields(buy_order) assert_model_fields(sell_order) @@ -151,7 +152,9 @@ def test_create_get_cancel( assert order.order_id try: - retrieved = sync_client.orders.get(order.order_id) + retrieved = wait_for_resource( + lambda: sync_client.orders.get(order.order_id), + ) assert isinstance(retrieved, Order) assert_model_fields(retrieved) assert retrieved.order_id == order.order_id @@ -262,7 +265,9 @@ async def test_create_get_cancel( assert order.order_id try: - retrieved = await async_client.orders.get(order.order_id) + retrieved = await await_resource( + lambda: async_client.orders.get(order.order_id), + ) assert isinstance(retrieved, Order) assert_model_fields(retrieved) assert retrieved.order_id == order.order_id From 6b5f102dbde1e61f7036d33da1cea8df87878401 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Sun, 17 May 2026 09:00:52 -0500 Subject: [PATCH 2/2] review(#129): drop dead last_error var, use PEP 695 type params Per bot review on PR #129: - Removed unused last_error assignment in wait_for_resource (ruff F841; caused CI lint failure). bare 'raise' re-raises the active exc directly, so the captured ref was dead. - Switched both helpers to PEP 695 type parameter syntax (def wait_for_resource[T](...)) to clear UP047. Drops the standalone TypeVar import. - Expanded await_resource one-line docstring with the demo-lag context so the file reads top-to-bottom. Verified: uv run ruff check . clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/integration/helpers.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/integration/helpers.py b/tests/integration/helpers.py index 5c2243f..15891dd 100644 --- a/tests/integration/helpers.py +++ b/tests/integration/helpers.py @@ -16,10 +16,8 @@ from kalshi.client import KalshiClient from kalshi.errors import KalshiConnectionError, KalshiError, KalshiNotFoundError -T = TypeVar("T") - -def wait_for_resource( +def wait_for_resource[T]( fetch: Callable[[], T], *, timeout: float = 15.0, @@ -33,24 +31,22 @@ def wait_for_resource( immediately reads it back. """ deadline = time.monotonic() + timeout - last_error: KalshiNotFoundError | None = None while True: try: return fetch() - except KalshiNotFoundError as exc: - last_error = exc + except KalshiNotFoundError: if time.monotonic() >= deadline: raise time.sleep(interval) -async def await_resource( +async def await_resource[T]( fetch: Callable[[], Awaitable[T]], *, timeout: float = 15.0, interval: float = 0.5, ) -> T: - """Async counterpart of :func:`wait_for_resource`.""" + """Async counterpart of :func:`wait_for_resource` — same ~10s demo lag rationale.""" deadline = time.monotonic() + timeout while True: try: