Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,17 @@ jobs:
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
timeout-minutes: 15

# Without this the matrix is decorative. `make dev` runs a bare `uv sync`,
# and uv resolves its interpreter from .python-version (pinned to 3.11)
# rather than from whatever actions/setup-python just installed -- so every
# leg built a 3.11 venv and all four ran the same interpreter. UV_PYTHON
# takes precedence over the pin file, which restores real version coverage.
#
# Set at job level, not on the install step: `make ci-quality-github` runs
# `uv run pytest`, which resolves the interpreter again.
env:
UV_PYTHON: ${{ matrix.python-version }}

steps:
- name: Checkout code
uses: actions/checkout@v4
Expand Down
32 changes: 29 additions & 3 deletions tests/unit/cli/commands/test_run_server_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,12 +336,34 @@ async def test_allows_populated_fields(self):
assert result == {"echo": "hello"}
func.assert_called_once_with(msg="hello")

# These two wrap the mock in a real async def instead of passing the mock
# straight to call_with_body.
#
# call_with_body -> _map_body_to_params calls inspect.signature(func), and
# signature() of a Mock is version-dependent: on 3.10 it raises
# `TypeError: 'Mock' object is not subscriptable` (Mock auto-creates a
# __signature__ child, which inspect then tries to use), while on 3.11+ it
# reports (*args, **kwargs). Passing spec= does not help -- the failure is
# in signature(), not in spec resolution.
#
# call_with_body catches Exception and converts it into a 500 JSONResponse,
# so on 3.10 these failed as `assert <JSONResponse object> == {'ok': True}`,
# with the real TypeError visible only inside the response body.
#
# The wrapper declares (*args, **kwargs), the same signature 3.11+ inferred
# from the Mock, so both branches of _map_body_to_params behave exactly as
# before -- and delegating to the mock keeps the call assertions.

@pytest.mark.asyncio
async def test_allows_plain_dict_body(self):
"""Non-empty plain dict passes through unchanged (no model_fields_set)."""
body = {"key": "value"}

func = AsyncMock(return_value={"ok": True})
mock = AsyncMock(return_value={"ok": True})

async def func(*args, **kwargs):
return await mock(*args, **kwargs)

result = await call_with_body(func, body)

assert result == {"ok": True}
Expand All @@ -354,8 +376,12 @@ async def test_allows_empty_plain_dict_body(self):
"""
body = {}

func = AsyncMock(return_value={"ok": True})
mock = AsyncMock(return_value={"ok": True})

async def func(*args, **kwargs):
return await mock(*args, **kwargs)

result = await call_with_body(func, body)

assert result == {"ok": True}
func.assert_called_once_with()
mock.assert_called_once_with()
19 changes: 18 additions & 1 deletion tests/unit/core/resources/test_resource_manager_extended.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
)


class _LegacyResource:
"""Minimal picklable stand-in for a resource in the pre-1.12 state file.

Module-level, not defined inside a test: cloudpickle serialises a class
defined in a function body by value, which is the thing being avoided here.
"""

def __init__(self, config_hash: str):
self.config_hash = config_hash


@pytest.fixture(autouse=True)
def reset_manager(isolate_resource_state_file, reset_singletons):
"""Reset singleton and state file between tests."""
Expand Down Expand Up @@ -56,7 +67,13 @@ def test_loads_legacy_dict_format(self, tmp_path):
state_file = tmp_path / ".runpod" / "resources.pkl"
state_file.parent.mkdir(parents=True)

resources = {"key1": MagicMock(config_hash="hash1")}
# A plain object rather than MagicMock: whether cloudpickle can pickle a
# Mock is version-dependent, and on 3.10 this failed with
# "Could not pickle object as excessively deep recursion required".
# Only .config_hash is read here (via _refresh_config_hashes), and the
# absence of get_resource_key is what keeps the legacy key un-migrated,
# which is exactly what this test is asserting.
resources = {"key1": _LegacyResource("hash1")}
with open(state_file, "wb") as f:
cloudpickle.dump(resources, f)

Expand Down
Loading