massgen 0.1.4__py3-none-any.whl → 0.1.6__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 (84) hide show
  1. massgen/__init__.py +1 -1
  2. massgen/backend/base_with_custom_tool_and_mcp.py +453 -23
  3. massgen/backend/capabilities.py +39 -0
  4. massgen/backend/chat_completions.py +111 -197
  5. massgen/backend/claude.py +210 -181
  6. massgen/backend/gemini.py +1015 -1559
  7. massgen/backend/grok.py +3 -2
  8. massgen/backend/response.py +160 -220
  9. massgen/chat_agent.py +340 -20
  10. massgen/cli.py +399 -25
  11. massgen/config_builder.py +20 -54
  12. massgen/config_validator.py +931 -0
  13. massgen/configs/README.md +95 -10
  14. massgen/configs/memory/gpt5mini_gemini_baseline_research_to_implementation.yaml +94 -0
  15. massgen/configs/memory/gpt5mini_gemini_context_window_management.yaml +187 -0
  16. massgen/configs/memory/gpt5mini_gemini_research_to_implementation.yaml +127 -0
  17. massgen/configs/memory/gpt5mini_high_reasoning_gemini.yaml +107 -0
  18. massgen/configs/memory/single_agent_compression_test.yaml +64 -0
  19. massgen/configs/tools/custom_tools/claude_code_custom_tool_with_mcp_example.yaml +1 -0
  20. massgen/configs/tools/custom_tools/claude_custom_tool_example_no_path.yaml +1 -1
  21. massgen/configs/tools/custom_tools/claude_custom_tool_with_mcp_example.yaml +1 -0
  22. massgen/configs/tools/custom_tools/computer_use_browser_example.yaml +1 -1
  23. massgen/configs/tools/custom_tools/computer_use_docker_example.yaml +1 -1
  24. massgen/configs/tools/custom_tools/gemini_custom_tool_with_mcp_example.yaml +1 -0
  25. massgen/configs/tools/custom_tools/gpt5_nano_custom_tool_with_mcp_example.yaml +1 -0
  26. massgen/configs/tools/custom_tools/gpt_oss_custom_tool_with_mcp_example.yaml +1 -0
  27. massgen/configs/tools/custom_tools/grok3_mini_custom_tool_with_mcp_example.yaml +1 -0
  28. massgen/configs/tools/custom_tools/interop/ag2_and_langgraph_lesson_planner.yaml +65 -0
  29. massgen/configs/tools/custom_tools/interop/ag2_and_openai_assistant_lesson_planner.yaml +65 -0
  30. massgen/configs/tools/custom_tools/interop/ag2_lesson_planner_example.yaml +48 -0
  31. massgen/configs/tools/custom_tools/interop/agentscope_lesson_planner_example.yaml +48 -0
  32. massgen/configs/tools/custom_tools/interop/langgraph_lesson_planner_example.yaml +49 -0
  33. massgen/configs/tools/custom_tools/interop/openai_assistant_lesson_planner_example.yaml +50 -0
  34. massgen/configs/tools/custom_tools/interop/smolagent_lesson_planner_example.yaml +49 -0
  35. massgen/configs/tools/custom_tools/qwen_api_custom_tool_with_mcp_example.yaml +1 -0
  36. massgen/configs/tools/custom_tools/two_models_with_tools_example.yaml +44 -0
  37. massgen/formatter/_gemini_formatter.py +61 -15
  38. massgen/memory/README.md +277 -0
  39. massgen/memory/__init__.py +26 -0
  40. massgen/memory/_base.py +193 -0
  41. massgen/memory/_compression.py +237 -0
  42. massgen/memory/_context_monitor.py +211 -0
  43. massgen/memory/_conversation.py +255 -0
  44. massgen/memory/_fact_extraction_prompts.py +333 -0
  45. massgen/memory/_mem0_adapters.py +257 -0
  46. massgen/memory/_persistent.py +687 -0
  47. massgen/memory/docker-compose.qdrant.yml +36 -0
  48. massgen/memory/docs/DESIGN.md +388 -0
  49. massgen/memory/docs/QUICKSTART.md +409 -0
  50. massgen/memory/docs/SUMMARY.md +319 -0
  51. massgen/memory/docs/agent_use_memory.md +408 -0
  52. massgen/memory/docs/orchestrator_use_memory.md +586 -0
  53. massgen/memory/examples.py +237 -0
  54. massgen/orchestrator.py +207 -7
  55. massgen/tests/memory/test_agent_compression.py +174 -0
  56. massgen/tests/memory/test_context_window_management.py +286 -0
  57. massgen/tests/memory/test_force_compression.py +154 -0
  58. massgen/tests/memory/test_simple_compression.py +147 -0
  59. massgen/tests/test_ag2_lesson_planner.py +223 -0
  60. massgen/tests/test_agent_memory.py +534 -0
  61. massgen/tests/test_config_validator.py +1156 -0
  62. massgen/tests/test_conversation_memory.py +382 -0
  63. massgen/tests/test_langgraph_lesson_planner.py +223 -0
  64. massgen/tests/test_orchestrator_memory.py +620 -0
  65. massgen/tests/test_persistent_memory.py +435 -0
  66. massgen/token_manager/token_manager.py +6 -0
  67. massgen/tool/__init__.py +2 -9
  68. massgen/tool/_decorators.py +52 -0
  69. massgen/tool/_extraframework_agents/ag2_lesson_planner_tool.py +251 -0
  70. massgen/tool/_extraframework_agents/agentscope_lesson_planner_tool.py +303 -0
  71. massgen/tool/_extraframework_agents/langgraph_lesson_planner_tool.py +275 -0
  72. massgen/tool/_extraframework_agents/openai_assistant_lesson_planner_tool.py +247 -0
  73. massgen/tool/_extraframework_agents/smolagent_lesson_planner_tool.py +180 -0
  74. massgen/tool/_manager.py +102 -16
  75. massgen/tool/_registered_tool.py +3 -0
  76. massgen/tool/_result.py +3 -0
  77. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/METADATA +138 -77
  78. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/RECORD +82 -37
  79. massgen/backend/gemini_mcp_manager.py +0 -545
  80. massgen/backend/gemini_trackers.py +0 -344
  81. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/WHEEL +0 -0
  82. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/entry_points.txt +0 -0
  83. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/licenses/LICENSE +0 -0
  84. {massgen-0.1.4.dist-info → massgen-0.1.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1156 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Tests for configuration validator.
4
+
5
+ Tests cover:
6
+ - Valid configs (should pass)
7
+ - Missing required fields
8
+ - Invalid types and values
9
+ - Backend-specific validation
10
+ - V1 config rejection
11
+ - Warning generation
12
+ - Error reporting format
13
+ """
14
+
15
+ import json
16
+ import tempfile
17
+ from pathlib import Path
18
+
19
+ import yaml
20
+
21
+ from massgen.config_validator import ConfigValidator, ValidationResult
22
+
23
+
24
+ class TestConfigValidator:
25
+ """Test suite for ConfigValidator."""
26
+
27
+ def test_valid_single_agent_config(self):
28
+ """Test validation of a valid single agent config."""
29
+ config = {
30
+ "agent": {
31
+ "id": "test-agent",
32
+ "backend": {"type": "openai", "model": "gpt-4o"},
33
+ },
34
+ }
35
+
36
+ validator = ConfigValidator()
37
+ result = validator.validate_config(config)
38
+
39
+ assert result.is_valid()
40
+ assert not result.has_errors()
41
+
42
+ def test_valid_multi_agent_config(self):
43
+ """Test validation of a valid multi-agent config."""
44
+ config = {
45
+ "agents": [
46
+ {
47
+ "id": "agent-1",
48
+ "backend": {"type": "openai", "model": "gpt-4o"},
49
+ },
50
+ {
51
+ "id": "agent-2",
52
+ "backend": {"type": "claude", "model": "claude-sonnet-4-5-20250929"},
53
+ },
54
+ ],
55
+ }
56
+
57
+ validator = ConfigValidator()
58
+ result = validator.validate_config(config)
59
+
60
+ assert result.is_valid()
61
+ assert not result.has_errors()
62
+
63
+ def test_valid_config_with_orchestrator(self):
64
+ """Test validation of config with orchestrator settings."""
65
+ config = {
66
+ "agents": [
67
+ {
68
+ "id": "agent-1",
69
+ "backend": {"type": "openai", "model": "gpt-4o"},
70
+ },
71
+ ],
72
+ "orchestrator": {
73
+ "voting_sensitivity": "balanced",
74
+ "answer_novelty_requirement": "strict",
75
+ "coordination": {
76
+ "enable_planning_mode": True,
77
+ "max_orchestration_restarts": 2,
78
+ },
79
+ "timeout": {
80
+ "orchestrator_timeout_seconds": 1800,
81
+ },
82
+ },
83
+ }
84
+
85
+ validator = ConfigValidator()
86
+ result = validator.validate_config(config)
87
+
88
+ assert result.is_valid()
89
+ assert not result.has_errors()
90
+
91
+ def test_valid_config_with_ui(self):
92
+ """Test validation of config with UI settings."""
93
+ config = {
94
+ "agent": {
95
+ "id": "test-agent",
96
+ "backend": {"type": "openai", "model": "gpt-4o"},
97
+ },
98
+ "ui": {
99
+ "display_type": "rich_terminal",
100
+ "logging_enabled": True,
101
+ },
102
+ }
103
+
104
+ validator = ConfigValidator()
105
+ result = validator.validate_config(config)
106
+
107
+ assert result.is_valid()
108
+ assert not result.has_errors()
109
+
110
+ def test_v1_config_rejected(self):
111
+ """Test that V1 configs are rejected with helpful error."""
112
+ config = {
113
+ "models": ["gpt-4o", "claude-3-opus"],
114
+ "num_agents": 2,
115
+ }
116
+
117
+ validator = ConfigValidator()
118
+ result = validator.validate_config(config)
119
+
120
+ assert not result.is_valid()
121
+ assert result.has_errors()
122
+ assert any("V1 config format detected" in error.message for error in result.errors)
123
+ assert any("migrate" in error.suggestion.lower() for error in result.errors if error.suggestion)
124
+
125
+ def test_missing_agents_field(self):
126
+ """Test error when neither 'agents' nor 'agent' is present."""
127
+ config = {
128
+ "orchestrator": {},
129
+ }
130
+
131
+ validator = ConfigValidator()
132
+ result = validator.validate_config(config)
133
+
134
+ assert not result.is_valid()
135
+ assert any("must have either 'agents'" in error.message for error in result.errors)
136
+
137
+ def test_both_agents_and_agent(self):
138
+ """Test error when both 'agents' and 'agent' are present."""
139
+ config = {
140
+ "agents": [{"id": "a1", "backend": {"type": "openai", "model": "gpt-4o"}}],
141
+ "agent": {"id": "a2", "backend": {"type": "openai", "model": "gpt-4o"}},
142
+ }
143
+
144
+ validator = ConfigValidator()
145
+ result = validator.validate_config(config)
146
+
147
+ assert not result.is_valid()
148
+ assert any("cannot have both 'agents' and 'agent'" in error.message for error in result.errors)
149
+
150
+ def test_missing_agent_id(self):
151
+ """Test error when agent is missing required 'id' field."""
152
+ config = {
153
+ "agent": {
154
+ "backend": {"type": "openai", "model": "gpt-4o"},
155
+ },
156
+ }
157
+
158
+ validator = ConfigValidator()
159
+ result = validator.validate_config(config)
160
+
161
+ assert not result.is_valid()
162
+ assert any("missing required field 'id'" in error.message for error in result.errors)
163
+
164
+ def test_missing_backend(self):
165
+ """Test error when agent is missing required 'backend' field."""
166
+ config = {
167
+ "agent": {
168
+ "id": "test-agent",
169
+ },
170
+ }
171
+
172
+ validator = ConfigValidator()
173
+ result = validator.validate_config(config)
174
+
175
+ assert not result.is_valid()
176
+ assert any("missing required field 'backend'" in error.message for error in result.errors)
177
+
178
+ def test_duplicate_agent_ids(self):
179
+ """Test error when multiple agents have the same ID."""
180
+ config = {
181
+ "agents": [
182
+ {"id": "agent-1", "backend": {"type": "openai", "model": "gpt-4o"}},
183
+ {"id": "agent-1", "backend": {"type": "claude", "model": "claude-sonnet-4-5-20250929"}},
184
+ ],
185
+ }
186
+
187
+ validator = ConfigValidator()
188
+ result = validator.validate_config(config)
189
+
190
+ assert not result.is_valid()
191
+ assert any("Duplicate agent ID" in error.message for error in result.errors)
192
+
193
+ def test_missing_backend_type(self):
194
+ """Test error when backend is missing required 'type' field."""
195
+ config = {
196
+ "agent": {
197
+ "id": "test-agent",
198
+ "backend": {"model": "gpt-4o"},
199
+ },
200
+ }
201
+
202
+ validator = ConfigValidator()
203
+ result = validator.validate_config(config)
204
+
205
+ assert not result.is_valid()
206
+ assert any("missing required field 'type'" in error.message for error in result.errors)
207
+
208
+ def test_missing_backend_model(self):
209
+ """Test error when backend is missing required 'model' field."""
210
+ config = {
211
+ "agent": {
212
+ "id": "test-agent",
213
+ "backend": {"type": "chatcompletion"}, # Uses default_model="custom", requires explicit model
214
+ },
215
+ }
216
+
217
+ validator = ConfigValidator()
218
+ result = validator.validate_config(config)
219
+
220
+ assert not result.is_valid()
221
+ assert any("missing required field 'model'" in error.message for error in result.errors)
222
+
223
+ def test_unknown_backend_type(self):
224
+ """Test error when backend type is not recognized."""
225
+ config = {
226
+ "agent": {
227
+ "id": "test-agent",
228
+ "backend": {"type": "unknown_backend", "model": "some-model"},
229
+ },
230
+ }
231
+
232
+ validator = ConfigValidator()
233
+ result = validator.validate_config(config)
234
+
235
+ assert not result.is_valid()
236
+ assert any("Unknown backend type" in error.message for error in result.errors)
237
+
238
+ def test_invalid_permission_mode(self):
239
+ """Test error when permission_mode has invalid value."""
240
+ config = {
241
+ "agent": {
242
+ "id": "test-agent",
243
+ "backend": {
244
+ "type": "claude_code",
245
+ "model": "claude-sonnet-4-5-20250929",
246
+ "permission_mode": "invalid_mode",
247
+ },
248
+ },
249
+ }
250
+
251
+ validator = ConfigValidator()
252
+ result = validator.validate_config(config)
253
+
254
+ assert not result.is_valid()
255
+ assert any("Invalid permission_mode" in error.message for error in result.errors)
256
+
257
+ def test_backend_capability_validation(self):
258
+ """Test that backend capabilities are validated."""
259
+ config = {
260
+ "agent": {
261
+ "id": "test-agent",
262
+ "backend": {
263
+ "type": "lmstudio", # lmstudio doesn't support web_search
264
+ "model": "custom",
265
+ "enable_web_search": True,
266
+ },
267
+ },
268
+ }
269
+
270
+ validator = ConfigValidator()
271
+ result = validator.validate_config(config)
272
+
273
+ assert not result.is_valid()
274
+ assert any("does not support" in error.message for error in result.errors)
275
+
276
+ def test_invalid_display_type(self):
277
+ """Test error when UI display_type is invalid."""
278
+ config = {
279
+ "agent": {
280
+ "id": "test-agent",
281
+ "backend": {"type": "openai", "model": "gpt-4o"},
282
+ },
283
+ "ui": {
284
+ "display_type": "invalid_type",
285
+ },
286
+ }
287
+
288
+ validator = ConfigValidator()
289
+ result = validator.validate_config(config)
290
+
291
+ assert not result.is_valid()
292
+ assert any("Invalid display_type" in error.message for error in result.errors)
293
+
294
+ def test_invalid_voting_sensitivity(self):
295
+ """Test error when voting_sensitivity is invalid."""
296
+ config = {
297
+ "agents": [
298
+ {"id": "agent-1", "backend": {"type": "openai", "model": "gpt-4o"}},
299
+ ],
300
+ "orchestrator": {
301
+ "voting_sensitivity": "invalid_value",
302
+ },
303
+ }
304
+
305
+ validator = ConfigValidator()
306
+ result = validator.validate_config(config)
307
+
308
+ assert not result.is_valid()
309
+ assert any("Invalid voting_sensitivity" in error.message for error in result.errors)
310
+
311
+ def test_invalid_context_path_permission(self):
312
+ """Test error when context_paths permission is invalid."""
313
+ config = {
314
+ "agents": [
315
+ {"id": "agent-1", "backend": {"type": "openai", "model": "gpt-4o"}},
316
+ ],
317
+ "orchestrator": {
318
+ "context_paths": [
319
+ {"path": "/some/path", "permission": "invalid_permission"},
320
+ ],
321
+ },
322
+ }
323
+
324
+ validator = ConfigValidator()
325
+ result = validator.validate_config(config)
326
+
327
+ assert not result.is_valid()
328
+ assert any("Invalid permission" in error.message for error in result.errors)
329
+
330
+ def test_warning_both_allowed_and_exclude_tools(self):
331
+ """Test warning when both allowed_tools and exclude_tools are used."""
332
+ config = {
333
+ "agent": {
334
+ "id": "test-agent",
335
+ "backend": {
336
+ "type": "claude_code",
337
+ "model": "claude-sonnet-4-5-20250929",
338
+ "allowed_tools": ["Read", "Write"],
339
+ "exclude_tools": ["Bash"],
340
+ },
341
+ },
342
+ }
343
+
344
+ validator = ConfigValidator()
345
+ result = validator.validate_config(config)
346
+
347
+ assert result.is_valid() # No errors
348
+ assert result.has_warnings()
349
+ assert any("both 'allowed_tools' and 'exclude_tools'" in warning.message for warning in result.warnings)
350
+
351
+ def test_no_warning_missing_system_message(self):
352
+ """Test that missing system_message doesn't generate a warning."""
353
+ config = {
354
+ "agent": {
355
+ "id": "test-agent",
356
+ "backend": {"type": "openai", "model": "gpt-4o"},
357
+ },
358
+ }
359
+
360
+ validator = ConfigValidator()
361
+ result = validator.validate_config(config)
362
+
363
+ assert result.is_valid()
364
+ # Should not have warnings about missing system_message
365
+ assert not any("system_message" in warning.message.lower() for warning in result.warnings)
366
+
367
+ def test_no_warning_multi_agent_no_orchestrator(self):
368
+ """Test that multi-agent setup without orchestrator doesn't generate a warning."""
369
+ config = {
370
+ "agents": [
371
+ {"id": "agent-1", "backend": {"type": "openai", "model": "gpt-4o"}},
372
+ {"id": "agent-2", "backend": {"type": "claude", "model": "claude-sonnet-4-5-20250929"}},
373
+ ],
374
+ }
375
+
376
+ validator = ConfigValidator()
377
+ result = validator.validate_config(config)
378
+
379
+ assert result.is_valid()
380
+ # Should not have warnings about missing orchestrator
381
+ assert not any("orchestrator" in warning.message.lower() for warning in result.warnings)
382
+
383
+ def test_invalid_type_field_types(self):
384
+ """Test errors for wrong field types."""
385
+ config = {
386
+ "agent": {
387
+ "id": 123, # Should be string
388
+ "backend": {
389
+ "type": "openai",
390
+ "model": "gpt-4o",
391
+ "enable_web_search": "yes", # Should be boolean
392
+ },
393
+ "system_message": ["not", "a", "string"], # Should be string
394
+ },
395
+ }
396
+
397
+ validator = ConfigValidator()
398
+ result = validator.validate_config(config)
399
+
400
+ assert not result.is_valid()
401
+ assert len(result.errors) >= 2 # Multiple type errors
402
+
403
+ def test_validate_file_not_found(self):
404
+ """Test validation of non-existent file."""
405
+ validator = ConfigValidator()
406
+ result = validator.validate_config_file("/nonexistent/config.yaml")
407
+
408
+ assert not result.is_valid()
409
+ assert any("Config file not found" in error.message for error in result.errors)
410
+
411
+ def test_validate_yaml_file(self):
412
+ """Test validation of a YAML file."""
413
+ config = {
414
+ "agent": {
415
+ "id": "test-agent",
416
+ "backend": {"type": "openai", "model": "gpt-4o"},
417
+ },
418
+ }
419
+
420
+ # Create temporary YAML file
421
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
422
+ yaml.dump(config, f)
423
+ temp_path = f.name
424
+
425
+ try:
426
+ validator = ConfigValidator()
427
+ result = validator.validate_config_file(temp_path)
428
+
429
+ assert result.is_valid()
430
+ assert not result.has_errors()
431
+ finally:
432
+ Path(temp_path).unlink()
433
+
434
+ def test_validate_json_file(self):
435
+ """Test validation of a JSON file."""
436
+ config = {
437
+ "agent": {
438
+ "id": "test-agent",
439
+ "backend": {"type": "openai", "model": "gpt-4o"},
440
+ },
441
+ }
442
+
443
+ # Create temporary JSON file
444
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
445
+ json.dump(config, f)
446
+ temp_path = f.name
447
+
448
+ try:
449
+ validator = ConfigValidator()
450
+ result = validator.validate_config_file(temp_path)
451
+
452
+ assert result.is_valid()
453
+ assert not result.has_errors()
454
+ finally:
455
+ Path(temp_path).unlink()
456
+
457
+ def test_validation_result_to_dict(self):
458
+ """Test conversion of ValidationResult to dict."""
459
+ result = ValidationResult()
460
+ result.add_error("Test error", "test.location", "Test suggestion")
461
+ result.add_warning("Test warning", "test.location", "Test suggestion")
462
+
463
+ result_dict = result.to_dict()
464
+
465
+ assert result_dict["valid"] is False
466
+ assert result_dict["error_count"] == 1
467
+ assert result_dict["warning_count"] == 1
468
+ assert len(result_dict["errors"]) == 1
469
+ assert len(result_dict["warnings"]) == 1
470
+ assert result_dict["errors"][0]["message"] == "Test error"
471
+ assert result_dict["warnings"][0]["message"] == "Test warning"
472
+
473
+ def test_validation_result_format_errors(self):
474
+ """Test error formatting."""
475
+ result = ValidationResult()
476
+ result.add_error("Test error message", "config.agent.backend", "Use correct type")
477
+
478
+ formatted = result.format_errors()
479
+
480
+ assert "Configuration Errors Found" in formatted
481
+ assert "Test error message" in formatted
482
+ assert "config.agent.backend" in formatted
483
+ assert "Use correct type" in formatted
484
+
485
+ def test_validation_result_format_warnings(self):
486
+ """Test warning formatting."""
487
+ result = ValidationResult()
488
+ result.add_warning("Test warning message", "config.agent", "Add system_message")
489
+
490
+ formatted = result.format_warnings()
491
+
492
+ assert "Configuration Warnings" in formatted
493
+ assert "Test warning message" in formatted
494
+ assert "config.agent" in formatted
495
+ assert "Add system_message" in formatted
496
+
497
+ def test_tool_filtering_validation(self):
498
+ """Test validation of tool filtering lists."""
499
+ config = {
500
+ "agent": {
501
+ "id": "test-agent",
502
+ "backend": {
503
+ "type": "claude_code",
504
+ "model": "claude-sonnet-4-5-20250929",
505
+ "allowed_tools": "not-a-list", # Should be list
506
+ },
507
+ },
508
+ }
509
+
510
+ validator = ConfigValidator()
511
+ result = validator.validate_config(config)
512
+
513
+ assert not result.is_valid()
514
+ assert any("'allowed_tools' must be a list" in error.message for error in result.errors)
515
+
516
+ def test_mcp_servers_validation(self):
517
+ """Test that MCP server configs are validated."""
518
+ config = {
519
+ "agent": {
520
+ "id": "test-agent",
521
+ "backend": {
522
+ "type": "claude",
523
+ "model": "claude-sonnet-4-5-20250929",
524
+ "mcp_servers": "invalid-format", # Should trigger MCP validator
525
+ },
526
+ },
527
+ }
528
+
529
+ validator = ConfigValidator()
530
+ result = validator.validate_config(config)
531
+
532
+ # Should have error from MCP validation
533
+ assert not result.is_valid()
534
+
535
+ def test_complex_valid_config(self):
536
+ """Test a complex but valid configuration."""
537
+ config = {
538
+ "agents": [
539
+ {
540
+ "id": "researcher",
541
+ "backend": {
542
+ "type": "openai",
543
+ "model": "gpt-4o",
544
+ "enable_web_search": True,
545
+ },
546
+ "system_message": "You are a research assistant.",
547
+ },
548
+ {
549
+ "id": "analyst",
550
+ "backend": {
551
+ "type": "claude",
552
+ "model": "claude-sonnet-4-5-20250929",
553
+ "enable_code_execution": True,
554
+ },
555
+ "system_message": "You are a data analyst.",
556
+ },
557
+ ],
558
+ "orchestrator": {
559
+ "voting_sensitivity": "balanced",
560
+ "answer_novelty_requirement": "lenient",
561
+ "coordination": {
562
+ "enable_planning_mode": False,
563
+ "max_orchestration_restarts": 1,
564
+ },
565
+ "context_paths": [
566
+ {"path": "/data", "permission": "read"},
567
+ {"path": "/output", "permission": "write"},
568
+ ],
569
+ "timeout": {
570
+ "orchestrator_timeout_seconds": 3600,
571
+ },
572
+ },
573
+ "ui": {
574
+ "display_type": "rich_terminal",
575
+ "logging_enabled": True,
576
+ },
577
+ }
578
+
579
+ validator = ConfigValidator()
580
+ result = validator.validate_config(config)
581
+
582
+ assert result.is_valid()
583
+ # May have warnings but should have no errors
584
+ assert not result.has_errors()
585
+
586
+
587
+ class TestCommonBadConfigs:
588
+ """Test suite for common configuration mistakes users might make."""
589
+
590
+ def test_v1_config_with_models_list(self):
591
+ """Test V1 config with models list is rejected."""
592
+ config = {
593
+ "models": ["gpt-4o", "claude-3-opus"],
594
+ "num_agents": 2,
595
+ }
596
+
597
+ validator = ConfigValidator()
598
+ result = validator.validate_config(config)
599
+
600
+ assert not result.is_valid()
601
+ assert any("V1 config format detected" in error.message for error in result.errors)
602
+ assert any("migrate" in error.suggestion.lower() for error in result.errors if error.suggestion)
603
+
604
+ def test_v1_config_with_model_configs(self):
605
+ """Test V1 config with model_configs is rejected."""
606
+ config = {
607
+ "model_configs": {
608
+ "gpt-4o": {"temperature": 0.7},
609
+ "claude-3-opus": {"temperature": 0.5},
610
+ },
611
+ "agents": [{"id": "test"}], # Even with agents present
612
+ }
613
+
614
+ validator = ConfigValidator()
615
+ result = validator.validate_config(config)
616
+
617
+ assert not result.is_valid()
618
+ assert any("V1 config" in error.message for error in result.errors)
619
+
620
+ def test_missing_both_agents_and_agent(self):
621
+ """Test config without agents or agent field."""
622
+ config = {
623
+ "orchestrator": {},
624
+ "ui": {"display_type": "simple"},
625
+ }
626
+
627
+ validator = ConfigValidator()
628
+ result = validator.validate_config(config)
629
+
630
+ assert not result.is_valid()
631
+ assert any("must have either 'agents'" in error.message for error in result.errors)
632
+
633
+ def test_typo_in_backend_type(self):
634
+ """Test common typo in backend type."""
635
+ config = {
636
+ "agent": {
637
+ "id": "test-agent",
638
+ "backend": {
639
+ "type": "openi", # Common typo
640
+ "model": "gpt-4o",
641
+ },
642
+ },
643
+ }
644
+
645
+ validator = ConfigValidator()
646
+ result = validator.validate_config(config)
647
+
648
+ assert not result.is_valid()
649
+ assert any("Unknown backend type: 'openi'" in error.message for error in result.errors)
650
+ assert any("openai" in error.suggestion for error in result.errors if error.suggestion)
651
+
652
+ def test_wrong_case_backend_type(self):
653
+ """Test wrong case in backend type."""
654
+ config = {
655
+ "agent": {
656
+ "id": "test-agent",
657
+ "backend": {
658
+ "type": "OpenAI", # Should be lowercase
659
+ "model": "gpt-4o",
660
+ },
661
+ },
662
+ }
663
+
664
+ validator = ConfigValidator()
665
+ result = validator.validate_config(config)
666
+
667
+ assert not result.is_valid()
668
+ assert any("Unknown backend type" in error.message for error in result.errors)
669
+
670
+ def test_unsupported_feature_for_backend(self):
671
+ """Test requesting unsupported feature from backend."""
672
+ config = {
673
+ "agent": {
674
+ "id": "test-agent",
675
+ "backend": {
676
+ "type": "lmstudio",
677
+ "model": "custom",
678
+ "enable_web_search": True, # lmstudio doesn't support this
679
+ },
680
+ },
681
+ }
682
+
683
+ validator = ConfigValidator()
684
+ result = validator.validate_config(config)
685
+
686
+ assert not result.is_valid()
687
+ assert any("does not support web_search" in error.message for error in result.errors)
688
+
689
+ def test_boolean_as_string(self):
690
+ """Test using string instead of boolean."""
691
+ config = {
692
+ "agent": {
693
+ "id": "test-agent",
694
+ "backend": {
695
+ "type": "openai",
696
+ "model": "gpt-4o",
697
+ "enable_web_search": "true", # Should be boolean true
698
+ },
699
+ },
700
+ }
701
+
702
+ validator = ConfigValidator()
703
+ result = validator.validate_config(config)
704
+
705
+ assert not result.is_valid()
706
+ assert any("must be a boolean" in error.message for error in result.errors)
707
+
708
+ def test_number_as_string(self):
709
+ """Test using string for numeric field."""
710
+ config = {
711
+ "agents": [
712
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
713
+ ],
714
+ "orchestrator": {
715
+ "timeout": {
716
+ "orchestrator_timeout_seconds": "1800", # Should be number
717
+ },
718
+ },
719
+ }
720
+
721
+ validator = ConfigValidator()
722
+ result = validator.validate_config(config)
723
+
724
+ assert not result.is_valid()
725
+ assert any("must be a positive number" in error.message for error in result.errors)
726
+
727
+ def test_invalid_display_type_typo(self):
728
+ """Test typo in display_type."""
729
+ config = {
730
+ "agent": {
731
+ "id": "test-agent",
732
+ "backend": {"type": "openai", "model": "gpt-4o"},
733
+ },
734
+ "ui": {
735
+ "display_type": "detailed", # Not a valid type
736
+ },
737
+ }
738
+
739
+ validator = ConfigValidator()
740
+ result = validator.validate_config(config)
741
+
742
+ assert not result.is_valid()
743
+ assert any("Invalid display_type" in error.message for error in result.errors)
744
+ assert any("rich_terminal" in error.suggestion for error in result.errors if error.suggestion)
745
+
746
+ def test_invalid_permission_mode(self):
747
+ """Test invalid permission_mode value."""
748
+ config = {
749
+ "agent": {
750
+ "id": "test-agent",
751
+ "backend": {
752
+ "type": "claude_code",
753
+ "model": "claude-sonnet-4-5-20250929",
754
+ "permission_mode": "auto", # Not valid
755
+ },
756
+ },
757
+ }
758
+
759
+ validator = ConfigValidator()
760
+ result = validator.validate_config(config)
761
+
762
+ assert not result.is_valid()
763
+ assert any("Invalid permission_mode" in error.message for error in result.errors)
764
+
765
+ def test_context_path_wrong_permission(self):
766
+ """Test wrong permission value in context_paths."""
767
+ config = {
768
+ "agents": [
769
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
770
+ ],
771
+ "orchestrator": {
772
+ "context_paths": [
773
+ {"path": "/data", "permission": "readonly"}, # Should be "read"
774
+ ],
775
+ },
776
+ }
777
+
778
+ validator = ConfigValidator()
779
+ result = validator.validate_config(config)
780
+
781
+ assert not result.is_valid()
782
+ assert any("Invalid permission" in error.message for error in result.errors)
783
+ assert any("'read' or 'write'" in error.suggestion for error in result.errors if error.suggestion)
784
+
785
+ def test_negative_timeout(self):
786
+ """Test negative timeout value."""
787
+ config = {
788
+ "agents": [
789
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
790
+ ],
791
+ "orchestrator": {
792
+ "timeout": {
793
+ "orchestrator_timeout_seconds": -100, # Negative
794
+ },
795
+ },
796
+ }
797
+
798
+ validator = ConfigValidator()
799
+ result = validator.validate_config(config)
800
+
801
+ assert not result.is_valid()
802
+ assert any("must be a positive number" in error.message for error in result.errors)
803
+
804
+ def test_negative_max_restarts(self):
805
+ """Test negative max_orchestration_restarts."""
806
+ config = {
807
+ "agents": [
808
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
809
+ ],
810
+ "orchestrator": {
811
+ "coordination": {
812
+ "max_orchestration_restarts": -1, # Negative
813
+ },
814
+ },
815
+ }
816
+
817
+ validator = ConfigValidator()
818
+ result = validator.validate_config(config)
819
+
820
+ assert not result.is_valid()
821
+ assert any("must be a non-negative integer" in error.message for error in result.errors)
822
+
823
+ def test_agent_without_id(self):
824
+ """Test agent missing id field (common mistake)."""
825
+ config = {
826
+ "agents": [
827
+ {
828
+ # Missing id
829
+ "backend": {"type": "openai", "model": "gpt-4o"},
830
+ },
831
+ ],
832
+ }
833
+
834
+ validator = ConfigValidator()
835
+ result = validator.validate_config(config)
836
+
837
+ assert not result.is_valid()
838
+ assert any("missing required field 'id'" in error.message for error in result.errors)
839
+
840
+ def test_agent_without_backend(self):
841
+ """Test agent missing backend field (common mistake)."""
842
+ config = {
843
+ "agents": [
844
+ {
845
+ "id": "test-agent",
846
+ # Missing backend
847
+ },
848
+ ],
849
+ }
850
+
851
+ validator = ConfigValidator()
852
+ result = validator.validate_config(config)
853
+
854
+ assert not result.is_valid()
855
+ assert any("missing required field 'backend'" in error.message for error in result.errors)
856
+
857
+ def test_tools_list_with_non_strings(self):
858
+ """Test tool filtering with non-string values."""
859
+ config = {
860
+ "agent": {
861
+ "id": "test-agent",
862
+ "backend": {
863
+ "type": "claude_code",
864
+ "model": "claude-sonnet-4-5-20250929",
865
+ "allowed_tools": ["Read", 123, "Write"], # 123 is not a string
866
+ },
867
+ },
868
+ }
869
+
870
+ validator = ConfigValidator()
871
+ result = validator.validate_config(config)
872
+
873
+ assert not result.is_valid()
874
+ assert any("must be a string" in error.message for error in result.errors)
875
+
876
+ def test_mcp_servers_wrong_type(self):
877
+ """Test mcp_servers as wrong type."""
878
+ config = {
879
+ "agent": {
880
+ "id": "test-agent",
881
+ "backend": {
882
+ "type": "claude",
883
+ "model": "claude-sonnet-4-5-20250929",
884
+ "mcp_servers": "filesystem", # Should be list or dict
885
+ },
886
+ },
887
+ }
888
+
889
+ validator = ConfigValidator()
890
+ result = validator.validate_config(config)
891
+
892
+ assert not result.is_valid()
893
+ assert any("MCP configuration error" in error.message for error in result.errors)
894
+
895
+ def test_invalid_voting_sensitivity(self):
896
+ """Test invalid voting_sensitivity value."""
897
+ config = {
898
+ "agents": [
899
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
900
+ ],
901
+ "orchestrator": {
902
+ "voting_sensitivity": "medium", # Should be lenient/balanced/strict
903
+ },
904
+ }
905
+
906
+ validator = ConfigValidator()
907
+ result = validator.validate_config(config)
908
+
909
+ assert not result.is_valid()
910
+ assert any("Invalid voting_sensitivity" in error.message for error in result.errors)
911
+
912
+ def test_invalid_answer_novelty(self):
913
+ """Test invalid answer_novelty_requirement value."""
914
+ config = {
915
+ "agents": [
916
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
917
+ ],
918
+ "orchestrator": {
919
+ "answer_novelty_requirement": "high", # Should be lenient/balanced/strict
920
+ },
921
+ }
922
+
923
+ validator = ConfigValidator()
924
+ result = validator.validate_config(config)
925
+
926
+ assert not result.is_valid()
927
+ assert any("Invalid answer_novelty_requirement" in error.message for error in result.errors)
928
+
929
+ def test_v1_max_rounds(self):
930
+ """Test V1 max_rounds parameter is rejected."""
931
+ config = {
932
+ "agents": [
933
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
934
+ ],
935
+ "max_rounds": 5, # V1 parameter
936
+ }
937
+
938
+ validator = ConfigValidator()
939
+ result = validator.validate_config(config)
940
+
941
+ assert not result.is_valid()
942
+ assert any("V1 config format detected" in error.message for error in result.errors)
943
+ assert any("max_rounds" in error.message for error in result.errors)
944
+
945
+ def test_v1_consensus_threshold(self):
946
+ """Test V1 consensus_threshold parameter is rejected."""
947
+ config = {
948
+ "agents": [
949
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
950
+ ],
951
+ "consensus_threshold": 0.6, # V1 parameter
952
+ }
953
+
954
+ validator = ConfigValidator()
955
+ result = validator.validate_config(config)
956
+
957
+ assert not result.is_valid()
958
+ assert any("V1 config format detected" in error.message for error in result.errors)
959
+ assert any("consensus_threshold" in error.message for error in result.errors)
960
+
961
+ def test_v1_voting_enabled(self):
962
+ """Test V1 voting_enabled parameter is rejected."""
963
+ config = {
964
+ "agents": [
965
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
966
+ ],
967
+ "voting_enabled": True, # V1 parameter
968
+ }
969
+
970
+ validator = ConfigValidator()
971
+ result = validator.validate_config(config)
972
+
973
+ assert not result.is_valid()
974
+ assert any("V1 config format detected" in error.message for error in result.errors)
975
+ assert any("voting_enabled" in error.message for error in result.errors)
976
+
977
+ def test_v1_multiple_keywords(self):
978
+ """Test config with multiple V1 keywords."""
979
+ config = {
980
+ "agents": [
981
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
982
+ ],
983
+ "max_rounds": 5,
984
+ "voting_enabled": True,
985
+ "consensus_threshold": 0.6,
986
+ }
987
+
988
+ validator = ConfigValidator()
989
+ result = validator.validate_config(config)
990
+
991
+ assert not result.is_valid()
992
+ assert any("V1 config format detected" in error.message for error in result.errors)
993
+ # Should mention all found V1 keywords
994
+ error_messages = " ".join([e.message for e in result.errors])
995
+ assert "max_rounds" in error_messages
996
+ assert "voting_enabled" in error_messages
997
+ assert "consensus_threshold" in error_messages
998
+
999
+
1000
+ class TestMemoryValidation:
1001
+ """Test suite for memory configuration validation."""
1002
+
1003
+ def test_valid_memory_config(self):
1004
+ """Test valid memory configuration."""
1005
+ config = {
1006
+ "agents": [
1007
+ {"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}},
1008
+ ],
1009
+ "memory": {
1010
+ "enabled": True,
1011
+ "conversation_memory": {
1012
+ "enabled": True,
1013
+ },
1014
+ "persistent_memory": {
1015
+ "enabled": True,
1016
+ "on_disk": True,
1017
+ "vector_store": "qdrant",
1018
+ "llm": {
1019
+ "provider": "openai",
1020
+ "model": "gpt-4.1-nano-2025-04-14",
1021
+ },
1022
+ "embedding": {
1023
+ "provider": "openai",
1024
+ "model": "text-embedding-3-small",
1025
+ },
1026
+ "qdrant": {
1027
+ "mode": "server",
1028
+ "host": "localhost",
1029
+ "port": 6333,
1030
+ },
1031
+ },
1032
+ "compression": {
1033
+ "trigger_threshold": 0.75,
1034
+ "target_ratio": 0.40,
1035
+ },
1036
+ "retrieval": {
1037
+ "limit": 10,
1038
+ "exclude_recent": True,
1039
+ },
1040
+ },
1041
+ }
1042
+
1043
+ validator = ConfigValidator()
1044
+ result = validator.validate_config(config)
1045
+
1046
+ assert result.is_valid()
1047
+ assert not result.has_errors()
1048
+
1049
+ def test_memory_enabled_wrong_type(self):
1050
+ """Test memory enabled with wrong type."""
1051
+ config = {
1052
+ "agents": [{"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}}],
1053
+ "memory": {
1054
+ "enabled": "yes", # Should be boolean
1055
+ },
1056
+ }
1057
+
1058
+ validator = ConfigValidator()
1059
+ result = validator.validate_config(config)
1060
+
1061
+ assert not result.is_valid()
1062
+ assert any("'enabled' must be a boolean" in error.message for error in result.errors)
1063
+
1064
+ def test_memory_qdrant_invalid_mode(self):
1065
+ """Test invalid qdrant mode."""
1066
+ config = {
1067
+ "agents": [{"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}}],
1068
+ "memory": {
1069
+ "persistent_memory": {
1070
+ "qdrant": {
1071
+ "mode": "distributed", # Should be 'server' or 'local'
1072
+ },
1073
+ },
1074
+ },
1075
+ }
1076
+
1077
+ validator = ConfigValidator()
1078
+ result = validator.validate_config(config)
1079
+
1080
+ assert not result.is_valid()
1081
+ assert any("Invalid qdrant mode" in error.message for error in result.errors)
1082
+ assert any("'server' or 'local'" in error.suggestion for error in result.errors if error.suggestion)
1083
+
1084
+ def test_memory_compression_out_of_range(self):
1085
+ """Test compression threshold out of valid range."""
1086
+ config = {
1087
+ "agents": [{"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}}],
1088
+ "memory": {
1089
+ "compression": {
1090
+ "trigger_threshold": 1.5, # Should be 0-1
1091
+ },
1092
+ },
1093
+ }
1094
+
1095
+ validator = ConfigValidator()
1096
+ result = validator.validate_config(config)
1097
+
1098
+ assert not result.is_valid()
1099
+ assert any("must be between 0 and 1" in error.message for error in result.errors)
1100
+
1101
+ def test_memory_retrieval_negative_limit(self):
1102
+ """Test negative retrieval limit."""
1103
+ config = {
1104
+ "agents": [{"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}}],
1105
+ "memory": {
1106
+ "retrieval": {
1107
+ "limit": -5, # Should be positive
1108
+ },
1109
+ },
1110
+ }
1111
+
1112
+ validator = ConfigValidator()
1113
+ result = validator.validate_config(config)
1114
+
1115
+ assert not result.is_valid()
1116
+ assert any("must be a positive integer" in error.message for error in result.errors)
1117
+
1118
+ def test_memory_qdrant_invalid_port(self):
1119
+ """Test invalid qdrant port."""
1120
+ config = {
1121
+ "agents": [{"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}}],
1122
+ "memory": {
1123
+ "persistent_memory": {
1124
+ "qdrant": {
1125
+ "mode": "server",
1126
+ "port": 99999, # Out of valid range
1127
+ },
1128
+ },
1129
+ },
1130
+ }
1131
+
1132
+ validator = ConfigValidator()
1133
+ result = validator.validate_config(config)
1134
+
1135
+ assert not result.is_valid()
1136
+ assert any("must be a valid port number" in error.message for error in result.errors)
1137
+
1138
+ def test_memory_llm_provider_wrong_type(self):
1139
+ """Test llm provider with wrong type."""
1140
+ config = {
1141
+ "agents": [{"id": "test", "backend": {"type": "openai", "model": "gpt-4o"}}],
1142
+ "memory": {
1143
+ "persistent_memory": {
1144
+ "llm": {
1145
+ "provider": 123, # Should be string
1146
+ "model": "gpt-4o",
1147
+ },
1148
+ },
1149
+ },
1150
+ }
1151
+
1152
+ validator = ConfigValidator()
1153
+ result = validator.validate_config(config)
1154
+
1155
+ assert not result.is_valid()
1156
+ assert any("must be a string" in error.message for error in result.errors)