From 3b59f1a117dbdfec6f31079da25a37768ec6d424 Mon Sep 17 00:00:00 2001 From: jhcipar Date: Wed, 25 Mar 2026 17:44:55 -0400 Subject: [PATCH 1/3] fix: network volume size bounds, forbid extra args --- .../core/resources/network_volume.py | 16 +++++++++++- tests/unit/resources/test_network_volume.py | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/runpod_flash/core/resources/network_volume.py b/src/runpod_flash/core/resources/network_volume.py index a0f1186c..d7b8058d 100644 --- a/src/runpod_flash/core/resources/network_volume.py +++ b/src/runpod_flash/core/resources/network_volume.py @@ -4,7 +4,9 @@ from typing import Optional, Dict, Any from pydantic import ( + ConfigDict, Field, + field_validator, field_serializer, model_validator, ) @@ -80,13 +82,25 @@ class NetworkVolume(DeployableResource): "name", } + model_config = ConfigDict(extra="forbid") + # public alias -- users pass datacenter=, which syncs to dataCenterId for the API datacenter: Optional[DataCenter] = Field(default=None, exclude=True) dataCenterId: DataCenter = Field(default=DataCenter.EU_RO_1) id: Optional[str] = Field(default=None) name: Optional[str] = None - size: Optional[int] = Field(default=100, gt=0) # Size in GB + size: Optional[int] = Field(default=100, gt=0, le=4096) # Size in GB + + @field_validator("name") + @classmethod + def validate_name_not_empty(cls, value: Optional[str]) -> Optional[str]: + """Reject empty/whitespace-only volume names.""" + if value is None: + return value + if not value.strip(): + raise ValueError("name must not be empty or whitespace-only") + return value @model_validator(mode="before") @classmethod diff --git a/tests/unit/resources/test_network_volume.py b/tests/unit/resources/test_network_volume.py index 49aace8b..92d7c286 100644 --- a/tests/unit/resources/test_network_volume.py +++ b/tests/unit/resources/test_network_volume.py @@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, patch, MagicMock import pytest +from pydantic import ValidationError from runpod_flash.core.resources.network_volume import NetworkVolume, DataCenter @@ -166,6 +167,31 @@ def test_resource_id_based_on_name_and_datacenter(self): assert volume1.resource_id == volume2.resource_id # Same name + datacenter assert volume1.resource_id != volume3.resource_id # Different name + def test_empty_name_rejected(self): + """Reject empty names at model construction time.""" + with pytest.raises(ValidationError, match="name must not be empty"): + NetworkVolume(name="") + + def test_whitespace_name_rejected(self): + """Reject whitespace-only names at model construction time.""" + with pytest.raises(ValidationError, match="name must not be empty"): + NetworkVolume(name=" ") + + def test_max_size_is_allowed(self): + """Max supported size (4TB) is accepted.""" + volume = NetworkVolume(name="large-vol", size=4096) + assert volume.size == 4096 + + def test_size_above_max_rejected(self): + """Size above 4TB should fail validation.""" + with pytest.raises(ValidationError, match="less than or equal to 4096"): + NetworkVolume(name="too-large", size=4097) + + def test_unknown_field_rejected(self): + """Unknown fields should raise validation errors.""" + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + NetworkVolume(name="data", sizee=500) + @pytest.mark.asyncio async def test_deploy_uses_resource_manager_to_register(self, sample_volume_data): """deploy() should go through the ResourceManager for persistence.""" From f035a07e18f766e9ff154bda14384bf1bcb8569c Mon Sep 17 00:00:00 2001 From: jhcipar Date: Wed, 25 Mar 2026 18:02:37 -0400 Subject: [PATCH 2/3] chore: update defaults and min network volume size --- src/runpod_flash/core/resources/network_volume.py | 2 +- tests/unit/resources/test_network_volume.py | 5 +++++ tests/unit/test_p2_gaps.py | 12 ++++++------ tests/unit/test_p2_remaining_gaps.py | 6 +++--- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/runpod_flash/core/resources/network_volume.py b/src/runpod_flash/core/resources/network_volume.py index d7b8058d..df0d888d 100644 --- a/src/runpod_flash/core/resources/network_volume.py +++ b/src/runpod_flash/core/resources/network_volume.py @@ -90,7 +90,7 @@ class NetworkVolume(DeployableResource): id: Optional[str] = Field(default=None) name: Optional[str] = None - size: Optional[int] = Field(default=100, gt=0, le=4096) # Size in GB + size: Optional[int] = Field(default=20, ge=10, le=4096) # Size in GB @field_validator("name") @classmethod diff --git a/tests/unit/resources/test_network_volume.py b/tests/unit/resources/test_network_volume.py index 92d7c286..21e76021 100644 --- a/tests/unit/resources/test_network_volume.py +++ b/tests/unit/resources/test_network_volume.py @@ -187,6 +187,11 @@ def test_size_above_max_rejected(self): with pytest.raises(ValidationError, match="less than or equal to 4096"): NetworkVolume(name="too-large", size=4097) + def test_size_below_min_rejected(self): + """Size below 10GB should fail validation.""" + with pytest.raises(ValidationError, match="greater than or equal to 10"): + NetworkVolume(name="too-small", size=5) + def test_unknown_field_rejected(self): """Unknown fields should raise validation errors.""" with pytest.raises(ValidationError, match="Extra inputs are not permitted"): diff --git a/tests/unit/test_p2_gaps.py b/tests/unit/test_p2_gaps.py index 9fa42501..ba804701 100644 --- a/tests/unit/test_p2_gaps.py +++ b/tests/unit/test_p2_gaps.py @@ -106,7 +106,7 @@ async def my_func(x): # --------------------------------------------------------------------------- -# VOL-005: Volume size=0 raises validation error +# VOL-005: Volume size below minimum raises validation error # VOL-006: Volume name empty # VOL-007: Undeploy raises NotImplementedError # --------------------------------------------------------------------------- @@ -114,17 +114,17 @@ class TestNetworkVolumeValidation: """Network volume validation edge cases.""" def test_volume_size_zero_raises(self): - """VOL-005: size=0 raises validation error (gt=0 constraint).""" + """VOL-005: size=0 raises validation error (min 10GB).""" from runpod_flash.core.resources.network_volume import NetworkVolume - with pytest.raises(ValidationError, match="greater than 0"): + with pytest.raises(ValidationError, match="greater than or equal to 10"): NetworkVolume(name="test-vol", size=0) def test_volume_size_negative_raises(self): """VOL-005: Negative size also rejected.""" from runpod_flash.core.resources.network_volume import NetworkVolume - with pytest.raises(ValidationError, match="greater than 0"): + with pytest.raises(ValidationError, match="greater than or equal to 10"): NetworkVolume(name="test-vol", size=-10) @pytest.mark.asyncio @@ -138,11 +138,11 @@ async def test_volume_undeploy_raises_not_implemented(self): await vol.undeploy() def test_volume_default_size(self): - """Default volume size is 100GB.""" + """Default volume size is 20GB.""" from runpod_flash.core.resources.network_volume import NetworkVolume vol = NetworkVolume(name="test-vol") - assert vol.size == 100 + assert vol.size == 20 # --------------------------------------------------------------------------- diff --git a/tests/unit/test_p2_remaining_gaps.py b/tests/unit/test_p2_remaining_gaps.py index 5cde7ba4..e7476fa4 100644 --- a/tests/unit/test_p2_remaining_gaps.py +++ b/tests/unit/test_p2_remaining_gaps.py @@ -4,7 +4,7 @@ CLI-RUN-018 – watchfiles fallback stub raises ModuleNotFoundError REM-CLS-013 – extract_class_code_simple fallback when inspect.getsource fails RES-LS-008 – ServerlessResource.env default populated from .env file - VOL-006 – NetworkVolume with empty name still constructs (no validator guards it) + VOL-006 – NetworkVolume with empty/whitespace name is rejected by validator SCAN-016 – RuntimeScanner handles @remote on nested class (class in function) SCAN-017 – RuntimeScanner handles conditional @remote gracefully STUB-STACK-004 – detect_remote_dependencies terminates on circular dependency graph @@ -217,7 +217,7 @@ def test_network_volume_with_empty_name_and_no_id_raises(self): from runpod_flash.core.resources.network_volume import NetworkVolume with pytest.raises( - ValidationError, match="either 'name' or 'id' must be provided" + ValidationError, match="name must not be empty or whitespace-only" ): NetworkVolume(name="") @@ -238,7 +238,7 @@ def test_network_volume_non_empty_name_works(self): assert vol.size == 50 def test_network_volume_size_zero_raises(self): - """VOL-006: size=0 violates the gt=0 constraint and raises ValidationError.""" + """VOL-006: size=0 violates the min 10GB constraint and raises ValidationError.""" from pydantic import ValidationError from runpod_flash.core.resources.network_volume import NetworkVolume From f00e70d8e16c6b7b8b7a479a81b41e2c8008d554 Mon Sep 17 00:00:00 2001 From: jhcipar Date: Wed, 25 Mar 2026 18:14:51 -0400 Subject: [PATCH 3/3] chore: update test_manifest --- tests/unit/cli/commands/build_utils/test_manifest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/cli/commands/build_utils/test_manifest.py b/tests/unit/cli/commands/build_utils/test_manifest.py index 6ab1280f..9d60cb69 100644 --- a/tests/unit/cli/commands/build_utils/test_manifest.py +++ b/tests/unit/cli/commands/build_utils/test_manifest.py @@ -617,7 +617,7 @@ def test_extract_deployment_config_network_volume_minimal(): assert config["networkVolume"]["name"] == "minimal-vol" # Default size and dataCenterId should still be present - assert config["networkVolume"]["size"] == 100 + assert config["networkVolume"]["size"] == 20 assert config["networkVolume"]["dataCenterId"] == "EU-RO-1"