Umbrella issue for the remaining test-coverage gaps from Wave 5 audit Q. Individually each is small; collectively they harden the suite.
F-Q-09 — `KalshiAuth.from_key_path` exception branches
- `auth.py:72-73` `PermissionError` when reading a key file the user lacks permission for.
- `auth.py:83-87` `TypeError` from `load_pem_private_key` on a passphrase-protected key (helpful "remove the passphrase" message).
Both have helpful UX messaging worth pinning.
Test: `chmod 000` a tempfile → `from_key_path` → assert wrapped `KalshiAuthError` contains "Permission denied". Generate an RSA key with `BestAvailableEncryption(b"pw")`, write to disk → `from_key_path` → assert error includes "Passphrase-protected" and the openssl hint.
F-Q-10 — `_to_decimal_dollars` / `_to_decimal_fp` `TypeError` fallback
`kalshi/types.py:27, 60` — `raise TypeError(f"Cannot convert {type(value).name} to Decimal")` for unexpected types. Search for that error string returns nothing.
Test: `class M(BaseModel): x: DollarDecimal` → `M.model_validate({"x": [1, 2]})` → assert `ValidationError` wraps `TypeError("Cannot convert list to Decimal")`. Mirror for `FixedPointCount`.
F-Q-12 — `is_authenticated` on the client facades
`KalshiClient.is_authenticated` and `AsyncKalshiClient.is_authenticated` are public properties (`client.py:119`, `async_client.py:117`) but only exercised in integration tests that skip without credentials.
Test: `KalshiClient(auth=test_auth).is_authenticated is True` and `KalshiClient().is_authenticated is False`. Mirror async.
F-Q-13 — WS timing-dependent assertions use bare `asyncio.sleep`
`tests/ws/test_client.py:212, 260, 297`, `tests/ws/test_integration.py:408, 464, 553, 582` use `await asyncio.sleep(0.1-0.3)` then assert. On a loaded CI runner with GIL contention these are flaky.
Fix: replace sleep-then-assert with an `asyncio.Event` that the callback `set()`s. `await asyncio.wait_for(event.wait(), timeout=2.0)` instead. No flake, faster passes, pedagogically correct (the dispatcher is deterministic, not eventually-consistent).
F-Q-15 — Multiple sids for the same channel on the same connection
`test_two_channels_on_same_connection` covers two different channels. The interesting case is two subs to the SAME channel with different params (e.g. two `orderbook_delta` subs with different `market_tickers`). Untested.
Test: subscribe twice to `orderbook_delta` with different tickers. Assert distinct `server_sid`s. Push two messages with the two sids. Read both iterators with `wait_for`. Assert each gets exactly its own message.
F-Q-16 — `_join_tickers` doesn't pin behavior for non-string elements
`resources/_base.py:47-67` — type signature is `list[str] | tuple[str, ...] | str | None` but no `isinstance(elem, str)` check. A `bool` or `int` element crashes at `"," in elem` with `TypeError: argument of type 'bool' is not iterable` — unhelpful error.
Fix: either add `isinstance` validation with a clear message, OR pin the current crash with `pytest.raises(TypeError, match="argument of type 'bool'")`. Pick one.
F-Q-18 — Recv-loop "reconnect failed → sentinel broadcast" path untested
`ws/client.py:197-203`: when `_connection.reconnect()` raises, the code broadcasts a sentinel to every active queue so iterators don't hang. `test_reconnect_max_retries_exceeded` tests the connection manager's failure; the higher-level "iterator gets sentinel and exits cleanly" promise isn't tested.
Test: connect via `KalshiWebSocket`, subscribe to ticker, get an iterator. Cause the fake server to close uncleanly with `ws_max_retries=1` and permanent reject. Assert `async for msg in stream:` exits via `StopAsyncIteration` within bounded timeout rather than hanging.
All seven are individually small, but together they shore up the long-tail. The deterministic-await pattern in F-Q-13 is the most impactful — a CI flake-elimination pass.
Umbrella issue for the remaining test-coverage gaps from Wave 5 audit Q. Individually each is small; collectively they harden the suite.
F-Q-09 — `KalshiAuth.from_key_path` exception branches
Both have helpful UX messaging worth pinning.
Test: `chmod 000` a tempfile → `from_key_path` → assert wrapped `KalshiAuthError` contains "Permission denied". Generate an RSA key with `BestAvailableEncryption(b"pw")`, write to disk → `from_key_path` → assert error includes "Passphrase-protected" and the openssl hint.
F-Q-10 — `_to_decimal_dollars` / `_to_decimal_fp` `TypeError` fallback
`kalshi/types.py:27, 60` — `raise TypeError(f"Cannot convert {type(value).name} to Decimal")` for unexpected types. Search for that error string returns nothing.
Test: `class M(BaseModel): x: DollarDecimal` → `M.model_validate({"x": [1, 2]})` → assert `ValidationError` wraps `TypeError("Cannot convert list to Decimal")`. Mirror for `FixedPointCount`.
F-Q-12 — `is_authenticated` on the client facades
`KalshiClient.is_authenticated` and `AsyncKalshiClient.is_authenticated` are public properties (`client.py:119`, `async_client.py:117`) but only exercised in integration tests that skip without credentials.
Test: `KalshiClient(auth=test_auth).is_authenticated is True` and `KalshiClient().is_authenticated is False`. Mirror async.
F-Q-13 — WS timing-dependent assertions use bare `asyncio.sleep`
`tests/ws/test_client.py:212, 260, 297`, `tests/ws/test_integration.py:408, 464, 553, 582` use `await asyncio.sleep(0.1-0.3)` then assert. On a loaded CI runner with GIL contention these are flaky.
Fix: replace sleep-then-assert with an `asyncio.Event` that the callback `set()`s. `await asyncio.wait_for(event.wait(), timeout=2.0)` instead. No flake, faster passes, pedagogically correct (the dispatcher is deterministic, not eventually-consistent).
F-Q-15 — Multiple sids for the same channel on the same connection
`test_two_channels_on_same_connection` covers two different channels. The interesting case is two subs to the SAME channel with different params (e.g. two `orderbook_delta` subs with different `market_tickers`). Untested.
Test: subscribe twice to `orderbook_delta` with different tickers. Assert distinct `server_sid`s. Push two messages with the two sids. Read both iterators with `wait_for`. Assert each gets exactly its own message.
F-Q-16 — `_join_tickers` doesn't pin behavior for non-string elements
`resources/_base.py:47-67` — type signature is `list[str] | tuple[str, ...] | str | None` but no `isinstance(elem, str)` check. A `bool` or `int` element crashes at `"," in elem` with `TypeError: argument of type 'bool' is not iterable` — unhelpful error.
Fix: either add `isinstance` validation with a clear message, OR pin the current crash with `pytest.raises(TypeError, match="argument of type 'bool'")`. Pick one.
F-Q-18 — Recv-loop "reconnect failed → sentinel broadcast" path untested
`ws/client.py:197-203`: when `_connection.reconnect()` raises, the code broadcasts a sentinel to every active queue so iterators don't hang. `test_reconnect_max_retries_exceeded` tests the connection manager's failure; the higher-level "iterator gets sentinel and exits cleanly" promise isn't tested.
Test: connect via `KalshiWebSocket`, subscribe to ticker, get an iterator. Cause the fake server to close uncleanly with `ws_max_retries=1` and permanent reject. Assert `async for msg in stream:` exits via `StopAsyncIteration` within bounded timeout rather than hanging.
All seven are individually small, but together they shore up the long-tail. The deterministic-await pattern in F-Q-13 is the most impactful — a CI flake-elimination pass.