import time import pytest # Import shared fixtures and constants from conftest from conftest import ( CREATE_DELAY_SQLITE, USING_SQLITE, ) from letta.constants import ( LETTA_TOOL_EXECUTION_DIR, ) from letta.schemas.enums import ( SandboxType, ) from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate, SandboxEnvironmentVariableUpdate from letta.schemas.sandbox_config import E2BSandboxConfig, LocalSandboxConfig, SandboxConfigCreate, SandboxConfigUpdate from letta.server.server import SyncServer from letta.settings import tool_settings # ====================================================================================================================== # SandboxConfigManager Tests - Sandbox Configs # ====================================================================================================================== @pytest.mark.asyncio async def test_create_or_update_sandbox_config(server: SyncServer, default_user): sandbox_config_create = SandboxConfigCreate( config=E2BSandboxConfig(), ) created_config = await server.sandbox_config_manager.create_or_update_sandbox_config_async(sandbox_config_create, actor=default_user) # Assertions assert created_config.type == SandboxType.E2B assert created_config.get_e2b_config() == sandbox_config_create.config assert created_config.organization_id == default_user.organization_id @pytest.mark.asyncio async def test_create_local_sandbox_config_defaults(server: SyncServer, default_user): sandbox_config_create = SandboxConfigCreate( config=LocalSandboxConfig(), ) created_config = await server.sandbox_config_manager.create_or_update_sandbox_config_async(sandbox_config_create, actor=default_user) # Assertions assert created_config.type == SandboxType.LOCAL assert created_config.get_local_config() == sandbox_config_create.config assert created_config.get_local_config().sandbox_dir in {LETTA_TOOL_EXECUTION_DIR, tool_settings.tool_exec_dir} assert created_config.organization_id == default_user.organization_id @pytest.mark.asyncio async def test_default_e2b_settings_sandbox_config(server: SyncServer, default_user): created_config = await server.sandbox_config_manager.get_or_create_default_sandbox_config_async( sandbox_type=SandboxType.E2B, actor=default_user ) e2b_config = created_config.get_e2b_config() # Assertions assert e2b_config.timeout == 5 * 60 assert e2b_config.template == tool_settings.e2b_sandbox_template_id @pytest.mark.asyncio async def test_update_existing_sandbox_config(server: SyncServer, sandbox_config_fixture, default_user): update_data = SandboxConfigUpdate(config=E2BSandboxConfig(template="template_2", timeout=120)) updated_config = await server.sandbox_config_manager.update_sandbox_config_async( sandbox_config_fixture.id, update_data, actor=default_user ) # Assertions assert updated_config.config["template"] == "template_2" assert updated_config.config["timeout"] == 120 @pytest.mark.asyncio async def test_delete_sandbox_config(server: SyncServer, sandbox_config_fixture, default_user): deleted_config = await server.sandbox_config_manager.delete_sandbox_config_async(sandbox_config_fixture.id, actor=default_user) # Assertions to verify deletion assert deleted_config.id == sandbox_config_fixture.id # Verify it no longer exists config_list = await server.sandbox_config_manager.list_sandbox_configs_async(actor=default_user) assert sandbox_config_fixture.id not in [config.id for config in config_list] @pytest.mark.asyncio async def test_get_sandbox_config_by_type(server: SyncServer, sandbox_config_fixture, default_user): retrieved_config = await server.sandbox_config_manager.get_sandbox_config_by_type_async(sandbox_config_fixture.type, actor=default_user) # Assertions to verify correct retrieval assert retrieved_config.id == sandbox_config_fixture.id assert retrieved_config.type == sandbox_config_fixture.type @pytest.mark.asyncio async def test_list_sandbox_configs(server: SyncServer, default_user): # Creating multiple sandbox configs config_e2b_create = SandboxConfigCreate( config=E2BSandboxConfig(), ) config_local_create = SandboxConfigCreate( config=LocalSandboxConfig(sandbox_dir=""), ) config_e2b = await server.sandbox_config_manager.create_or_update_sandbox_config_async(config_e2b_create, actor=default_user) if USING_SQLITE: time.sleep(CREATE_DELAY_SQLITE) config_local = await server.sandbox_config_manager.create_or_update_sandbox_config_async(config_local_create, actor=default_user) # List configs without pagination configs = await server.sandbox_config_manager.list_sandbox_configs_async(actor=default_user) assert len(configs) >= 2 # List configs with pagination paginated_configs = await server.sandbox_config_manager.list_sandbox_configs_async(actor=default_user, limit=1) assert len(paginated_configs) == 1 next_page = await server.sandbox_config_manager.list_sandbox_configs_async(actor=default_user, after=paginated_configs[-1].id, limit=1) assert len(next_page) == 1 assert next_page[0].id != paginated_configs[0].id # List configs using sandbox_type filter configs = await server.sandbox_config_manager.list_sandbox_configs_async(actor=default_user, sandbox_type=SandboxType.E2B) assert len(configs) == 1 assert configs[0].id == config_e2b.id configs = await server.sandbox_config_manager.list_sandbox_configs_async(actor=default_user, sandbox_type=SandboxType.LOCAL) assert len(configs) == 1 assert configs[0].id == config_local.id # ====================================================================================================================== # SandboxConfigManager Tests - Environment Variables # ====================================================================================================================== @pytest.mark.asyncio async def test_create_sandbox_env_var(server: SyncServer, sandbox_config_fixture, default_user): env_var_create = SandboxEnvironmentVariableCreate(key="TEST_VAR", value="test_value", description="A test environment variable.") created_env_var = await server.sandbox_config_manager.create_sandbox_env_var_async( env_var_create, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) # Assertions assert created_env_var.key == env_var_create.key assert created_env_var.value == env_var_create.value assert created_env_var.organization_id == default_user.organization_id @pytest.mark.asyncio async def test_update_sandbox_env_var(server: SyncServer, sandbox_env_var_fixture, default_user): update_data = SandboxEnvironmentVariableUpdate(value="updated_value") updated_env_var = await server.sandbox_config_manager.update_sandbox_env_var_async( sandbox_env_var_fixture.id, update_data, actor=default_user ) # Assertions assert updated_env_var.value == "updated_value" assert updated_env_var.id == sandbox_env_var_fixture.id @pytest.mark.asyncio async def test_delete_sandbox_env_var(server: SyncServer, sandbox_config_fixture, sandbox_env_var_fixture, default_user): deleted_env_var = await server.sandbox_config_manager.delete_sandbox_env_var_async(sandbox_env_var_fixture.id, actor=default_user) # Assertions to verify deletion assert deleted_env_var.id == sandbox_env_var_fixture.id # Verify it no longer exists env_vars = await server.sandbox_config_manager.list_sandbox_env_vars_async( sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) assert sandbox_env_var_fixture.id not in [env_var.id for env_var in env_vars] @pytest.mark.asyncio async def test_list_sandbox_env_vars(server: SyncServer, sandbox_config_fixture, default_user): # Creating multiple environment variables env_var_create_a = SandboxEnvironmentVariableCreate(key="VAR1", value="value1") env_var_create_b = SandboxEnvironmentVariableCreate(key="VAR2", value="value2") await server.sandbox_config_manager.create_sandbox_env_var_async( env_var_create_a, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) if USING_SQLITE: time.sleep(CREATE_DELAY_SQLITE) await server.sandbox_config_manager.create_sandbox_env_var_async( env_var_create_b, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) # List env vars without pagination env_vars = await server.sandbox_config_manager.list_sandbox_env_vars_async( sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) assert len(env_vars) >= 2 # List env vars with pagination paginated_env_vars = await server.sandbox_config_manager.list_sandbox_env_vars_async( sandbox_config_id=sandbox_config_fixture.id, actor=default_user, limit=1 ) assert len(paginated_env_vars) == 1 next_page = await server.sandbox_config_manager.list_sandbox_env_vars_async( sandbox_config_id=sandbox_config_fixture.id, actor=default_user, after=paginated_env_vars[-1].id, limit=1 ) assert len(next_page) == 1 assert next_page[0].id != paginated_env_vars[0].id @pytest.mark.asyncio async def test_get_sandbox_env_var_by_key(server: SyncServer, sandbox_env_var_fixture, default_user): retrieved_env_var = await server.sandbox_config_manager.get_sandbox_env_var_by_key_and_sandbox_config_id_async( sandbox_env_var_fixture.key, sandbox_env_var_fixture.sandbox_config_id, actor=default_user ) # Assertions to verify correct retrieval assert retrieved_env_var.id == sandbox_env_var_fixture.id @pytest.mark.asyncio async def test_gather_env_vars_layering(server: SyncServer, sandbox_config_fixture, default_user): """Test that _gather_env_vars properly layers env vars with correct priority. Priority order (later overrides earlier): 1. Global sandbox env vars from DB (always included) 2. Provided sandbox env vars (agent-scoped, override global on key collision) 3. Agent state env vars 4. Additional runtime env vars (highest priority) """ from unittest.mock import MagicMock from letta.services.tool_sandbox.local_sandbox import AsyncToolSandboxLocal # Create global sandbox env vars in the database global_var1 = SandboxEnvironmentVariableCreate(key="GLOBAL_ONLY", value="global_value") global_var2 = SandboxEnvironmentVariableCreate(key="OVERRIDE_BY_PROVIDED", value="global_will_be_overridden") global_var3 = SandboxEnvironmentVariableCreate(key="OVERRIDE_BY_AGENT", value="global_will_be_overridden_by_agent") global_var4 = SandboxEnvironmentVariableCreate(key="OVERRIDE_BY_ADDITIONAL", value="global_will_be_overridden_by_additional") await server.sandbox_config_manager.create_sandbox_env_var_async( global_var1, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) await server.sandbox_config_manager.create_sandbox_env_var_async( global_var2, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) await server.sandbox_config_manager.create_sandbox_env_var_async( global_var3, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) await server.sandbox_config_manager.create_sandbox_env_var_async( global_var4, sandbox_config_id=sandbox_config_fixture.id, actor=default_user ) # Define provided sandbox env vars (agent-scoped) provided_env_vars = { "OVERRIDE_BY_PROVIDED": "provided_value", "PROVIDED_ONLY": "provided_only_value", } # Create a mock agent state with secrets mock_agent_state = MagicMock() mock_agent_state.get_agent_env_vars_as_dict.return_value = { "OVERRIDE_BY_AGENT": "agent_value", "AGENT_ONLY": "agent_only_value", } # Define additional runtime env vars additional_env_vars = { "OVERRIDE_BY_ADDITIONAL": "additional_value", "ADDITIONAL_ONLY": "additional_only_value", } # Create a minimal sandbox instance to test _gather_env_vars sandbox = AsyncToolSandboxLocal( tool_name="test_tool", args={}, user=default_user, tool_id="test-tool-id", sandbox_env_vars=provided_env_vars, ) # Call _gather_env_vars result = await sandbox._gather_env_vars( agent_state=mock_agent_state, additional_env_vars=additional_env_vars, sbx_id=sandbox_config_fixture.id, is_local=False, # Use False to avoid copying os.environ ) # Verify layering: # 1. Global vars included assert result["GLOBAL_ONLY"] == "global_value" # 2. Provided vars override global assert result["OVERRIDE_BY_PROVIDED"] == "provided_value" assert result["PROVIDED_ONLY"] == "provided_only_value" # 3. Agent vars override provided/global assert result["OVERRIDE_BY_AGENT"] == "agent_value" assert result["AGENT_ONLY"] == "agent_only_value" # 4. Additional vars have highest priority assert result["OVERRIDE_BY_ADDITIONAL"] == "additional_value" assert result["ADDITIONAL_ONLY"] == "additional_only_value" # Verify LETTA IDs are injected assert result["LETTA_TOOL_ID"] == "test-tool-id"