test_config.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. import os
  2. import pytest
  3. from opendevin.core.config import (
  4. AgentConfig,
  5. AppConfig,
  6. LLMConfig,
  7. finalize_config,
  8. load_from_env,
  9. load_from_toml,
  10. )
  11. @pytest.fixture
  12. def setup_env():
  13. # Create old-style and new-style TOML files
  14. with open('old_style_config.toml', 'w') as f:
  15. f.write('[default]\nLLM_MODEL="GPT-4"\n')
  16. with open('new_style_config.toml', 'w') as f:
  17. f.write('[app]\nLLM_MODEL="GPT-3"\n')
  18. yield
  19. # Cleanup TOML files after the test
  20. os.remove('old_style_config.toml')
  21. os.remove('new_style_config.toml')
  22. @pytest.fixture
  23. def temp_toml_file(tmp_path):
  24. # Fixture to create a temporary directory and TOML file for testing
  25. tmp_toml_file = os.path.join(tmp_path, 'config.toml')
  26. yield tmp_toml_file
  27. @pytest.fixture
  28. def default_config(monkeypatch):
  29. # Fixture to provide a default AppConfig instance
  30. AppConfig.reset()
  31. yield AppConfig()
  32. def test_compat_env_to_config(monkeypatch, setup_env):
  33. # Use `monkeypatch` to set environment variables for this specific test
  34. monkeypatch.setenv('WORKSPACE_BASE', '/repos/opendevin/workspace')
  35. monkeypatch.setenv('LLM_API_KEY', 'sk-proj-rgMV0...')
  36. monkeypatch.setenv('LLM_MODEL', 'gpt-3.5-turbo')
  37. monkeypatch.setenv('AGENT_MEMORY_MAX_THREADS', '4')
  38. monkeypatch.setenv('AGENT_MEMORY_ENABLED', 'True')
  39. monkeypatch.setenv('AGENT', 'CodeActAgent')
  40. config = AppConfig()
  41. load_from_env(config, os.environ)
  42. assert config.workspace_base == '/repos/opendevin/workspace'
  43. assert isinstance(config.llm, LLMConfig)
  44. assert config.llm.api_key == 'sk-proj-rgMV0...'
  45. assert config.llm.model == 'gpt-3.5-turbo'
  46. assert isinstance(config.agent, AgentConfig)
  47. assert isinstance(config.agent.memory_max_threads, int)
  48. assert config.agent.memory_max_threads == 4
  49. def test_load_from_old_style_env(monkeypatch, default_config):
  50. # Test loading configuration from old-style environment variables using monkeypatch
  51. monkeypatch.setenv('LLM_API_KEY', 'test-api-key')
  52. monkeypatch.setenv('AGENT_MEMORY_ENABLED', 'True')
  53. monkeypatch.setenv('AGENT_NAME', 'PlannerAgent')
  54. monkeypatch.setenv('WORKSPACE_BASE', '/opt/files/workspace')
  55. load_from_env(default_config, os.environ)
  56. assert default_config.llm.api_key == 'test-api-key'
  57. assert default_config.agent.memory_enabled is True
  58. assert default_config.agent.name == 'PlannerAgent'
  59. assert default_config.workspace_base == '/opt/files/workspace'
  60. def test_load_from_new_style_toml(default_config, temp_toml_file):
  61. # Test loading configuration from a new-style TOML file
  62. with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
  63. toml_file.write("""
  64. [llm]
  65. model = "test-model"
  66. api_key = "toml-api-key"
  67. [agent]
  68. name = "TestAgent"
  69. memory_enabled = true
  70. [core]
  71. workspace_base = "/opt/files2/workspace"
  72. """)
  73. load_from_toml(default_config, temp_toml_file)
  74. assert default_config.llm.model == 'test-model'
  75. assert default_config.llm.api_key == 'toml-api-key'
  76. assert default_config.agent.name == 'TestAgent'
  77. assert default_config.agent.memory_enabled is True
  78. assert default_config.workspace_base == '/opt/files2/workspace'
  79. def test_env_overrides_toml(monkeypatch, default_config, temp_toml_file):
  80. # Test that environment variables override TOML values using monkeypatch
  81. with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
  82. toml_file.write("""
  83. [llm]
  84. model = "test-model"
  85. api_key = "toml-api-key"
  86. [core]
  87. workspace_base = "/opt/files3/workspace"
  88. sandbox_type = "local"
  89. disable_color = true
  90. """)
  91. monkeypatch.setenv('LLM_API_KEY', 'env-api-key')
  92. monkeypatch.setenv('WORKSPACE_BASE', '/opt/files4/workspace')
  93. monkeypatch.setenv('SANDBOX_TYPE', 'ssh')
  94. load_from_toml(default_config, temp_toml_file)
  95. load_from_env(default_config, os.environ)
  96. assert os.environ.get('LLM_MODEL') is None
  97. assert default_config.llm.model == 'test-model'
  98. assert default_config.llm.api_key == 'env-api-key'
  99. assert default_config.workspace_base == '/opt/files4/workspace'
  100. assert default_config.sandbox_type == 'ssh'
  101. assert default_config.disable_color is True
  102. def test_defaults_dict_after_updates(default_config):
  103. # Test that `defaults_dict` retains initial values after updates.
  104. initial_defaults = default_config.defaults_dict
  105. updated_config = AppConfig()
  106. updated_config.llm.api_key = 'updated-api-key'
  107. updated_config.agent.name = 'MonologueAgent'
  108. defaults_after_updates = updated_config.defaults_dict
  109. assert defaults_after_updates['llm']['api_key']['default'] is None
  110. assert defaults_after_updates['agent']['name']['default'] == 'CodeActAgent'
  111. assert defaults_after_updates == initial_defaults
  112. AppConfig.reset()
  113. def test_invalid_toml_format(monkeypatch, temp_toml_file, default_config):
  114. # Invalid TOML format doesn't break the configuration
  115. monkeypatch.setenv('LLM_MODEL', 'gpt-5-turbo-1106')
  116. monkeypatch.delenv('LLM_API_KEY', raising=False)
  117. with open(temp_toml_file, 'w', encoding='utf-8') as toml_file:
  118. toml_file.write('INVALID TOML CONTENT')
  119. load_from_toml(default_config)
  120. load_from_env(default_config, os.environ)
  121. assert default_config.llm.model == 'gpt-5-turbo-1106'
  122. assert default_config.llm.custom_llm_provider is None
  123. assert default_config.github_token is None
  124. assert default_config.llm.api_key is None
  125. def test_finalize_config(default_config):
  126. # Test finalize config
  127. default_config.sandbox_type = 'local'
  128. finalize_config(default_config)
  129. assert (
  130. default_config.workspace_mount_path_in_sandbox
  131. == default_config.workspace_mount_path
  132. )
  133. # tests for workspace, mount path, path in sandbox, cache dir
  134. def test_workspace_mount_path_default(default_config):
  135. assert default_config.workspace_mount_path is None
  136. finalize_config(default_config)
  137. assert default_config.workspace_mount_path == os.path.abspath(
  138. default_config.workspace_base
  139. )
  140. def test_workspace_mount_path_in_sandbox_local(default_config):
  141. assert default_config.workspace_mount_path_in_sandbox == '/workspace'
  142. default_config.sandbox_type = 'local'
  143. finalize_config(default_config)
  144. assert (
  145. default_config.workspace_mount_path_in_sandbox
  146. == default_config.workspace_mount_path
  147. )
  148. def test_workspace_mount_rewrite(default_config, monkeypatch):
  149. default_config.workspace_base = '/home/user/project'
  150. default_config.workspace_mount_rewrite = '/home/user:/sandbox'
  151. monkeypatch.setattr('os.getcwd', lambda: '/current/working/directory')
  152. finalize_config(default_config)
  153. assert default_config.workspace_mount_path == '/sandbox/project'
  154. def test_embedding_base_url_default(default_config):
  155. default_config.llm.base_url = 'https://api.exampleapi.com'
  156. finalize_config(default_config)
  157. assert default_config.llm.embedding_base_url == 'https://api.exampleapi.com'
  158. def test_cache_dir_creation(default_config, tmpdir):
  159. default_config.cache_dir = str(tmpdir.join('test_cache'))
  160. finalize_config(default_config)
  161. assert os.path.exists(default_config.cache_dir)
  162. def test_api_keys_repr_str():
  163. # Test LLMConfig
  164. llm_config = LLMConfig(
  165. api_key='my_api_key',
  166. aws_access_key_id='my_access_key',
  167. aws_secret_access_key='my_secret_key',
  168. )
  169. assert "api_key='******'" in repr(llm_config)
  170. assert "aws_access_key_id='******'" in repr(llm_config)
  171. assert "aws_secret_access_key='******'" in repr(llm_config)
  172. assert "api_key='******'" in str(llm_config)
  173. assert "aws_access_key_id='******'" in str(llm_config)
  174. assert "aws_secret_access_key='******'" in str(llm_config)
  175. # Check that no other attrs in LLMConfig have 'key' or 'token' in their name
  176. # This will fail when new attrs are added, and attract attention
  177. known_key_token_attrs_llm = [
  178. 'api_key',
  179. 'aws_access_key_id',
  180. 'aws_secret_access_key',
  181. ]
  182. for attr_name in dir(LLMConfig):
  183. if (
  184. not attr_name.startswith('__')
  185. and attr_name not in known_key_token_attrs_llm
  186. ):
  187. assert (
  188. 'key' not in attr_name.lower()
  189. ), f"Unexpected attribute '{attr_name}' contains 'key' in LLMConfig"
  190. assert (
  191. 'token' not in attr_name.lower() or 'tokens' in attr_name.lower()
  192. ), f"Unexpected attribute '{attr_name}' contains 'token' in LLMConfig"
  193. # Test AgentConfig
  194. # No attrs in AgentConfig have 'key' or 'token' in their name
  195. agent_config = AgentConfig(
  196. name='my_agent', memory_enabled=True, memory_max_threads=4
  197. )
  198. for attr_name in dir(AgentConfig):
  199. if not attr_name.startswith('__'):
  200. assert (
  201. 'key' not in attr_name.lower()
  202. ), f"Unexpected attribute '{attr_name}' contains 'key' in AgentConfig"
  203. assert (
  204. 'token' not in attr_name.lower() or 'tokens' in attr_name.lower()
  205. ), f"Unexpected attribute '{attr_name}' contains 'token' in AgentConfig"
  206. # Test AppConfig
  207. app_config = AppConfig(
  208. llm=llm_config,
  209. agent=agent_config,
  210. e2b_api_key='my_e2b_api_key',
  211. github_token='my_github_token',
  212. )
  213. assert "e2b_api_key='******'" in repr(app_config)
  214. assert "github_token='******'" in repr(app_config)
  215. assert "e2b_api_key='******'" in str(app_config)
  216. assert "github_token='******'" in str(app_config)
  217. # Check that no other attrs in AppConfig have 'key' or 'token' in their name
  218. # This will fail when new attrs are added, and attract attention
  219. known_key_token_attrs_app = ['e2b_api_key', 'github_token']
  220. for attr_name in dir(AppConfig):
  221. if (
  222. not attr_name.startswith('__')
  223. and attr_name not in known_key_token_attrs_app
  224. ):
  225. assert (
  226. 'key' not in attr_name.lower()
  227. ), f"Unexpected attribute '{attr_name}' contains 'key' in AppConfig"
  228. assert (
  229. 'token' not in attr_name.lower() or 'tokens' in attr_name.lower()
  230. ), f"Unexpected attribute '{attr_name}' contains 'token' in AppConfig"