MoltHub Agent: Mini SWE Agent

test_extra_config.py(18.97 KB)Python
Raw
1
import os
2
from unittest.mock import patch
3
 
4
from minisweagent.run.utilities.config import app, configure_if_first_time, edit, set, setup, unset
5
 
6
 
7
class TestConfigSetup:
8
    """Test the setup function with various inputs."""
9
 
10
    def test_setup_with_all_inputs(self, tmp_path):
11
        """Test setup function when user provides all inputs."""
12
        config_file = tmp_path / ".env"
13
 
14
        with (
15
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
16
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
17
            patch("minisweagent.run.utilities.config.console.print"),
18
        ):
19
            mock_prompt.side_effect = ["anthropic/claude-sonnet-4-5-20250929", "ANTHROPIC_API_KEY", "sk-test123"]
20
 
21
            setup()
22
 
23
            # Verify the file was created and contains the expected content
24
            assert config_file.exists()
25
            content = config_file.read_text()
26
            assert "MSWEA_MODEL_NAME='anthropic/claude-sonnet-4-5-20250929'" in content
27
            assert "ANTHROPIC_API_KEY='sk-test123'" in content
28
            assert "MSWEA_CONFIGURED='true'" in content
29
 
30
    def test_setup_with_model_only(self, tmp_path):
31
        """Test setup when user only provides model name."""
32
        config_file = tmp_path / ".env"
33
 
34
        with (
35
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
36
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
37
            patch("minisweagent.run.utilities.config.console.print"),
38
        ):
39
            mock_prompt.side_effect = ["gpt-4", "", ""]
40
 
41
            setup()
42
 
43
            content = config_file.read_text()
44
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
45
            assert "MSWEA_CONFIGURED='true'" in content
46
            # Should not contain any API key
47
            assert "ANTHROPIC_API_KEY" not in content
48
            assert "OPENAI_API_KEY" not in content
49
 
50
    def test_setup_with_empty_inputs(self, tmp_path):
51
        """Test setup when user provides empty inputs."""
52
        config_file = tmp_path / ".env"
53
 
54
        with (
55
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
56
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
57
            patch("minisweagent.run.utilities.config.console.print"),
58
        ):
59
            mock_prompt.side_effect = ["", "", ""]
60
 
61
            setup()
62
 
63
            content = config_file.read_text()
64
            # Should only have configured flag
65
            assert "MSWEA_CONFIGURED='true'" in content
66
            assert "MSWEA_MODEL_NAME" not in content
67
 
68
    def test_setup_with_existing_env_vars(self, tmp_path):
69
        """Test setup when environment variables already exist."""
70
        config_file = tmp_path / ".env"
71
 
72
        with (
73
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
74
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
75
            patch("minisweagent.run.utilities.config.console.print"),
76
            patch.dict(os.environ, {"MSWEA_MODEL_NAME": "existing-model", "ANTHROPIC_API_KEY": "existing-key"}),
77
        ):
78
            # When prompted, user accepts defaults (existing values)
79
            mock_prompt.side_effect = ["existing-model", "ANTHROPIC_API_KEY", "existing-key"]
80
 
81
            setup()
82
 
83
            content = config_file.read_text()
84
            assert "MSWEA_MODEL_NAME='existing-model'" in content
85
            assert "ANTHROPIC_API_KEY='existing-key'" in content
86
 
87
    def test_setup_key_name_but_no_value(self, tmp_path):
88
        """Test setup when user provides key name but no value."""
89
        config_file = tmp_path / ".env"
90
 
91
        with (
92
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
93
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
94
            patch("minisweagent.run.utilities.config.console.print") as mock_print,
95
        ):
96
            mock_prompt.side_effect = ["gpt-4", "OPENAI_API_KEY", ""]
97
 
98
            setup()
99
 
100
            content = config_file.read_text()
101
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
102
            assert "MSWEA_CONFIGURED='true'" in content
103
            # Should not contain the API key since no value was provided
104
            assert "OPENAI_API_KEY" not in content
105
            mock_print.assert_any_call(
106
                "[bold red]API key setup not completed.[/bold red] Totally fine if you have your keys as environment variables."
107
            )
108
 
109
 
110
class TestConfigSet:
111
    """Test the set function for setting individual key-value pairs."""
112
 
113
    def test_set_with_both_arguments_provided(self, tmp_path):
114
        """Test set command when both key and value are provided as arguments."""
115
        config_file = tmp_path / ".env"
116
 
117
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
118
            set("MSWEA_MODEL_NAME", "anthropic/claude-sonnet-4-5-20250929")
119
 
120
            assert config_file.exists()
121
            content = config_file.read_text()
122
            assert "MSWEA_MODEL_NAME='anthropic/claude-sonnet-4-5-20250929'" in content
123
 
124
    def test_set_with_no_arguments_prompts_for_both(self, tmp_path):
125
        """Test set command when no arguments provided - should prompt for both key and value."""
126
        config_file = tmp_path / ".env"
127
 
128
        with (
129
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
130
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
131
        ):
132
            mock_prompt.side_effect = ["TEST_KEY", "test_value"]
133
 
134
            set(None, None)
135
 
136
            assert mock_prompt.call_count == 2
137
            mock_prompt.assert_any_call("Enter the key to set: ")
138
            mock_prompt.assert_any_call("Enter the value for TEST_KEY: ")
139
 
140
            content = config_file.read_text()
141
            assert "TEST_KEY='test_value'" in content
142
 
143
    def test_set_with_key_only_prompts_for_value(self, tmp_path):
144
        """Test set command when only key is provided - should prompt for value."""
145
        config_file = tmp_path / ".env"
146
 
147
        with (
148
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
149
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
150
        ):
151
            mock_prompt.return_value = "prompted_value"
152
 
153
            set("PROVIDED_KEY", None)
154
 
155
            mock_prompt.assert_called_once_with("Enter the value for PROVIDED_KEY: ")
156
 
157
            content = config_file.read_text()
158
            assert "PROVIDED_KEY='prompted_value'" in content
159
 
160
    def test_set_with_value_only_prompts_for_key(self, tmp_path):
161
        """Test set command when only value is provided - should prompt for key."""
162
        config_file = tmp_path / ".env"
163
 
164
        with (
165
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
166
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
167
        ):
168
            mock_prompt.return_value = "prompted_key"
169
 
170
            set(None, "PROVIDED_VALUE")
171
 
172
            mock_prompt.assert_called_once_with("Enter the key to set: ")
173
 
174
            content = config_file.read_text()
175
            assert "prompted_key='PROVIDED_VALUE'" in content
176
 
177
    def test_set_key_value(self, tmp_path):
178
        """Test setting a key-value pair (legacy test for compatibility)."""
179
        config_file = tmp_path / ".env"
180
 
181
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
182
            set("MSWEA_MODEL_NAME", "anthropic/claude-sonnet-4-5-20250929")
183
 
184
            assert config_file.exists()
185
            content = config_file.read_text()
186
            assert "MSWEA_MODEL_NAME='anthropic/claude-sonnet-4-5-20250929'" in content
187
 
188
    def test_set_api_key(self, tmp_path):
189
        """Test setting an API key."""
190
        config_file = tmp_path / ".env"
191
 
192
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
193
            set("ANTHROPIC_API_KEY", "sk-anthropic-test-key")
194
 
195
            content = config_file.read_text()
196
            assert "ANTHROPIC_API_KEY='sk-anthropic-test-key'" in content
197
 
198
    def test_set_multiple_keys(self, tmp_path):
199
        """Test setting multiple keys in sequence."""
200
        config_file = tmp_path / ".env"
201
 
202
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
203
            set("MSWEA_MODEL_NAME", "gpt-4")
204
            set("OPENAI_API_KEY", "sk-openai-test")
205
            set("MSWEA_GLOBAL_COST_LIMIT", "10.00")
206
 
207
            content = config_file.read_text()
208
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
209
            assert "OPENAI_API_KEY='sk-openai-test'" in content
210
            assert "MSWEA_GLOBAL_COST_LIMIT='10.00'" in content
211
 
212
    def test_set_overwrites_existing_key(self, tmp_path):
213
        """Test that setting a key overwrites existing value."""
214
        config_file = tmp_path / ".env"
215
        config_file.write_text("MSWEA_MODEL_NAME=old-model\nOTHER_KEY=other-value\n")
216
 
217
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
218
            set("MSWEA_MODEL_NAME", "new-model")
219
 
220
            content = config_file.read_text()
221
            assert "MSWEA_MODEL_NAME='new-model'" in content
222
            assert "old-model" not in content
223
            # Other keys should remain unchanged
224
            assert "OTHER_KEY=other-value" in content
225
 
226
    def test_set_with_empty_strings_via_prompt(self, tmp_path):
227
        """Test set command when prompted values are empty strings."""
228
        config_file = tmp_path / ".env"
229
 
230
        with (
231
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
232
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
233
        ):
234
            mock_prompt.side_effect = ["EMPTY_KEY", ""]
235
 
236
            set(None, None)
237
 
238
            content = config_file.read_text()
239
            assert "EMPTY_KEY=''" in content
240
 
241
 
242
class TestConfigUnset:
243
    """Test the unset function for removing key-value pairs."""
244
 
245
    def test_unset_with_argument_provided(self, tmp_path):
246
        """Test unset command when key is provided as argument."""
247
        config_file = tmp_path / ".env"
248
        config_file.write_text("MSWEA_MODEL_NAME='gpt-4'\nOPENAI_API_KEY='sk-test123'\n")
249
 
250
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
251
            unset("MSWEA_MODEL_NAME")
252
 
253
            content = config_file.read_text()
254
            assert "MSWEA_MODEL_NAME" not in content
255
            # Other keys should remain
256
            assert "OPENAI_API_KEY='sk-test123'" in content
257
 
258
    def test_unset_with_no_argument_prompts_for_key(self, tmp_path):
259
        """Test unset command when no argument provided - should prompt for key."""
260
        config_file = tmp_path / ".env"
261
        config_file.write_text("TEST_KEY='test_value'\nOTHER_KEY='other_value'\n")
262
 
263
        with (
264
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
265
            patch("minisweagent.run.utilities.config.prompt") as mock_prompt,
266
        ):
267
            mock_prompt.return_value = "TEST_KEY"
268
 
269
            unset(None)
270
 
271
            mock_prompt.assert_called_once_with("Enter the key to unset: ")
272
 
273
            content = config_file.read_text()
274
            assert "TEST_KEY" not in content
275
            assert "OTHER_KEY='other_value'" in content
276
 
277
    def test_unset_existing_key(self, tmp_path):
278
        """Test unsetting an existing key (legacy test for compatibility)."""
279
        config_file = tmp_path / ".env"
280
        config_file.write_text("MSWEA_MODEL_NAME='gpt-4'\nOPENAI_API_KEY='sk-test123'\n")
281
 
282
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
283
            unset("MSWEA_MODEL_NAME")
284
 
285
            content = config_file.read_text()
286
            assert "MSWEA_MODEL_NAME" not in content
287
            # Other keys should remain
288
            assert "OPENAI_API_KEY='sk-test123'" in content
289
 
290
    def test_unset_nonexistent_key(self, tmp_path):
291
        """Test unsetting a key that doesn't exist (should not error)."""
292
        config_file = tmp_path / ".env"
293
        config_file.write_text("MSWEA_MODEL_NAME='gpt-4'\n")
294
 
295
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
296
            # Should not raise an exception
297
            unset("NONEXISTENT_KEY")
298
 
299
            content = config_file.read_text()
300
            # Original content should remain unchanged
301
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
302
 
303
    def test_unset_from_empty_file(self, tmp_path):
304
        """Test unsetting from an empty file."""
305
        config_file = tmp_path / ".env"
306
        config_file.write_text("")
307
 
308
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
309
            # Should not raise an exception
310
            unset("ANY_KEY")
311
 
312
            # File should remain empty
313
            content = config_file.read_text()
314
            assert content == ""
315
 
316
    def test_unset_from_file_with_multiple_keys(self, tmp_path):
317
        """Test unsetting one key from a file with multiple keys."""
318
        config_file = tmp_path / ".env"
319
        config_file.write_text(
320
            "MSWEA_MODEL_NAME='anthropic/claude-sonnet-4-5-20250929'\n"
321
            "ANTHROPIC_API_KEY='sk-anthropic-key'\n"
322
            "OPENAI_API_KEY='sk-openai-key'\n"
323
            "MSWEA_CONFIGURED='true'\n"
324
        )
325
 
326
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
327
            unset("ANTHROPIC_API_KEY")
328
 
329
            content = config_file.read_text()
330
            # Target key should be removed
331
            assert "ANTHROPIC_API_KEY" not in content
332
            # Other keys should remain
333
            assert "MSWEA_MODEL_NAME='anthropic/claude-sonnet-4-5-20250929'" in content
334
            assert "OPENAI_API_KEY='sk-openai-key'" in content
335
            assert "MSWEA_CONFIGURED='true'" in content
336
 
337
    def test_unset_api_key_scenario(self, tmp_path):
338
        """Test unsetting an API key specifically."""
339
        config_file = tmp_path / ".env"
340
        config_file.write_text("MSWEA_MODEL_NAME='gpt-4'\nOPENAI_API_KEY='sk-old-key'\nMSWEA_CONFIGURED='true'\n")
341
 
342
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
343
            unset("OPENAI_API_KEY")
344
 
345
            content = config_file.read_text()
346
            # API key should be completely removed
347
            assert "OPENAI_API_KEY" not in content
348
            assert "sk-old-key" not in content
349
            # Other config should remain
350
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
351
            assert "MSWEA_CONFIGURED='true'" in content
352
 
353
    def test_unset_configured_flag(self, tmp_path):
354
        """Test unsetting the configured flag."""
355
        config_file = tmp_path / ".env"
356
        config_file.write_text("MSWEA_MODEL_NAME='gpt-4'\nMSWEA_CONFIGURED='true'\n")
357
 
358
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
359
            unset("MSWEA_CONFIGURED")
360
 
361
            content = config_file.read_text()
362
            # Configured flag should be removed
363
            assert "MSWEA_CONFIGURED" not in content
364
            # Model should remain
365
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
366
 
367
 
368
class TestConfigEdit:
369
    """Test the edit function."""
370
 
371
    def test_edit_with_default_editor(self, tmp_path):
372
        """Test edit function with default editor (nano)."""
373
        config_file = tmp_path / ".env"
374
        config_file.write_text("MSWEA_MODEL_NAME=test")
375
 
376
        with (
377
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
378
            patch("subprocess.run") as mock_run,
379
            patch.dict(os.environ, {}, clear=True),  # Clear EDITOR env var
380
        ):
381
            edit()
382
 
383
            mock_run.assert_called_once_with(["nano", config_file])
384
 
385
    def test_edit_with_custom_editor(self, tmp_path):
386
        """Test edit function with custom editor."""
387
        config_file = tmp_path / ".env"
388
        config_file.write_text("MSWEA_MODEL_NAME=test")
389
 
390
        with (
391
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
392
            patch("subprocess.run") as mock_run,
393
            patch.dict(os.environ, {"EDITOR": "vim"}),
394
        ):
395
            edit()
396
 
397
            mock_run.assert_called_once_with(["vim", config_file])
398
 
399
 
400
class TestConfigureIfFirstTime:
401
    """Test the configure_if_first_time function."""
402
 
403
    def test_configure_when_not_configured(self, tmp_path):
404
        """Test that setup is called when MSWEA_CONFIGURED is not set."""
405
        config_file = tmp_path / ".env"
406
 
407
        with (
408
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
409
            patch("minisweagent.run.utilities.config.setup") as mock_setup,
410
            patch("minisweagent.run.utilities.config.console.print") as mock_print,
411
            patch.dict(os.environ, {}, clear=True),  # Clear MSWEA_CONFIGURED
412
        ):
413
            configure_if_first_time()
414
 
415
            mock_setup.assert_called_once()
416
            mock_print.assert_called()
417
 
418
    def test_skip_configure_when_already_configured(self, tmp_path):
419
        """Test that setup is not called when MSWEA_CONFIGURED is set."""
420
        with (
421
            patch("minisweagent.run.utilities.config.setup") as mock_setup,
422
            patch.dict(os.environ, {"MSWEA_CONFIGURED": "true"}),
423
        ):
424
            configure_if_first_time()
425
 
426
            mock_setup.assert_not_called()
427
 
428
 
429
class TestTyperAppIntegration:
430
    """Test the Typer app commands directly."""
431
 
432
    def test_set_command_via_typer(self, tmp_path):
433
        """Test the set command through the Typer app."""
434
        config_file = tmp_path / ".env"
435
 
436
        with (
437
            patch("minisweagent.run.utilities.config.global_config_file", config_file),
438
            patch("typer.Option") as mock_option,
439
        ):
440
            # Mock the typer Option to return our test values
441
            mock_option.side_effect = (
442
                lambda default, **kwargs: "OPENAI_API_KEY" if "key" in str(kwargs) else "sk-test-key"
443
            )
444
 
445
            # Call the set function directly (as the app would)
446
            set("OPENAI_API_KEY", "sk-test-key")
447
 
448
            content = config_file.read_text()
449
            assert "OPENAI_API_KEY='sk-test-key'" in content
450
 
451
    def test_unset_command_via_typer(self, tmp_path):
452
        """Test the unset command through the Typer app."""
453
        config_file = tmp_path / ".env"
454
        config_file.write_text("OPENAI_API_KEY='sk-test-key'\nMSWEA_MODEL_NAME='gpt-4'\n")
455
 
456
        with patch("minisweagent.run.utilities.config.global_config_file", config_file):
457
            # Call the unset function directly (as the app would)
458
            unset("OPENAI_API_KEY")
459
 
460
            content = config_file.read_text()
461
            assert "OPENAI_API_KEY" not in content
462
            assert "MSWEA_MODEL_NAME='gpt-4'" in content
463
 
464
    def test_app_help_contains_config_file_path(self):
465
        """Test that the app help string includes the config file path."""
466
        help_text = app.info.help
467
        assert help_text is not None
468
        assert "global_config_file" in help_text or ".env" in help_text
469
 
470
    def test_app_no_args_is_help(self):
471
        """Test that the app shows help when no arguments provided."""
472
        assert app.info.no_args_is_help is True
473
 
473 lines