massgen 0.1.0a3__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of massgen might be problematic. Click here for more details.

Files changed (111) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/agent_config.py +17 -0
  3. massgen/api_params_handler/_api_params_handler_base.py +1 -0
  4. massgen/api_params_handler/_chat_completions_api_params_handler.py +8 -1
  5. massgen/api_params_handler/_claude_api_params_handler.py +8 -1
  6. massgen/api_params_handler/_gemini_api_params_handler.py +73 -0
  7. massgen/api_params_handler/_response_api_params_handler.py +8 -1
  8. massgen/backend/base.py +31 -0
  9. massgen/backend/{base_with_mcp.py → base_with_custom_tool_and_mcp.py} +282 -11
  10. massgen/backend/chat_completions.py +182 -92
  11. massgen/backend/claude.py +115 -18
  12. massgen/backend/claude_code.py +378 -14
  13. massgen/backend/docs/CLAUDE_API_RESEARCH.md +3 -3
  14. massgen/backend/gemini.py +1275 -1607
  15. massgen/backend/gemini_mcp_manager.py +545 -0
  16. massgen/backend/gemini_trackers.py +344 -0
  17. massgen/backend/gemini_utils.py +43 -0
  18. massgen/backend/response.py +129 -70
  19. massgen/cli.py +577 -110
  20. massgen/config_builder.py +376 -27
  21. massgen/configs/README.md +111 -80
  22. massgen/configs/basic/multi/three_agents_default.yaml +1 -1
  23. massgen/configs/basic/single/single_agent.yaml +1 -1
  24. massgen/configs/providers/openai/gpt5_nano.yaml +3 -3
  25. massgen/configs/tools/custom_tools/claude_code_custom_tool_example.yaml +32 -0
  26. massgen/configs/tools/custom_tools/claude_code_custom_tool_example_no_path.yaml +28 -0
  27. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +40 -0
  28. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_wrong_mcp_example.yaml +38 -0
  29. massgen/configs/tools/custom_tools/claude_code_wrong_custom_tool_with_mcp_example.yaml +38 -0
  30. massgen/configs/tools/custom_tools/claude_custom_tool_example.yaml +24 -0
  31. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +22 -0
  32. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +35 -0
  33. massgen/configs/tools/custom_tools/claude_custom_tool_with_wrong_mcp_example.yaml +33 -0
  34. massgen/configs/tools/custom_tools/claude_wrong_custom_tool_with_mcp_example.yaml +33 -0
  35. massgen/configs/tools/custom_tools/gemini_custom_tool_example.yaml +24 -0
  36. massgen/configs/tools/custom_tools/gemini_custom_tool_example_no_path.yaml +22 -0
  37. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +35 -0
  38. massgen/configs/tools/custom_tools/gemini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  39. massgen/configs/tools/custom_tools/gemini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  40. massgen/configs/tools/custom_tools/github_issue_market_analysis.yaml +94 -0
  41. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example.yaml +24 -0
  42. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_example_no_path.yaml +22 -0
  43. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +35 -0
  44. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_wrong_mcp_example.yaml +33 -0
  45. massgen/configs/tools/custom_tools/gpt5_nano_wrong_custom_tool_with_mcp_example.yaml +33 -0
  46. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example.yaml +25 -0
  47. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_example_no_path.yaml +23 -0
  48. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +34 -0
  49. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_wrong_mcp_example.yaml +34 -0
  50. massgen/configs/tools/custom_tools/gpt_oss_wrong_custom_tool_with_mcp_example.yaml +34 -0
  51. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example.yaml +24 -0
  52. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_example_no_path.yaml +22 -0
  53. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +35 -0
  54. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_wrong_mcp_example.yaml +33 -0
  55. massgen/configs/tools/custom_tools/grok3_mini_wrong_custom_tool_with_mcp_example.yaml +33 -0
  56. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example.yaml +25 -0
  57. massgen/configs/tools/custom_tools/qwen_api_custom_tool_example_no_path.yaml +23 -0
  58. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +36 -0
  59. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_wrong_mcp_example.yaml +34 -0
  60. massgen/configs/tools/custom_tools/qwen_api_wrong_custom_tool_with_mcp_example.yaml +34 -0
  61. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example.yaml +24 -0
  62. massgen/configs/tools/custom_tools/qwen_local_custom_tool_example_no_path.yaml +22 -0
  63. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_mcp_example.yaml +35 -0
  64. massgen/configs/tools/custom_tools/qwen_local_custom_tool_with_wrong_mcp_example.yaml +33 -0
  65. massgen/configs/tools/custom_tools/qwen_local_wrong_custom_tool_with_mcp_example.yaml +33 -0
  66. massgen/configs/tools/filesystem/claude_code_context_sharing.yaml +1 -1
  67. massgen/configs/voting/gemini_gpt_voting_sensitivity.yaml +67 -0
  68. massgen/formatter/_chat_completions_formatter.py +104 -0
  69. massgen/formatter/_claude_formatter.py +120 -0
  70. massgen/formatter/_gemini_formatter.py +448 -0
  71. massgen/formatter/_response_formatter.py +88 -0
  72. massgen/frontend/coordination_ui.py +4 -2
  73. massgen/logger_config.py +35 -3
  74. massgen/message_templates.py +56 -6
  75. massgen/orchestrator.py +179 -10
  76. massgen/stream_chunk/base.py +3 -0
  77. massgen/tests/custom_tools_example.py +392 -0
  78. massgen/tests/mcp_test_server.py +17 -7
  79. massgen/tests/test_config_builder.py +423 -0
  80. massgen/tests/test_custom_tools.py +401 -0
  81. massgen/tests/test_tools.py +127 -0
  82. massgen/tool/README.md +935 -0
  83. massgen/tool/__init__.py +39 -0
  84. massgen/tool/_async_helpers.py +70 -0
  85. massgen/tool/_basic/__init__.py +8 -0
  86. massgen/tool/_basic/_two_num_tool.py +24 -0
  87. massgen/tool/_code_executors/__init__.py +10 -0
  88. massgen/tool/_code_executors/_python_executor.py +74 -0
  89. massgen/tool/_code_executors/_shell_executor.py +61 -0
  90. massgen/tool/_exceptions.py +39 -0
  91. massgen/tool/_file_handlers/__init__.py +10 -0
  92. massgen/tool/_file_handlers/_file_operations.py +218 -0
  93. massgen/tool/_manager.py +634 -0
  94. massgen/tool/_registered_tool.py +88 -0
  95. massgen/tool/_result.py +66 -0
  96. massgen/tool/_self_evolution/_github_issue_analyzer.py +369 -0
  97. massgen/tool/docs/builtin_tools.md +681 -0
  98. massgen/tool/docs/exceptions.md +794 -0
  99. massgen/tool/docs/execution_results.md +691 -0
  100. massgen/tool/docs/manager.md +887 -0
  101. massgen/tool/docs/workflow_toolkits.md +529 -0
  102. massgen/tool/workflow_toolkits/__init__.py +57 -0
  103. massgen/tool/workflow_toolkits/base.py +55 -0
  104. massgen/tool/workflow_toolkits/new_answer.py +126 -0
  105. massgen/tool/workflow_toolkits/vote.py +167 -0
  106. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/METADATA +89 -131
  107. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/RECORD +111 -36
  108. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/WHEEL +0 -0
  109. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/entry_points.txt +0 -0
  110. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/licenses/LICENSE +0 -0
  111. {massgen-0.1.0a3.dist-info → massgen-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,423 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Comprehensive tests for config_builder.py - Agent cloning and preset application.
4
+
5
+ These tests ensure:
6
+ 1. Cross-provider cloning preserves correct settings
7
+ 2. Incompatible settings are skipped with warnings
8
+ 3. Workspace uniqueness across agents
9
+ 4. Tool compatibility matrix is enforced
10
+
11
+ Run with: uv run pytest massgen/tests/test_config_builder.py -v
12
+ """
13
+
14
+ import pytest
15
+
16
+ from massgen.config_builder import ConfigBuilder
17
+
18
+
19
+ class TestCloneAgent:
20
+ """Test agent cloning across providers with compatibility checks."""
21
+
22
+ @pytest.fixture
23
+ def builder(self):
24
+ """Create a ConfigBuilder instance for testing."""
25
+ return ConfigBuilder()
26
+
27
+ def test_clone_openai_to_gemini_preserves_provider(self, builder):
28
+ """Test cloning OpenAI to Gemini preserves Gemini provider."""
29
+ source = {
30
+ "id": "agent_a",
31
+ "backend": {
32
+ "type": "openai",
33
+ "model": "gpt-5",
34
+ "enable_web_search": True,
35
+ "cwd": "workspace1",
36
+ },
37
+ }
38
+
39
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="gemini")
40
+
41
+ assert cloned["id"] == "agent_b"
42
+ assert cloned["backend"]["type"] == "gemini"
43
+ assert cloned["backend"]["model"] == "gemini-2.5-flash" # Default Gemini model
44
+ assert cloned["backend"]["enable_web_search"] is True # Compatible, should copy
45
+ assert cloned["backend"]["cwd"] == "workspace2" # Filesystem copied and updated to agent_b's number
46
+
47
+ def test_clone_copies_filesystem_cwd(self, builder):
48
+ """Test that cwd (filesystem) is copied and updated for target agent."""
49
+ source = {
50
+ "id": "agent_a",
51
+ "backend": {"type": "openai", "model": "gpt-5", "cwd": "workspace1"},
52
+ }
53
+
54
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="claude")
55
+
56
+ # cwd should exist (copied from source) and updated to agent_b's workspace number
57
+ assert "cwd" in cloned["backend"]
58
+ assert cloned["backend"]["cwd"] == "workspace2" # agent_b -> workspace2
59
+
60
+ def test_clone_updates_workspace_number(self, builder):
61
+ """Test that workspace number updates for target agent ID."""
62
+ source = {
63
+ "id": "agent_a",
64
+ "backend": {"type": "openai", "model": "gpt-5", "cwd": "workspace1"},
65
+ }
66
+
67
+ cloned = builder.clone_agent(source, "agent_c", target_backend_type="openai")
68
+
69
+ assert cloned["backend"]["cwd"] == "workspace3" # agent_c -> workspace3
70
+
71
+ def test_clone_copies_compatible_tools(self, builder):
72
+ """Test that compatible tools are copied across providers."""
73
+ source = {
74
+ "id": "agent_a",
75
+ "backend": {
76
+ "type": "openai",
77
+ "model": "gpt-5",
78
+ "enable_web_search": True,
79
+ "enable_mcp_command_line": True,
80
+ "command_line_execution_mode": "docker",
81
+ },
82
+ }
83
+
84
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="gemini")
85
+
86
+ # Universal tools should be copied
87
+ assert cloned["backend"]["enable_mcp_command_line"] is True
88
+ assert cloned["backend"]["command_line_execution_mode"] == "docker"
89
+ # Web search compatible with both OpenAI and Gemini
90
+ assert cloned["backend"]["enable_web_search"] is True
91
+
92
+ def test_clone_skips_incompatible_tools(self, builder):
93
+ """Test that incompatible tools are skipped with warnings."""
94
+ source = {
95
+ "id": "agent_a",
96
+ "backend": {
97
+ "type": "openai",
98
+ "model": "gpt-5",
99
+ "enable_code_interpreter": True, # OpenAI-specific
100
+ },
101
+ }
102
+
103
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="gemini")
104
+
105
+ # Incompatible setting should be skipped
106
+ assert "enable_code_interpreter" not in cloned["backend"]
107
+ # Should generate skipped settings list
108
+ assert "_skipped_settings" in cloned
109
+ assert any("enable_code_interpreter" in s for s in cloned["_skipped_settings"])
110
+
111
+ def test_clone_skips_openai_reasoning_to_gemini(self, builder):
112
+ """Test that OpenAI reasoning/text settings are skipped for other providers."""
113
+ source = {
114
+ "id": "agent_a",
115
+ "backend": {
116
+ "type": "openai",
117
+ "model": "gpt-5",
118
+ "text": {"verbosity": "medium"},
119
+ "reasoning": {"effort": "high", "summary": "auto"},
120
+ },
121
+ }
122
+
123
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="gemini")
124
+
125
+ # OpenAI-specific settings should be skipped
126
+ assert "text" not in cloned["backend"]
127
+ assert "reasoning" not in cloned["backend"]
128
+ # Should be in skipped list
129
+ assert any("text" in s for s in cloned["_skipped_settings"])
130
+ assert any("reasoning" in s for s in cloned["_skipped_settings"])
131
+
132
+ def test_clone_preserves_openai_reasoning_to_openai(self, builder):
133
+ """Test that OpenAI reasoning settings are preserved when cloning to OpenAI."""
134
+ source = {
135
+ "id": "agent_a",
136
+ "backend": {
137
+ "type": "openai",
138
+ "model": "gpt-5",
139
+ "text": {"verbosity": "medium"},
140
+ "reasoning": {"effort": "high", "summary": "auto"},
141
+ },
142
+ }
143
+
144
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="openai")
145
+
146
+ # OpenAI-specific settings should be preserved
147
+ assert cloned["backend"]["text"]["verbosity"] == "medium"
148
+ assert cloned["backend"]["reasoning"]["effort"] == "high"
149
+
150
+ def test_clone_skips_code_interpreter_openai_to_gemini(self, builder):
151
+ """Test code_interpreter is skipped when cloning OpenAI to Gemini."""
152
+ source = {
153
+ "id": "agent_a",
154
+ "backend": {
155
+ "type": "openai",
156
+ "model": "gpt-5",
157
+ "enable_code_interpreter": True,
158
+ },
159
+ }
160
+
161
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="gemini")
162
+
163
+ assert "enable_code_interpreter" not in cloned["backend"]
164
+ assert any("enable_code_interpreter" in s for s in cloned["_skipped_settings"])
165
+
166
+ def test_clone_skips_code_execution_gemini_to_openai(self, builder):
167
+ """Test code_execution is skipped when cloning Gemini to OpenAI."""
168
+ source = {
169
+ "id": "agent_a",
170
+ "backend": {"type": "gemini", "model": "gemini-2.5-pro", "enable_code_execution": True},
171
+ }
172
+
173
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="openai")
174
+
175
+ assert "enable_code_execution" not in cloned["backend"]
176
+ assert any("enable_code_execution" in s for s in cloned["_skipped_settings"])
177
+
178
+ def test_clone_copies_mcp_servers_when_supported(self, builder):
179
+ """Test MCP servers are copied when target supports MCP."""
180
+ source = {
181
+ "id": "agent_a",
182
+ "backend": {
183
+ "type": "openai",
184
+ "model": "gpt-5",
185
+ "mcp_servers": [
186
+ {"name": "weather", "command": "npx", "args": ["-y", "@fak111/weather-mcp"]},
187
+ ],
188
+ },
189
+ }
190
+
191
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="gemini")
192
+
193
+ # Gemini supports MCP, should copy
194
+ assert "mcp_servers" in cloned["backend"]
195
+ assert cloned["backend"]["mcp_servers"][0]["name"] == "weather"
196
+
197
+ def test_clone_same_provider_copies_everything(self, builder):
198
+ """Test cloning within same provider copies all settings."""
199
+ source = {
200
+ "id": "agent_a",
201
+ "backend": {
202
+ "type": "openai",
203
+ "model": "gpt-5",
204
+ "enable_web_search": True,
205
+ "enable_code_interpreter": True,
206
+ "text": {"verbosity": "high"},
207
+ "reasoning": {"effort": "medium", "summary": "auto"},
208
+ "cwd": "workspace1",
209
+ },
210
+ }
211
+
212
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="openai")
213
+
214
+ # Everything should be copied for same provider
215
+ assert cloned["backend"]["enable_web_search"] is True
216
+ assert cloned["backend"]["enable_code_interpreter"] is True
217
+ assert cloned["backend"]["text"]["verbosity"] == "high"
218
+ assert cloned["backend"]["reasoning"]["effort"] == "medium"
219
+
220
+ def test_clone_generates_skipped_settings_list(self, builder):
221
+ """Test that _skipped_settings list is generated for incompatible settings."""
222
+ source = {
223
+ "id": "agent_a",
224
+ "backend": {
225
+ "type": "openai",
226
+ "model": "gpt-5",
227
+ "enable_code_interpreter": True,
228
+ "text": {"verbosity": "high"},
229
+ "reasoning": {"effort": "medium", "summary": "auto"},
230
+ },
231
+ }
232
+
233
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type="claude")
234
+
235
+ # Should have skipped settings
236
+ assert "_skipped_settings" in cloned
237
+ skipped = cloned["_skipped_settings"]
238
+ assert len(skipped) > 0
239
+ # All three should be skipped
240
+ assert any("enable_code_interpreter" in s for s in skipped)
241
+ assert any("text" in s for s in skipped)
242
+ assert any("reasoning" in s for s in skipped)
243
+
244
+
245
+ class TestApplyPresetToAgent:
246
+ """Test preset application generates unique workspaces."""
247
+
248
+ @pytest.fixture
249
+ def builder(self):
250
+ """Create a ConfigBuilder instance for testing."""
251
+ return ConfigBuilder()
252
+
253
+ def test_apply_preset_generates_unique_workspace(self, builder):
254
+ """Test that preset application generates workspace based on agent index."""
255
+ agent = {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5"}}
256
+
257
+ updated = builder.apply_preset_to_agent(agent, "coding", agent_index=1)
258
+
259
+ assert updated["backend"]["cwd"] == "workspace1"
260
+
261
+ def test_apply_preset_multiple_agents_unique_workspaces(self, builder):
262
+ """Test that multiple agents get unique workspace numbers."""
263
+ agents = [
264
+ {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5"}},
265
+ {"id": "agent_b", "backend": {"type": "gemini", "model": "gemini-2.5-pro"}},
266
+ {"id": "agent_c", "backend": {"type": "claude", "model": "claude-3-5-sonnet-20241022"}},
267
+ ]
268
+
269
+ for i, agent in enumerate(agents):
270
+ updated = builder.apply_preset_to_agent(agent, "coding", agent_index=i + 1)
271
+ agents[i] = updated
272
+
273
+ # Check all have unique workspaces
274
+ assert agents[0]["backend"]["cwd"] == "workspace1"
275
+ assert agents[1]["backend"]["cwd"] == "workspace2"
276
+ assert agents[2]["backend"]["cwd"] == "workspace3"
277
+
278
+ def test_apply_preset_filesystem_enabled_for_preset(self, builder):
279
+ """Test that filesystem is enabled for presets that require it."""
280
+ agent = {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5"}}
281
+
282
+ updated = builder.apply_preset_to_agent(agent, "coding", agent_index=1)
283
+
284
+ # Coding preset requires filesystem
285
+ assert "cwd" in updated["backend"]
286
+
287
+ def test_apply_preset_respects_agent_index(self, builder):
288
+ """Test that agent_index correctly determines workspace number."""
289
+ agent = {"id": "agent_c", "backend": {"type": "openai", "model": "gpt-5"}}
290
+
291
+ updated = builder.apply_preset_to_agent(agent, "coding", agent_index=3)
292
+
293
+ assert updated["backend"]["cwd"] == "workspace3"
294
+
295
+
296
+ class TestCrossProviderCompatibility:
297
+ """Test tool compatibility matrix across providers."""
298
+
299
+ @pytest.fixture
300
+ def builder(self):
301
+ """Create a ConfigBuilder instance for testing."""
302
+ return ConfigBuilder()
303
+
304
+ def test_web_search_compatible_providers(self, builder):
305
+ """Test web_search is compatible with OpenAI, Gemini, Claude, Grok."""
306
+ source = {
307
+ "id": "agent_a",
308
+ "backend": {"type": "openai", "model": "gpt-5", "enable_web_search": True},
309
+ }
310
+
311
+ for target_type in ["gemini", "claude", "grok"]:
312
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type=target_type)
313
+ assert cloned["backend"]["enable_web_search"] is True, f"web_search should work on {target_type}"
314
+
315
+ def test_code_interpreter_only_openai_azure(self, builder):
316
+ """Test code_interpreter only works on OpenAI and Azure OpenAI."""
317
+ source = {
318
+ "id": "agent_a",
319
+ "backend": {"type": "openai", "model": "gpt-5", "enable_code_interpreter": True},
320
+ }
321
+
322
+ # Should be skipped for non-OpenAI providers
323
+ for target_type in ["gemini", "claude", "grok"]:
324
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type=target_type)
325
+ assert "enable_code_interpreter" not in cloned["backend"], f"code_interpreter shouldn't work on {target_type}"
326
+ assert any("enable_code_interpreter" in s for s in cloned["_skipped_settings"])
327
+
328
+ def test_code_execution_only_claude_gemini(self, builder):
329
+ """Test code_execution only works on Claude and Gemini."""
330
+ source = {
331
+ "id": "agent_a",
332
+ "backend": {"type": "gemini", "model": "gemini-2.5-pro", "enable_code_execution": True},
333
+ }
334
+
335
+ # Should work on Claude
336
+ cloned_claude = builder.clone_agent(source, "agent_b", target_backend_type="claude")
337
+ assert cloned_claude["backend"]["enable_code_execution"] is True
338
+
339
+ # Should be skipped for OpenAI
340
+ cloned_openai = builder.clone_agent(source, "agent_c", target_backend_type="openai")
341
+ assert "enable_code_execution" not in cloned_openai["backend"]
342
+ assert any("enable_code_execution" in s for s in cloned_openai["_skipped_settings"])
343
+
344
+ def test_filesystem_copied_across_all_providers(self, builder):
345
+ """Test filesystem (cwd) is copied across all providers and updated to target agent number."""
346
+ source = {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5", "cwd": "workspace1"}}
347
+
348
+ for target_type in ["gemini", "claude", "grok", "claude_code", "lmstudio"]:
349
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type=target_type)
350
+ # cwd should exist and be updated to workspace2 for agent_b
351
+ assert "cwd" in cloned["backend"], f"cwd should be copied to {target_type}"
352
+ assert cloned["backend"]["cwd"] == "workspace2", f"cwd should be workspace2 for {target_type}"
353
+
354
+ def test_command_line_execution_mode_universal(self, builder):
355
+ """Test command_line_execution_mode is universal across providers."""
356
+ source = {
357
+ "id": "agent_a",
358
+ "backend": {
359
+ "type": "openai",
360
+ "model": "gpt-5",
361
+ "enable_mcp_command_line": True,
362
+ "command_line_execution_mode": "docker",
363
+ },
364
+ }
365
+
366
+ for target_type in ["gemini", "claude", "grok"]:
367
+ cloned = builder.clone_agent(source, "agent_b", target_backend_type=target_type)
368
+ assert cloned["backend"]["enable_mcp_command_line"] is True
369
+ assert cloned["backend"]["command_line_execution_mode"] == "docker"
370
+
371
+
372
+ class TestWorkspaceUniqueness:
373
+ """Test workspace uniqueness across different scenarios."""
374
+
375
+ @pytest.fixture
376
+ def builder(self):
377
+ """Create a ConfigBuilder instance for testing."""
378
+ return ConfigBuilder()
379
+
380
+ def test_three_agents_get_workspace1_2_3(self, builder):
381
+ """Test that three agents get workspace1, workspace2, workspace3."""
382
+ agents = [
383
+ {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5"}},
384
+ {"id": "agent_b", "backend": {"type": "gemini", "model": "gemini-2.5-pro"}},
385
+ {"id": "agent_c", "backend": {"type": "claude", "model": "claude-3-5-sonnet-20241022"}},
386
+ ]
387
+
388
+ for i, agent in enumerate(agents):
389
+ agents[i] = builder.apply_preset_to_agent(agent, "coding", agent_index=i + 1)
390
+
391
+ assert agents[0]["backend"]["cwd"] == "workspace1"
392
+ assert agents[1]["backend"]["cwd"] == "workspace2"
393
+ assert agents[2]["backend"]["cwd"] == "workspace3"
394
+
395
+ def test_clone_updates_workspace_for_target_agent(self, builder):
396
+ """Test that cloning updates workspace to match target agent ID."""
397
+ source = {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5", "cwd": "workspace1"}}
398
+
399
+ # Clone to agent_d should get workspace4
400
+ cloned = builder.clone_agent(source, "agent_d", target_backend_type="openai")
401
+
402
+ assert cloned["backend"]["cwd"] == "workspace4"
403
+
404
+ def test_mixed_providers_all_unique_workspaces(self, builder):
405
+ """Test mixed preset + clone scenario maintains unique workspaces."""
406
+ # Agent A from preset
407
+ agent_a = {"id": "agent_a", "backend": {"type": "openai", "model": "gpt-5"}}
408
+ agent_a = builder.apply_preset_to_agent(agent_a, "coding", agent_index=1)
409
+
410
+ # Agent B cloned from A
411
+ agent_b = builder.clone_agent(agent_a, "agent_b", target_backend_type="gemini")
412
+
413
+ # Agent C from preset
414
+ agent_c = {"id": "agent_c", "backend": {"type": "claude", "model": "claude-3-5-sonnet-20241022"}}
415
+ agent_c = builder.apply_preset_to_agent(agent_c, "coding", agent_index=3)
416
+
417
+ assert agent_a["backend"]["cwd"] == "workspace1"
418
+ assert agent_b["backend"]["cwd"] == "workspace2" # Updated from agent_b ID
419
+ assert agent_c["backend"]["cwd"] == "workspace3"
420
+
421
+
422
+ if __name__ == "__main__":
423
+ pytest.main([__file__, "-v"])