Skip to content

Commit f787db5

Browse files
committed
test: guard that every response type carries the envelope
The four statically-typed SDKs each got a sweep guard; Python and JS did not, purely because TypedDicts and interfaces are erased at runtime and there was no object to reflect over. That left the largest sweep in the fleet - 84 types in the live Python SDK - resting on a regex having touched every one, with nothing to catch a type added later. Reading the declarations from source removes the excuse. The guard walks the declared annotations rather than any instance, so it checks the source has not drifted rather than any runtime behaviour. It asserts the scan found response types at all, so it cannot pass vacuously if a module is renamed or the scan breaks, and pins the nine feed names: spot and options being reported separately is a contract shared with the live API and the other SDKs, and collapsing any pair would lose the distinction the field exists to make. Mutation-checked by removing the envelope from VexResponse.
1 parent 89bd904 commit f787db5

1 file changed

Lines changed: 64 additions & 0 deletions

File tree

tests/test_response_envelope.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Guard that every response type carries the envelope.
2+
3+
The envelope was added to 84 TypedDicts by a sweep. Trusting that the sweep reached
4+
all of them - and that a response type added later will not quietly miss it - is
5+
exactly the assumption worth testing.
6+
7+
TypedDicts are erased at runtime, so this checks the declared annotations rather than
8+
any instance: it is a guard against the source drifting, not a runtime behaviour test.
9+
"""
10+
11+
import pytest
12+
13+
from flashalpha import DataAsOf, types
14+
15+
16+
def response_types():
17+
"""Every *Response TypedDict declared in the types module."""
18+
return [
19+
(name, obj)
20+
for name, obj in vars(types).items()
21+
if name.endswith("Response") and isinstance(obj, type) and hasattr(obj, "__annotations__")
22+
]
23+
24+
25+
def test_the_guard_actually_finds_response_types():
26+
# Without this the parametrized tests below would pass vacuously if the module
27+
# were renamed or the scan broke.
28+
assert len(response_types()) > 50, f"only found {len(response_types())} response types"
29+
30+
31+
@pytest.mark.parametrize("name,obj", response_types(), ids=lambda v: v if isinstance(v, str) else "")
32+
def test_every_response_type_declares_the_envelope(name, obj):
33+
annotations = obj.__annotations__
34+
assert "data_as_of" in annotations, f"{name} is missing data_as_of"
35+
assert "endpoint_version" in annotations, f"{name} is missing endpoint_version"
36+
37+
38+
def test_data_as_of_declares_every_feed():
39+
"""The nine feeds are a contract shared with the live API and the other SDKs.
40+
41+
Spot and options are separate on purpose: they arrive over different pipes and fail
42+
independently, so collapsing any pair would lose the distinction the field exists
43+
to make.
44+
"""
45+
expected = {
46+
"node",
47+
"equity_feed",
48+
"equity_options_feed",
49+
"index_feed",
50+
"index_options_feed",
51+
"futures_feed",
52+
"futures_options_feed",
53+
"flow_feed",
54+
"oi_feed",
55+
"macro_feed",
56+
}
57+
58+
assert set(DataAsOf.__annotations__) == expected
59+
60+
61+
def test_data_as_of_is_exported_from_the_package_root():
62+
import flashalpha
63+
64+
assert "DataAsOf" in flashalpha.__all__

0 commit comments

Comments
 (0)