@@ -1693,3 +1693,181 @@ async def test_update_includes_env_when_changed(self):
16931693 assert mock_client .update_template .called
16941694 template_payload = mock_client .update_template .call_args .args [0 ]
16951695 assert "env" in template_payload
1696+
1697+ @pytest .mark .asyncio
1698+ async def test_update_injects_runtime_vars_when_env_changed (self ):
1699+ """update() injects RUNPOD_API_KEY into template.env when env changed.
1700+
1701+ Without this, runtime-injected vars (set during _do_deploy) would be
1702+ lost when update() overwrites the template env.
1703+ """
1704+ old_resource = ServerlessEndpoint (
1705+ name = "update-inject-test" ,
1706+ imageName = "test:latest" ,
1707+ env = {"LOG_LEVEL" : "INFO" },
1708+ flashboot = False ,
1709+ )
1710+ old_resource .id = "ep-inject"
1711+ old_resource .templateId = "tpl-inject"
1712+
1713+ new_resource = ServerlessEndpoint (
1714+ name = "update-inject-test" ,
1715+ imageName = "test:latest" ,
1716+ env = {"LOG_LEVEL" : "DEBUG" },
1717+ flashboot = False ,
1718+ )
1719+
1720+ mock_client = AsyncMock ()
1721+ mock_client .save_endpoint = AsyncMock (
1722+ return_value = {
1723+ "id" : "ep-inject" ,
1724+ "name" : "update-inject-test" ,
1725+ "templateId" : "tpl-inject" ,
1726+ "gpuIds" : "AMPERE_48" ,
1727+ "allowedCudaVersions" : "" ,
1728+ }
1729+ )
1730+ mock_client .update_template = AsyncMock (return_value = {})
1731+
1732+ with patch (
1733+ "runpod_flash.core.resources.serverless.RunpodGraphQLClient"
1734+ ) as mock_client_class :
1735+ mock_client_class .return_value .__aenter__ .return_value = mock_client
1736+ mock_client_class .return_value .__aexit__ .return_value = None
1737+
1738+ with patch .object (
1739+ ServerlessResource ,
1740+ "_ensure_network_volume_deployed" ,
1741+ new = AsyncMock (),
1742+ ):
1743+ with patch .object (
1744+ ServerlessResource ,
1745+ "_check_makes_remote_calls" ,
1746+ return_value = True ,
1747+ ):
1748+ with patch .dict (os .environ , {"RUNPOD_API_KEY" : "inject-key" }):
1749+ await old_resource .update (new_resource )
1750+
1751+ template_payload = mock_client .update_template .call_args .args [0 ]
1752+ env_entries = template_payload .get ("env" , [])
1753+ api_key_entries = [e for e in env_entries if e ["key" ] == "RUNPOD_API_KEY" ]
1754+ assert len (api_key_entries ) == 1
1755+ assert api_key_entries [0 ]["value" ] == "inject-key"
1756+
1757+ @pytest .mark .asyncio
1758+ async def test_update_skips_runtime_injection_when_env_unchanged (self ):
1759+ """update() does not inject runtime vars when env is unchanged.
1760+
1761+ When skip_env=True, the template env payload is omitted entirely,
1762+ so runtime vars already on the platform are preserved as-is.
1763+ """
1764+ env = {"LOG_LEVEL" : "INFO" }
1765+ old_resource = ServerlessEndpoint (
1766+ name = "update-no-inject" ,
1767+ imageName = "test:latest" ,
1768+ env = env ,
1769+ flashboot = False ,
1770+ )
1771+ old_resource .id = "ep-no-inject"
1772+ old_resource .templateId = "tpl-no-inject"
1773+
1774+ new_resource = ServerlessEndpoint (
1775+ name = "update-no-inject" ,
1776+ imageName = "test:latest" ,
1777+ env = env ,
1778+ flashboot = False ,
1779+ )
1780+
1781+ mock_client = AsyncMock ()
1782+ mock_client .save_endpoint = AsyncMock (
1783+ return_value = {
1784+ "id" : "ep-no-inject" ,
1785+ "name" : "update-no-inject" ,
1786+ "templateId" : "tpl-no-inject" ,
1787+ "gpuIds" : "AMPERE_48" ,
1788+ "allowedCudaVersions" : "" ,
1789+ }
1790+ )
1791+ mock_client .update_template = AsyncMock (return_value = {})
1792+
1793+ with patch (
1794+ "runpod_flash.core.resources.serverless.RunpodGraphQLClient"
1795+ ) as mock_client_class :
1796+ mock_client_class .return_value .__aenter__ .return_value = mock_client
1797+ mock_client_class .return_value .__aexit__ .return_value = None
1798+
1799+ with patch .object (
1800+ ServerlessResource ,
1801+ "_ensure_network_volume_deployed" ,
1802+ new = AsyncMock (),
1803+ ):
1804+ with patch .object (
1805+ ServerlessResource ,
1806+ "_check_makes_remote_calls" ,
1807+ return_value = True ,
1808+ ):
1809+ with patch .dict (os .environ , {"RUNPOD_API_KEY" : "inject-key" }):
1810+ await old_resource .update (new_resource )
1811+
1812+ # env should be omitted from template payload (skip_env=True)
1813+ template_payload = mock_client .update_template .call_args .args [0 ]
1814+ assert "env" not in template_payload
1815+
1816+ @pytest .mark .asyncio
1817+ async def test_update_includes_env_for_explicit_template_env (self ):
1818+ """update() sends env when caller provides explicit template.env with empty env.
1819+
1820+ Even if self.env == new_config.env (both empty), explicit template.env
1821+ entries must not be silently dropped.
1822+ """
1823+ old_resource = ServerlessEndpoint (
1824+ name = "update-tpl-env" ,
1825+ imageName = "test:latest" ,
1826+ env = {},
1827+ flashboot = False ,
1828+ )
1829+ old_resource .id = "ep-tpl-env"
1830+ old_resource .templateId = "tpl-tpl-env"
1831+
1832+ new_resource = ServerlessEndpoint (
1833+ name = "update-tpl-env" ,
1834+ imageName = "test:latest" ,
1835+ env = {},
1836+ flashboot = False ,
1837+ template = PodTemplate (
1838+ name = "explicit-tpl" ,
1839+ imageName = "test:latest" ,
1840+ env = [KeyValuePair (key = "EXPLICIT_VAR" , value = "explicit_val" )],
1841+ ),
1842+ )
1843+
1844+ mock_client = AsyncMock ()
1845+ mock_client .save_endpoint = AsyncMock (
1846+ return_value = {
1847+ "id" : "ep-tpl-env" ,
1848+ "name" : "update-tpl-env" ,
1849+ "templateId" : "tpl-tpl-env" ,
1850+ "gpuIds" : "AMPERE_48" ,
1851+ "allowedCudaVersions" : "" ,
1852+ }
1853+ )
1854+ mock_client .update_template = AsyncMock (return_value = {})
1855+
1856+ with patch (
1857+ "runpod_flash.core.resources.serverless.RunpodGraphQLClient"
1858+ ) as mock_client_class :
1859+ mock_client_class .return_value .__aenter__ .return_value = mock_client
1860+ mock_client_class .return_value .__aexit__ .return_value = None
1861+
1862+ with patch .object (
1863+ ServerlessResource ,
1864+ "_ensure_network_volume_deployed" ,
1865+ new = AsyncMock (),
1866+ ):
1867+ await old_resource .update (new_resource )
1868+
1869+ template_payload = mock_client .update_template .call_args .args [0 ]
1870+ assert "env" in template_payload
1871+ env_entries = template_payload ["env" ]
1872+ explicit = [e for e in env_entries if e ["key" ] == "EXPLICIT_VAR" ]
1873+ assert len (explicit ) == 1
0 commit comments