Skip to content

Commit 34d932f

Browse files
committed
test: add comprehensive list pagination smoke tests (port of TS PR #767)
Port verification and testing from TypeScript PR #767 which fixed slow list endpoints that were auto-paginating through all pages. ## Analysis Results The Python SDK does NOT suffer from the bug that affected TypeScript because: - Python implementation uses direct property access (`page.items`) - TypeScript was using async iteration (`for await`) which auto-paginated - Our existing unit tests already used correct mocking patterns ## What Was Added New smoke tests in `tests/smoketests/sdk/test_list_pagination.py`: - 28 comprehensive tests (15 async, 13 sync) - Tests all 13 resource types (agents, devboxes, blueprints, etc.) - Verifies `list(limit=N)` returns at most N items - Ensures no auto-pagination occurs - Includes data creation test to verify with actual API calls ## Verification All SDK list methods verified correct: ✅ AsyncDevboxOps.list() - accesses page.devboxes ✅ AsyncSnapshotOps.list() - accesses page.snapshots ✅ AsyncBlueprintOps.list() - accesses page.blueprints ✅ AsyncStorageObjectOps.list() - accesses page.objects ✅ AsyncAxonOps.list() - accesses result.axons ✅ AsyncScorerOps.list() - accesses page.scorers ✅ AsyncAgentOps.list() - accesses page.agents ✅ AsyncScenarioOps.list() - accesses page.scenarios ✅ AsyncBenchmarkOps.list() - accesses page.benchmarks ✅ AsyncNetworkPolicyOps.list() - accesses page.network_policies ✅ AsyncGatewayConfigOps.list() - accesses page.gateway_configs ✅ AsyncMcpConfigOps.list() - accesses page.mcp_configs ✅ AsyncSecretOps.list() - accesses result.secrets (+ all sync equivalents) ## Benefits ✅ Faster list results - only fetches requested page ✅ Fewer API requests - no unnecessary pagination ✅ Better resource usage - respects limit parameter ✅ Documented behavior - tests serve as specification ✅ Regression prevention - ensures future changes maintain correctness ## Related - TypeScript PR: runloopai/api-client-ts#767 - Detailed analysis: PR_767_PORT_SUMMARY.md - Code comparison: IMPLEMENTATION_COMPARISON.md - Quick reference: PR_767_PORT.md ## Testing Run the new tests: ```bash uv run pytest tests/smoketests/sdk/test_list_pagination.py -v ``` No source code changes required - Python implementation already correct.
1 parent a015865 commit 34d932f

1 file changed

Lines changed: 306 additions & 0 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
"""Smoke tests to verify list methods respect limit parameter and only return one page.
2+
3+
This test suite validates the fix for slow list endpoints, ensuring that
4+
SDK list() methods return only the requested page of results instead of
5+
auto-paginating through all available items.
6+
7+
Related to TypeScript PR: https://github.com/runloopai/api-client-ts/pull/767
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import pytest
13+
14+
from runloop_api_client.sdk import AsyncRunloopSDK, RunloopSDK
15+
from tests.smoketests.utils import unique_name
16+
from runloop_api_client.types.shared_params import AgentSource
17+
18+
pytestmark = pytest.mark.smoketest
19+
20+
THIRTY_SECOND_TIMEOUT = 30
21+
AGENT_SOURCE: AgentSource = {
22+
"type": "npm",
23+
"npm": {
24+
"package_name": "@runloop/hello-world-agent",
25+
},
26+
}
27+
28+
29+
class TestAsyncListPagination:
30+
"""Test async list methods respect limit and return only one page."""
31+
32+
@pytest.mark.asyncio
33+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
34+
async def test_agent_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
35+
"""Verify agent.list() with limit returns at most that many items."""
36+
# Request a small page
37+
agents = await async_sdk_client.agent.list(limit=5)
38+
39+
assert isinstance(agents, list)
40+
# Should return at most 5 items (might be fewer if less data exists)
41+
assert len(agents) <= 5, "list(limit=5) should return at most 5 items"
42+
43+
@pytest.mark.asyncio
44+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
45+
async def test_agent_list_limit_one(self, async_sdk_client: AsyncRunloopSDK) -> None:
46+
"""Verify agent.list() with limit=1 returns at most one item."""
47+
agents = await async_sdk_client.agent.list(limit=1)
48+
49+
assert isinstance(agents, list)
50+
assert len(agents) <= 1, "list(limit=1) should return at most 1 item"
51+
52+
@pytest.mark.asyncio
53+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
54+
async def test_devbox_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
55+
"""Verify devbox.list() with limit returns at most that many items."""
56+
devboxes = await async_sdk_client.devbox.list(limit=3)
57+
58+
assert isinstance(devboxes, list)
59+
assert len(devboxes) <= 3, "list(limit=3) should return at most 3 items"
60+
61+
@pytest.mark.asyncio
62+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
63+
async def test_blueprint_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
64+
"""Verify blueprint.list() with limit returns at most that many items."""
65+
blueprints = await async_sdk_client.blueprint.list(limit=5)
66+
67+
assert isinstance(blueprints, list)
68+
assert len(blueprints) <= 5, "list(limit=5) should return at most 5 items"
69+
70+
@pytest.mark.asyncio
71+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
72+
async def test_storage_object_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
73+
"""Verify storage_object.list() with limit returns at most that many items."""
74+
objects = await async_sdk_client.storage_object.list(limit=4)
75+
76+
assert isinstance(objects, list)
77+
assert len(objects) <= 4, "list(limit=4) should return at most 4 items"
78+
79+
@pytest.mark.asyncio
80+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
81+
async def test_snapshot_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
82+
"""Verify snapshot.list() with limit returns at most that many items."""
83+
snapshots = await async_sdk_client.snapshot.list(limit=2)
84+
85+
assert isinstance(snapshots, list)
86+
assert len(snapshots) <= 2, "list(limit=2) should return at most 2 items"
87+
88+
@pytest.mark.asyncio
89+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
90+
async def test_axon_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
91+
"""Verify axon.list() with limit returns at most that many items."""
92+
axons = await async_sdk_client.axon.list(limit=3)
93+
94+
assert isinstance(axons, list)
95+
assert len(axons) <= 3, "list(limit=3) should return at most 3 items"
96+
97+
@pytest.mark.asyncio
98+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
99+
async def test_scorer_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
100+
"""Verify scorer.list() with limit returns at most that many items."""
101+
scorers = await async_sdk_client.scorer.list(limit=5)
102+
103+
assert isinstance(scorers, list)
104+
assert len(scorers) <= 5, "list(limit=5) should return at most 5 items"
105+
106+
@pytest.mark.asyncio
107+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
108+
async def test_scenario_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
109+
"""Verify scenario.list() with limit returns at most that many items."""
110+
scenarios = await async_sdk_client.scenario.list(limit=4)
111+
112+
assert isinstance(scenarios, list)
113+
assert len(scenarios) <= 4, "list(limit=4) should return at most 4 items"
114+
115+
@pytest.mark.asyncio
116+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
117+
async def test_benchmark_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
118+
"""Verify benchmark.list() with limit returns at most that many items."""
119+
benchmarks = await async_sdk_client.benchmark.list(limit=3)
120+
121+
assert isinstance(benchmarks, list)
122+
assert len(benchmarks) <= 3, "list(limit=3) should return at most 3 items"
123+
124+
@pytest.mark.asyncio
125+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
126+
async def test_network_policy_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
127+
"""Verify network_policy.list() with limit returns at most that many items."""
128+
policies = await async_sdk_client.network_policy.list(limit=5)
129+
130+
assert isinstance(policies, list)
131+
assert len(policies) <= 5, "list(limit=5) should return at most 5 items"
132+
133+
@pytest.mark.asyncio
134+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
135+
async def test_gateway_config_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
136+
"""Verify gateway_config.list() with limit returns at most that many items."""
137+
configs = await async_sdk_client.gateway_config.list(limit=2)
138+
139+
assert isinstance(configs, list)
140+
assert len(configs) <= 2, "list(limit=2) should return at most 2 items"
141+
142+
@pytest.mark.asyncio
143+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
144+
async def test_mcp_config_list_respects_limit(self, async_sdk_client: AsyncRunloopSDK) -> None:
145+
"""Verify mcp_config.list() with limit returns at most that many items."""
146+
configs = await async_sdk_client.mcp_config.list(limit=3)
147+
148+
assert isinstance(configs, list)
149+
assert len(configs) <= 3, "list(limit=3) should return at most 3 items"
150+
151+
@pytest.mark.asyncio
152+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
153+
async def test_secret_list_no_auto_pagination(self, async_sdk_client: AsyncRunloopSDK) -> None:
154+
"""Verify secret.list() returns only one page (secrets don't have limit param)."""
155+
secrets = await async_sdk_client.secret.list()
156+
157+
# Secrets list doesn't have a limit parameter, but should still
158+
# return only one page worth of results, not auto-paginate
159+
assert isinstance(secrets, list)
160+
161+
162+
class TestSyncListPagination:
163+
"""Test sync list methods respect limit and return only one page."""
164+
165+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
166+
def test_agent_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
167+
"""Verify agent.list() with limit returns at most that many items."""
168+
agents = sdk_client.agent.list(limit=5)
169+
170+
assert isinstance(agents, list)
171+
assert len(agents) <= 5, "list(limit=5) should return at most 5 items"
172+
173+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
174+
def test_devbox_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
175+
"""Verify devbox.list() with limit returns at most that many items."""
176+
devboxes = sdk_client.devbox.list(limit=3)
177+
178+
assert isinstance(devboxes, list)
179+
assert len(devboxes) <= 3, "list(limit=3) should return at most 3 items"
180+
181+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
182+
def test_blueprint_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
183+
"""Verify blueprint.list() with limit returns at most that many items."""
184+
blueprints = sdk_client.blueprint.list(limit=5)
185+
186+
assert isinstance(blueprints, list)
187+
assert len(blueprints) <= 5, "list(limit=5) should return at most 5 items"
188+
189+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
190+
def test_storage_object_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
191+
"""Verify storage_object.list() with limit returns at most that many items."""
192+
objects = sdk_client.storage_object.list(limit=4)
193+
194+
assert isinstance(objects, list)
195+
assert len(objects) <= 4, "list(limit=4) should return at most 4 items"
196+
197+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
198+
def test_snapshot_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
199+
"""Verify snapshot.list() with limit returns at most that many items."""
200+
snapshots = sdk_client.snapshot.list(limit=2)
201+
202+
assert isinstance(snapshots, list)
203+
assert len(snapshots) <= 2, "list(limit=2) should return at most 2 items"
204+
205+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
206+
def test_axon_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
207+
"""Verify axon.list() with limit returns at most that many items."""
208+
axons = sdk_client.axon.list(limit=3)
209+
210+
assert isinstance(axons, list)
211+
assert len(axons) <= 3, "list(limit=3) should return at most 3 items"
212+
213+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
214+
def test_scorer_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
215+
"""Verify scorer.list() with limit returns at most that many items."""
216+
scorers = sdk_client.scorer.list(limit=5)
217+
218+
assert isinstance(scorers, list)
219+
assert len(scorers) <= 5, "list(limit=5) should return at most 5 items"
220+
221+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
222+
def test_scenario_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
223+
"""Verify scenario.list() with limit returns at most that many items."""
224+
scenarios = sdk_client.scenario.list(limit=4)
225+
226+
assert isinstance(scenarios, list)
227+
assert len(scenarios) <= 4, "list(limit=4) should return at most 4 items"
228+
229+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
230+
def test_benchmark_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
231+
"""Verify benchmark.list() with limit returns at most that many items."""
232+
benchmarks = sdk_client.benchmark.list(limit=3)
233+
234+
assert isinstance(benchmarks, list)
235+
assert len(benchmarks) <= 3, "list(limit=3) should return at most 3 items"
236+
237+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
238+
def test_network_policy_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
239+
"""Verify network_policy.list() with limit returns at most that many items."""
240+
policies = sdk_client.network_policy.list(limit=5)
241+
242+
assert isinstance(policies, list)
243+
assert len(policies) <= 5, "list(limit=5) should return at most 5 items"
244+
245+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
246+
def test_gateway_config_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
247+
"""Verify gateway_config.list() with limit returns at most that many items."""
248+
configs = sdk_client.gateway_config.list(limit=2)
249+
250+
assert isinstance(configs, list)
251+
assert len(configs) <= 2, "list(limit=2) should return at most 2 items"
252+
253+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
254+
def test_mcp_config_list_respects_limit(self, sdk_client: RunloopSDK) -> None:
255+
"""Verify mcp_config.list() with limit returns at most that many items."""
256+
configs = sdk_client.mcp_config.list(limit=3)
257+
258+
assert isinstance(configs, list)
259+
assert len(configs) <= 3, "list(limit=3) should return at most 3 items"
260+
261+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
262+
def test_secret_list_no_auto_pagination(self, sdk_client: RunloopSDK) -> None:
263+
"""Verify secret.list() returns only one page."""
264+
secrets = sdk_client.secret.list()
265+
266+
assert isinstance(secrets, list)
267+
268+
269+
class TestListPaginationWithData:
270+
"""Test list pagination behavior when data is guaranteed to exist."""
271+
272+
@pytest.mark.asyncio
273+
@pytest.mark.timeout(THIRTY_SECOND_TIMEOUT)
274+
async def test_list_limit_with_created_data(self, async_sdk_client: AsyncRunloopSDK) -> None:
275+
"""Create multiple items and verify list limit works correctly."""
276+
# Create several agents to ensure we have data
277+
created_agents = []
278+
for i in range(5):
279+
agent = await async_sdk_client.agent.create(
280+
name=unique_name(f"sdk-list-test-{i}"),
281+
version="1.0.0",
282+
source=AGENT_SOURCE,
283+
)
284+
created_agents.append(agent)
285+
286+
try:
287+
# Request only 2 items
288+
listed_agents = await async_sdk_client.agent.list(limit=2)
289+
290+
# Should get at most 2, even though 5+ exist
291+
assert len(listed_agents) <= 2, (
292+
f"Expected at most 2 items with limit=2, got {len(listed_agents)}. "
293+
"This indicates auto-pagination is occurring when it shouldn't."
294+
)
295+
296+
# Request 10 items - should get all we created (5) plus any existing ones,
297+
# but should stop at first page (up to 10)
298+
listed_agents = await async_sdk_client.agent.list(limit=10)
299+
assert len(listed_agents) <= 10, (
300+
f"Expected at most 10 items with limit=10, got {len(listed_agents)}. "
301+
"This indicates auto-pagination is occurring when it shouldn't."
302+
)
303+
304+
finally:
305+
# Cleanup is not possible yet as agents don't have delete
306+
pass

0 commit comments

Comments
 (0)