test_config.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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-4o')
  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-4o'
  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. default_config.ssh_password = None # prevent leak
  122. default_config.jwt_secret = None # prevent leak
  123. assert default_config.llm.model == 'gpt-5-turbo-1106'
  124. assert default_config.llm.custom_llm_provider is None
  125. if default_config.github_token is not None: # prevent leak
  126. pytest.fail('GitHub token should be empty')
  127. if default_config.llm.api_key is not None: # prevent leak
  128. pytest.fail('LLM API key should be empty.')
  129. def test_finalize_config(default_config):
  130. # Test finalize config
  131. default_config.sandbox_type = 'local'
  132. finalize_config(default_config)
  133. assert (
  134. default_config.workspace_mount_path_in_sandbox
  135. == default_config.workspace_mount_path
  136. )
  137. # tests for workspace, mount path, path in sandbox, cache dir
  138. def test_workspace_mount_path_default(default_config):
  139. assert default_config.workspace_mount_path is None
  140. finalize_config(default_config)
  141. assert default_config.workspace_mount_path == os.path.abspath(
  142. default_config.workspace_base
  143. )
  144. def test_workspace_mount_path_in_sandbox_local(default_config):
  145. assert default_config.workspace_mount_path_in_sandbox == '/workspace'
  146. default_config.sandbox_type = 'local'
  147. finalize_config(default_config)
  148. assert (
  149. default_config.workspace_mount_path_in_sandbox
  150. == default_config.workspace_mount_path
  151. )
  152. def test_workspace_mount_rewrite(default_config, monkeypatch):
  153. default_config.workspace_base = '/home/user/project'
  154. default_config.workspace_mount_rewrite = '/home/user:/sandbox'
  155. monkeypatch.setattr('os.getcwd', lambda: '/current/working/directory')
  156. finalize_config(default_config)
  157. assert default_config.workspace_mount_path == '/sandbox/project'
  158. def test_embedding_base_url_default(default_config):
  159. default_config.llm.base_url = 'https://api.exampleapi.com'
  160. finalize_config(default_config)
  161. assert default_config.llm.embedding_base_url == 'https://api.exampleapi.com'
  162. def test_cache_dir_creation(default_config, tmpdir):
  163. default_config.cache_dir = str(tmpdir.join('test_cache'))
  164. finalize_config(default_config)
  165. assert os.path.exists(default_config.cache_dir)
  166. def test_api_keys_repr_str():
  167. # Test LLMConfig
  168. llm_config = LLMConfig(
  169. api_key='my_api_key',
  170. aws_access_key_id='my_access_key',
  171. aws_secret_access_key='my_secret_key',
  172. )
  173. assert "api_key='******'" in repr(llm_config)
  174. assert "aws_access_key_id='******'" in repr(llm_config)
  175. assert "aws_secret_access_key='******'" in repr(llm_config)
  176. assert "api_key='******'" in str(llm_config)
  177. assert "aws_access_key_id='******'" in str(llm_config)
  178. assert "aws_secret_access_key='******'" in str(llm_config)
  179. # Check that no other attrs in LLMConfig have 'key' or 'token' in their name
  180. # This will fail when new attrs are added, and attract attention
  181. known_key_token_attrs_llm = [
  182. 'api_key',
  183. 'aws_access_key_id',
  184. 'aws_secret_access_key',
  185. 'input_cost_per_token',
  186. 'output_cost_per_token',
  187. ]
  188. for attr_name in dir(LLMConfig):
  189. if (
  190. not attr_name.startswith('__')
  191. and attr_name not in known_key_token_attrs_llm
  192. ):
  193. assert (
  194. 'key' not in attr_name.lower()
  195. ), f"Unexpected attribute '{attr_name}' contains 'key' in LLMConfig"
  196. assert (
  197. 'token' not in attr_name.lower() or 'tokens' in attr_name.lower()
  198. ), f"Unexpected attribute '{attr_name}' contains 'token' in LLMConfig"
  199. # Test AgentConfig
  200. # No attrs in AgentConfig have 'key' or 'token' in their name
  201. agent_config = AgentConfig(
  202. name='my_agent', memory_enabled=True, memory_max_threads=4
  203. )
  204. for attr_name in dir(AgentConfig):
  205. if not attr_name.startswith('__'):
  206. assert (
  207. 'key' not in attr_name.lower()
  208. ), f"Unexpected attribute '{attr_name}' contains 'key' in AgentConfig"
  209. assert (
  210. 'token' not in attr_name.lower() or 'tokens' in attr_name.lower()
  211. ), f"Unexpected attribute '{attr_name}' contains 'token' in AgentConfig"
  212. # Test AppConfig
  213. app_config = AppConfig(
  214. llm=llm_config,
  215. agent=agent_config,
  216. e2b_api_key='my_e2b_api_key',
  217. github_token='my_github_token',
  218. )
  219. assert "e2b_api_key='******'" in repr(app_config)
  220. assert "github_token='******'" in repr(app_config)
  221. assert "e2b_api_key='******'" in str(app_config)
  222. assert "github_token='******'" in str(app_config)
  223. # Check that no other attrs in AppConfig have 'key' or 'token' in their name
  224. # This will fail when new attrs are added, and attract attention
  225. known_key_token_attrs_app = ['e2b_api_key', 'github_token']
  226. for attr_name in dir(AppConfig):
  227. if (
  228. not attr_name.startswith('__')
  229. and attr_name not in known_key_token_attrs_app
  230. ):
  231. assert (
  232. 'key' not in attr_name.lower()
  233. ), f"Unexpected attribute '{attr_name}' contains 'key' in AppConfig"
  234. assert (
  235. 'token' not in attr_name.lower() or 'tokens' in attr_name.lower()
  236. ), f"Unexpected attribute '{attr_name}' contains 'token' in AppConfig"
  237. def test_max_iterations_and_max_budget_per_task_from_toml(temp_toml_file):
  238. temp_toml = """
  239. [core]
  240. max_iterations = 100
  241. max_budget_per_task = 4.0
  242. """
  243. config = AppConfig()
  244. with open(temp_toml_file, 'w') as f:
  245. f.write(temp_toml)
  246. load_from_toml(config, temp_toml_file)
  247. assert config.max_iterations == 100
  248. assert config.max_budget_per_task == 4.0