sugarai 3.3.0__tar.gz → 3.3.1.dev1__tar.gz

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.
Files changed (102) hide show
  1. {sugarai-3.3.0/sugarai.egg-info → sugarai-3.3.1.dev1}/PKG-INFO +4 -4
  2. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/README.md +2 -2
  3. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/pyproject.toml +2 -2
  4. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/base.py +72 -56
  5. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/core/loop.py +20 -0
  6. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/agent_sdk_executor.py +8 -0
  7. {sugarai-3.3.0 → sugarai-3.3.1.dev1/sugarai.egg-info}/PKG-INFO +4 -4
  8. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/SOURCES.txt +1 -0
  9. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_agent.py +24 -1
  10. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_core_loop.py +48 -0
  11. sugarai-3.3.1.dev1/tests/test_mcp_integration.py +498 -0
  12. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/LICENSE +0 -0
  13. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/setup.cfg +0 -0
  14. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/__init__.py +0 -0
  15. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/__version__.py +0 -0
  16. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/__init__.py +0 -0
  17. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/hooks.py +0 -0
  18. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/subagent_manager.py +0 -0
  19. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/tools.py +0 -0
  20. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/__init__.py +0 -0
  21. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/api_keys.py +0 -0
  22. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/tiers.py +0 -0
  23. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/usage.py +0 -0
  24. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/config/__init__.py +0 -0
  25. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/config/issue_responder_config.py +0 -0
  26. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/config/prompt_config.py +0 -0
  27. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/core/__init__.py +0 -0
  28. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/__init__.py +0 -0
  29. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/code_quality.py +0 -0
  30. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/error_monitor.py +0 -0
  31. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/github_watcher.py +0 -0
  32. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/test_coverage.py +0 -0
  33. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/__init__.py +0 -0
  34. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/base.py +0 -0
  35. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/claude_wrapper.py +0 -0
  36. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/structured_request.py +0 -0
  37. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/integrations/__init__.py +0 -0
  38. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/integrations/github.py +0 -0
  39. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/learning/__init__.py +0 -0
  40. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/learning/adaptive_scheduler.py +0 -0
  41. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/learning/feedback_processor.py +0 -0
  42. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/main.py +0 -0
  43. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/mcp/__init__.py +0 -0
  44. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/mcp/server.py +0 -0
  45. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/orchestration/__init__.py +0 -0
  46. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/orchestration/agent_router.py +0 -0
  47. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/orchestration/task_orchestrator.py +0 -0
  48. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/__init__.py +0 -0
  49. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/base.py +0 -0
  50. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/default.py +0 -0
  51. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/issue_responder.py +0 -0
  52. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/__init__.py +0 -0
  53. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/coordinator.py +0 -0
  54. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/diff_validator.py +0 -0
  55. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/evidence.py +0 -0
  56. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/failure_handler.py +0 -0
  57. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/functional_verifier.py +0 -0
  58. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/preflight_checks.py +0 -0
  59. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/success_criteria.py +0 -0
  60. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/test_validator.py +0 -0
  61. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/truth_enforcer.py +0 -0
  62. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/__init__.py +0 -0
  63. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/config.py +0 -0
  64. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/profile.py +0 -0
  65. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/validator.py +0 -0
  66. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/__init__.py +0 -0
  67. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/issue_response_manager.py +0 -0
  68. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/task_type_manager.py +0 -0
  69. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/work_queue.py +0 -0
  70. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/__init__.py +0 -0
  71. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/capability_scanner.py +0 -0
  72. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/complexity_analyzer.py +0 -0
  73. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/triage_analyzer.py +0 -0
  74. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/utils/__init__.py +0 -0
  75. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/utils/git_operations.py +0 -0
  76. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/utils/toon_encoder.py +0 -0
  77. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/workflow/__init__.py +0 -0
  78. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/workflow/orchestrator.py +0 -0
  79. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/dependency_links.txt +0 -0
  80. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/entry_points.txt +0 -0
  81. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/requires.txt +0 -0
  82. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/top_level.txt +0 -0
  83. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_billing.py +0 -0
  84. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_cli.py +0 -0
  85. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_complexity_analyzer.py +0 -0
  86. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_github_client.py +0 -0
  87. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_goose_integration.py +0 -0
  88. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_hold_functionality.py +0 -0
  89. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_hooks.py +0 -0
  90. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_issue_responder_cli.py +0 -0
  91. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_issue_response_integration.py +0 -0
  92. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_mcp_server.py +0 -0
  93. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_orchestration.py +0 -0
  94. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_profiles.py +0 -0
  95. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_prompt_config.py +0 -0
  96. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_quality_gates.py +0 -0
  97. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_ralph.py +0 -0
  98. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_storage.py +0 -0
  99. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_subagent_manager.py +0 -0
  100. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_task_types.py +0 -0
  101. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_toon_encoder.py +0 -0
  102. {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_v3_benchmarks.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sugarai
3
- Version: 3.3.0
4
- Summary: Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background.
3
+ Version: 3.3.1.dev1
4
+ Summary: Sugar 🍰 - Vibe coding while you sleep. Autonomous AI development for Claude Code.
5
5
  Author-email: Steven Leggett <contact@roboticforce.io>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/roboticforce/sugar
@@ -58,9 +58,9 @@ Dynamic: license-file
58
58
 
59
59
  # Sugar 🍰
60
60
 
61
- A dev team that never stops.
61
+ Vibe coding while you sleep.
62
62
 
63
- Delegate full tasks to AI in the background. Sugar builds features, fixes bugs, and ships code while you focus on what matters.
63
+ Autonomous AI development for Claude Code. Sugar builds features, fixes bugs, and ships code while you focus on what matters.
64
64
 
65
65
  ## What It Does
66
66
 
@@ -1,8 +1,8 @@
1
1
  # Sugar 🍰
2
2
 
3
- A dev team that never stops.
3
+ Vibe coding while you sleep.
4
4
 
5
- Delegate full tasks to AI in the background. Sugar builds features, fixes bugs, and ships code while you focus on what matters.
5
+ Autonomous AI development for Claude Code. Sugar builds features, fixes bugs, and ships code while you focus on what matters.
6
6
 
7
7
  ## What It Does
8
8
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sugarai"
7
- version = "3.3.0"
8
- description = "Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background."
7
+ version = "3.3.1.dev1"
8
+ description = "Sugar 🍰 - Vibe coding while you sleep. Autonomous AI development for Claude Code."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  license = {text = "MIT"}
@@ -134,8 +134,8 @@ class SugarAgentConfig:
134
134
  # Working directory
135
135
  working_directory: Optional[str] = None
136
136
 
137
- # Timeout for operations (seconds)
138
- timeout: int = 300
137
+ # Timeout for operations (seconds) - default 2 hours for complex tasks
138
+ timeout: int = 7200
139
139
 
140
140
  # Retry settings for transient errors
141
141
  max_retries: int = 3
@@ -299,60 +299,76 @@ Guidelines:
299
299
  files_modified = []
300
300
 
301
301
  # Use the SDK's query() function which returns an async generator
302
- async for message in query(prompt=prompt, options=options):
303
- # Handle different message types from the SDK
304
- # The SDK may return dicts or typed objects depending on version
305
- if SDK_HAS_TYPES and isinstance(message, AssistantMessage):
306
- # Typed SDK - iterate through content blocks
307
- for block in message.content:
308
- if isinstance(block, TextBlock):
309
- content_parts.append(block.text)
310
- elif isinstance(block, ToolUseBlock):
311
- tool_use = {
312
- "tool": block.name,
313
- "input": block.input,
314
- }
315
- tool_uses.append(tool_use)
316
-
317
- # Track file modifications
318
- if block.name in ("Write", "Edit"):
319
- file_path = block.input.get("file_path")
320
- if file_path and file_path not in files_modified:
321
- files_modified.append(file_path)
322
-
323
- elif isinstance(message, dict):
324
- # Dict-based SDK response
325
- msg_type = message.get("type", "")
326
-
327
- if msg_type == "assistant":
328
- # Process content blocks
329
- content = message.get("content", [])
330
- for block in content:
331
- block_type = block.get("type", "")
332
- if block_type == "text":
333
- content_parts.append(block.get("text", ""))
334
- elif block_type == "tool_use":
335
- tool_use = {
336
- "tool": block.get("name", ""),
337
- "input": block.get("input", {}),
338
- }
339
- tool_uses.append(tool_use)
340
-
341
- # Track file modifications
342
- tool_name = block.get("name", "")
343
- if tool_name in ("Write", "Edit"):
344
- file_path = block.get("input", {}).get("file_path")
345
- if file_path and file_path not in files_modified:
346
- files_modified.append(file_path)
347
-
348
- elif msg_type == "text":
349
- # Direct text message
350
- content_parts.append(message.get("text", ""))
351
-
352
- elif msg_type == "result":
353
- # Final result message
354
- if message.get("content"):
355
- content_parts.append(str(message.get("content", "")))
302
+ # Wrap with timeout to prevent hanging if generator doesn't signal completion
303
+ try:
304
+ async with asyncio.timeout(self.config.timeout):
305
+ async for message in query(prompt=prompt, options=options):
306
+ # Handle different message types from the SDK
307
+ # The SDK may return dicts or typed objects depending on version
308
+ if SDK_HAS_TYPES and isinstance(message, AssistantMessage):
309
+ # Typed SDK - iterate through content blocks
310
+ for block in message.content:
311
+ if isinstance(block, TextBlock):
312
+ content_parts.append(block.text)
313
+ elif isinstance(block, ToolUseBlock):
314
+ tool_use = {
315
+ "tool": block.name,
316
+ "input": block.input,
317
+ }
318
+ tool_uses.append(tool_use)
319
+
320
+ # Track file modifications
321
+ if block.name in ("Write", "Edit"):
322
+ file_path = block.input.get("file_path")
323
+ if file_path and file_path not in files_modified:
324
+ files_modified.append(file_path)
325
+
326
+ elif isinstance(message, dict):
327
+ # Dict-based SDK response
328
+ msg_type = message.get("type", "")
329
+
330
+ if msg_type == "assistant":
331
+ # Process content blocks
332
+ content = message.get("content", [])
333
+ for block in content:
334
+ block_type = block.get("type", "")
335
+ if block_type == "text":
336
+ content_parts.append(block.get("text", ""))
337
+ elif block_type == "tool_use":
338
+ tool_use = {
339
+ "tool": block.get("name", ""),
340
+ "input": block.get("input", {}),
341
+ }
342
+ tool_uses.append(tool_use)
343
+
344
+ # Track file modifications
345
+ tool_name = block.get("name", "")
346
+ if tool_name in ("Write", "Edit"):
347
+ file_path = block.get("input", {}).get(
348
+ "file_path"
349
+ )
350
+ if (
351
+ file_path
352
+ and file_path not in files_modified
353
+ ):
354
+ files_modified.append(file_path)
355
+
356
+ elif msg_type == "text":
357
+ # Direct text message
358
+ content_parts.append(message.get("text", ""))
359
+
360
+ elif msg_type == "result":
361
+ # Final result message
362
+ if message.get("content"):
363
+ content_parts.append(str(message.get("content", "")))
364
+
365
+ except asyncio.TimeoutError:
366
+ logger.warning(
367
+ f"Query timed out after {self.config.timeout}s. "
368
+ f"Returning partial results: {len(tool_uses)} tool uses, "
369
+ f"{len(files_modified)} files modified"
370
+ )
371
+ # Return partial results - the work done so far is still valid
356
372
 
357
373
  return content_parts, tool_uses, files_modified
358
374
 
@@ -343,6 +343,26 @@ class SugarLoop:
343
343
  datetime.now(timezone.utc) - start_time
344
344
  ).total_seconds()
345
345
 
346
+ # Check if execution actually succeeded
347
+ # Default to False as fail-safe - all executors should set success explicitly
348
+ if "success" not in result:
349
+ logger.warning(
350
+ f"⚠️ Missing 'success' field in result for [{work_item['id']}]"
351
+ )
352
+ execution_success = result.get("success", False)
353
+ if not execution_success:
354
+ error_msg = result.get("error") or result.get(
355
+ "summary", "Execution returned failure"
356
+ )
357
+ logger.warning(
358
+ f"⚠️ Task execution failed [{work_item['id']}]: {error_msg}"
359
+ )
360
+ await self.work_queue.fail_work(
361
+ work_item["id"], error_msg, execution_time=execution_time
362
+ )
363
+ await self._handle_failed_workflow(work_item, workflow, error_msg)
364
+ return
365
+
346
366
  # Complete unified workflow (commit, branch, PR, issues)
347
367
  workflow_success = (
348
368
  await self.workflow_orchestrator.complete_work_execution(
@@ -60,6 +60,14 @@ class AgentSDKExecutor(BaseExecutor):
60
60
  # MCP servers
61
61
  self.mcp_servers = config.get("mcp_servers", {})
62
62
 
63
+ # Log MCP server configuration for debugging
64
+ if self.mcp_servers:
65
+ logger.info(f"MCP servers configured: {list(self.mcp_servers.keys())}")
66
+ for name, server_config in self.mcp_servers.items():
67
+ logger.debug(f" {name}: {server_config.get('command', 'N/A')}")
68
+ else:
69
+ logger.debug("No MCP servers configured")
70
+
63
71
  # Agent instance (lazy initialization)
64
72
  self._agent: Optional[SugarAgent] = None
65
73
  self._session_active = False
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sugarai
3
- Version: 3.3.0
4
- Summary: Sugar 🍰 - A dev team that never stops. Delegate full tasks to AI in the background.
3
+ Version: 3.3.1.dev1
4
+ Summary: Sugar 🍰 - Vibe coding while you sleep. Autonomous AI development for Claude Code.
5
5
  Author-email: Steven Leggett <contact@roboticforce.io>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/roboticforce/sugar
@@ -58,9 +58,9 @@ Dynamic: license-file
58
58
 
59
59
  # Sugar 🍰
60
60
 
61
- A dev team that never stops.
61
+ Vibe coding while you sleep.
62
62
 
63
- Delegate full tasks to AI in the background. Sugar builds features, fixes bugs, and ships code while you focus on what matters.
63
+ Autonomous AI development for Claude Code. Sugar builds features, fixes bugs, and ships code while you focus on what matters.
64
64
 
65
65
  ## What It Does
66
66
 
@@ -86,6 +86,7 @@ tests/test_hold_functionality.py
86
86
  tests/test_hooks.py
87
87
  tests/test_issue_responder_cli.py
88
88
  tests/test_issue_response_integration.py
89
+ tests/test_mcp_integration.py
89
90
  tests/test_mcp_server.py
90
91
  tests/test_orchestration.py
91
92
  tests/test_profiles.py
@@ -118,7 +118,9 @@ class TestSugarAgentConfig:
118
118
  assert config.allowed_tools == []
119
119
  assert config.mcp_servers == {}
120
120
  assert config.quality_gates_enabled is True
121
- assert config.timeout == 300
121
+ assert (
122
+ config.timeout == 7200
123
+ ) # 2 hours - increased to handle long-running tasks
122
124
  assert config.max_retries == 3
123
125
 
124
126
  def test_custom_values(self):
@@ -576,6 +578,27 @@ class TestExecute:
576
578
  assert response.success is True
577
579
  assert "Success after retry" in response.content
578
580
 
581
+ @pytest.mark.asyncio
582
+ async def test_execute_with_timeout(self, agent_config):
583
+ """Test timeout handling when query hangs."""
584
+ agent_config.timeout = 0.1 # 100ms for testing
585
+ agent = SugarAgent(agent_config)
586
+
587
+ async def mock_query_that_hangs(**kwargs):
588
+ yield {"type": "text", "text": "Started work"}
589
+ # Simulate a hanging generator
590
+ await asyncio.sleep(10) # Much longer than timeout
591
+ yield {"type": "text", "text": "This should not appear"}
592
+
593
+ with patch("sugar.agent.base.query", mock_query_that_hangs):
594
+ response = await agent.execute("Test with timeout")
595
+
596
+ # Should succeed with partial results (the first message before hang)
597
+ assert response.success is True
598
+ assert "Started work" in response.content
599
+ # The second message should NOT appear since we timed out
600
+ assert "This should not appear" not in response.content
601
+
579
602
 
580
603
  # ============================================================================
581
604
  # Test Execute Work Item
@@ -258,6 +258,54 @@ class TestSugarLoop:
258
258
  # Verify work was marked as failed
259
259
  loop.work_queue.fail_work.assert_called_once()
260
260
 
261
+ @pytest.mark.asyncio
262
+ async def test_execute_work_result_failure(self, sugar_config_file):
263
+ """Test work execution when result indicates failure (not exception)"""
264
+ with (
265
+ patch("sugar.core.loop.WorkQueue"),
266
+ patch("sugar.core.loop.ClaudeWrapper"),
267
+ patch("sugar.core.loop.AgentSDKExecutor"),
268
+ patch("sugar.core.loop.ErrorLogMonitor"),
269
+ patch("sugar.core.loop.WorkflowOrchestrator"),
270
+ ):
271
+
272
+ loop = SugarLoop(str(sugar_config_file))
273
+
274
+ mock_tasks = [
275
+ {
276
+ "id": "task-1",
277
+ "type": "bug_fix",
278
+ "title": "Fix auth bug",
279
+ "priority": 5,
280
+ }
281
+ ]
282
+
283
+ # Replace components with AsyncMock
284
+ loop.work_queue = AsyncMock()
285
+ loop.work_queue.get_next_work = AsyncMock(side_effect=[mock_tasks[0], None])
286
+ loop.work_queue.fail_work = AsyncMock()
287
+ loop.work_queue.complete_work = AsyncMock()
288
+ loop.workflow_orchestrator = AsyncMock()
289
+ loop.workflow_orchestrator.prepare_work_execution = AsyncMock(
290
+ return_value={}
291
+ )
292
+ loop.executor = AsyncMock()
293
+ # Return failure result instead of raising exception
294
+ loop.executor.execute_work = AsyncMock(
295
+ return_value={
296
+ "success": False,
297
+ "error": "Task could not be completed",
298
+ }
299
+ )
300
+ loop._handle_failed_workflow = AsyncMock()
301
+
302
+ await loop._execute_work()
303
+
304
+ # Verify work was marked as failed (not completed)
305
+ loop.work_queue.fail_work.assert_called_once()
306
+ loop.work_queue.complete_work.assert_not_called()
307
+ loop._handle_failed_workflow.assert_called_once()
308
+
261
309
  @pytest.mark.asyncio
262
310
  async def test_concurrent_work_execution(self, sugar_config_file):
263
311
  """Test concurrent execution of multiple tasks"""
@@ -0,0 +1,498 @@
1
+ """
2
+ Integration tests for MCP server configuration and execution success handling.
3
+
4
+ These tests verify:
5
+ 1. MCP server configuration is properly logged
6
+ 2. Execution success/failure is properly detected and handled
7
+ 3. Tasks are correctly marked as complete or failed based on result
8
+ """
9
+
10
+ import pytest
11
+ import asyncio
12
+ import yaml
13
+ import logging
14
+ from pathlib import Path
15
+ from unittest.mock import AsyncMock, MagicMock, patch, call
16
+ from io import StringIO
17
+
18
+ from sugar.executor.agent_sdk_executor import AgentSDKExecutor
19
+ from sugar.core.loop import SugarLoop
20
+
21
+
22
+ class TestMCPServerLogging:
23
+ """Test MCP server configuration logging."""
24
+
25
+ def test_mcp_servers_logged_when_configured(self, caplog):
26
+ """Verify MCP servers are logged at INFO level when configured."""
27
+ config = {
28
+ "model": "claude-sonnet-4-20250514",
29
+ "timeout": 300,
30
+ "dry_run": True,
31
+ "mcp_servers": {
32
+ "playwright": {
33
+ "command": "npx",
34
+ "args": ["-y", "@anthropic/mcp-server-playwright"],
35
+ },
36
+ "filesystem": {
37
+ "command": "npx",
38
+ "args": ["-y", "@anthropic/mcp-server-filesystem"],
39
+ },
40
+ },
41
+ }
42
+
43
+ with caplog.at_level(logging.INFO):
44
+ executor = AgentSDKExecutor(config)
45
+
46
+ # Verify INFO log about MCP servers
47
+ assert "MCP servers configured: ['playwright', 'filesystem']" in caplog.text
48
+ # Verify the servers are stored
49
+ assert executor.mcp_servers == config["mcp_servers"]
50
+
51
+ def test_mcp_servers_debug_logged_when_configured(self, caplog):
52
+ """Verify individual MCP server details are logged at DEBUG level."""
53
+ config = {
54
+ "model": "claude-sonnet-4-20250514",
55
+ "timeout": 300,
56
+ "dry_run": True,
57
+ "mcp_servers": {
58
+ "playwright": {
59
+ "command": "npx",
60
+ "args": ["-y", "@anthropic/mcp-server-playwright"],
61
+ },
62
+ },
63
+ }
64
+
65
+ with caplog.at_level(logging.DEBUG):
66
+ executor = AgentSDKExecutor(config)
67
+
68
+ # Verify DEBUG log with command details
69
+ assert "playwright: npx" in caplog.text
70
+
71
+ def test_no_mcp_servers_debug_logged(self, caplog):
72
+ """Verify debug message when no MCP servers configured."""
73
+ config = {
74
+ "model": "claude-sonnet-4-20250514",
75
+ "timeout": 300,
76
+ "dry_run": True,
77
+ }
78
+
79
+ with caplog.at_level(logging.DEBUG):
80
+ executor = AgentSDKExecutor(config)
81
+
82
+ # Verify DEBUG log indicates no MCP servers
83
+ assert "No MCP servers configured" in caplog.text
84
+ assert executor.mcp_servers == {}
85
+
86
+ def test_empty_mcp_servers_dict(self, caplog):
87
+ """Verify empty MCP servers dict is handled correctly."""
88
+ config = {
89
+ "model": "claude-sonnet-4-20250514",
90
+ "timeout": 300,
91
+ "dry_run": True,
92
+ "mcp_servers": {},
93
+ }
94
+
95
+ with caplog.at_level(logging.DEBUG):
96
+ executor = AgentSDKExecutor(config)
97
+
98
+ assert "No MCP servers configured" in caplog.text
99
+ assert executor.mcp_servers == {}
100
+
101
+ def test_mcp_servers_passed_to_agent_config(self):
102
+ """Verify MCP servers are passed to agent config."""
103
+ config = {
104
+ "model": "claude-sonnet-4-20250514",
105
+ "timeout": 300,
106
+ "dry_run": True,
107
+ "mcp_servers": {
108
+ "playwright": {"command": "npx"},
109
+ },
110
+ }
111
+
112
+ executor = AgentSDKExecutor(config)
113
+ agent_config = executor._create_agent_config()
114
+
115
+ assert agent_config.mcp_servers == {"playwright": {"command": "npx"}}
116
+
117
+
118
+ class TestExecutionSuccessHandling:
119
+ """Test execution success/failure detection in SugarLoop."""
120
+
121
+ @pytest.fixture
122
+ def loop_with_mocks(self, sugar_config_file):
123
+ """Create a SugarLoop with mocked dependencies."""
124
+ with (
125
+ patch("sugar.core.loop.WorkQueue"),
126
+ patch("sugar.core.loop.ClaudeWrapper"),
127
+ patch("sugar.core.loop.AgentSDKExecutor"),
128
+ patch("sugar.core.loop.ErrorLogMonitor"),
129
+ patch("sugar.core.loop.WorkflowOrchestrator"),
130
+ ):
131
+ loop = SugarLoop(str(sugar_config_file))
132
+
133
+ # Set up common mocks
134
+ loop.work_queue = AsyncMock()
135
+ loop.work_queue.fail_work = AsyncMock()
136
+ loop.work_queue.complete_work = AsyncMock()
137
+ loop.workflow_orchestrator = AsyncMock()
138
+ loop.workflow_orchestrator.prepare_work_execution = AsyncMock(
139
+ return_value={}
140
+ )
141
+ loop.workflow_orchestrator.complete_work_execution = AsyncMock(
142
+ return_value=True
143
+ )
144
+ loop.executor = AsyncMock()
145
+ loop._handle_failed_workflow = AsyncMock()
146
+
147
+ yield loop
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_success_true_completes_work(self, loop_with_mocks):
151
+ """Verify task with success=True is marked complete."""
152
+ loop = loop_with_mocks
153
+
154
+ mock_task = {
155
+ "id": "task-success",
156
+ "type": "bug_fix",
157
+ "title": "Fix bug",
158
+ "priority": 5,
159
+ }
160
+
161
+ loop.work_queue.get_next_work = AsyncMock(side_effect=[mock_task, None])
162
+ loop.executor.execute_work = AsyncMock(
163
+ return_value={
164
+ "success": True,
165
+ "result": "Task completed successfully",
166
+ "files_changed": ["src/fix.py"],
167
+ }
168
+ )
169
+
170
+ await loop._execute_work()
171
+
172
+ # Verify complete_work was called, not fail_work
173
+ loop.work_queue.complete_work.assert_called_once()
174
+ loop.work_queue.fail_work.assert_not_called()
175
+ loop._handle_failed_workflow.assert_not_called()
176
+
177
+ @pytest.mark.asyncio
178
+ async def test_success_false_fails_work(self, loop_with_mocks):
179
+ """Verify task with success=False is marked failed."""
180
+ loop = loop_with_mocks
181
+
182
+ mock_task = {
183
+ "id": "task-failure",
184
+ "type": "bug_fix",
185
+ "title": "Fix bug",
186
+ "priority": 5,
187
+ }
188
+
189
+ loop.work_queue.get_next_work = AsyncMock(side_effect=[mock_task, None])
190
+ loop.executor.execute_work = AsyncMock(
191
+ return_value={
192
+ "success": False,
193
+ "error": "Could not complete the task",
194
+ }
195
+ )
196
+
197
+ await loop._execute_work()
198
+
199
+ # Verify fail_work was called, not complete_work
200
+ loop.work_queue.fail_work.assert_called_once()
201
+ loop.work_queue.complete_work.assert_not_called()
202
+ loop._handle_failed_workflow.assert_called_once()
203
+
204
+ # Verify error message was passed
205
+ call_args = loop.work_queue.fail_work.call_args
206
+ assert call_args[0][0] == "task-failure"
207
+ assert "Could not complete the task" in call_args[0][1]
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_success_false_uses_summary_if_no_error(self, loop_with_mocks):
211
+ """Verify summary is used as error message if error field missing."""
212
+ loop = loop_with_mocks
213
+
214
+ mock_task = {
215
+ "id": "task-no-error",
216
+ "type": "bug_fix",
217
+ "title": "Fix bug",
218
+ "priority": 5,
219
+ }
220
+
221
+ loop.work_queue.get_next_work = AsyncMock(side_effect=[mock_task, None])
222
+ loop.executor.execute_work = AsyncMock(
223
+ return_value={
224
+ "success": False,
225
+ "summary": "Task failed due to permissions",
226
+ }
227
+ )
228
+
229
+ await loop._execute_work()
230
+
231
+ loop.work_queue.fail_work.assert_called_once()
232
+ call_args = loop.work_queue.fail_work.call_args
233
+ assert "Task failed due to permissions" in call_args[0][1]
234
+
235
+ @pytest.mark.asyncio
236
+ async def test_missing_success_field_logs_warning(self, loop_with_mocks, caplog):
237
+ """Verify warning is logged when success field is missing."""
238
+ loop = loop_with_mocks
239
+
240
+ mock_task = {
241
+ "id": "task-no-success",
242
+ "type": "bug_fix",
243
+ "title": "Fix bug",
244
+ "priority": 5,
245
+ }
246
+
247
+ loop.work_queue.get_next_work = AsyncMock(side_effect=[mock_task, None])
248
+ # Return result without 'success' field
249
+ loop.executor.execute_work = AsyncMock(
250
+ return_value={
251
+ "result": "Something happened",
252
+ }
253
+ )
254
+
255
+ with caplog.at_level(logging.WARNING):
256
+ await loop._execute_work()
257
+
258
+ # Verify warning was logged about missing success field
259
+ assert "Missing 'success' field" in caplog.text
260
+ assert "task-no-success" in caplog.text
261
+
262
+ # With default=False, task should be marked as failed
263
+ loop.work_queue.fail_work.assert_called_once()
264
+ loop.work_queue.complete_work.assert_not_called()
265
+
266
+ @pytest.mark.asyncio
267
+ async def test_exception_during_execution_fails_work(self, loop_with_mocks):
268
+ """Verify exception during execution marks work as failed."""
269
+ loop = loop_with_mocks
270
+
271
+ mock_task = {
272
+ "id": "task-exception",
273
+ "type": "bug_fix",
274
+ "title": "Fix bug",
275
+ "priority": 5,
276
+ }
277
+
278
+ loop.work_queue.get_next_work = AsyncMock(side_effect=[mock_task, None])
279
+ loop.executor.execute_work = AsyncMock(
280
+ side_effect=Exception("Claude API error")
281
+ )
282
+
283
+ await loop._execute_work()
284
+
285
+ loop.work_queue.fail_work.assert_called_once()
286
+ loop.work_queue.complete_work.assert_not_called()
287
+
288
+ # Verify error message contains exception
289
+ call_args = loop.work_queue.fail_work.call_args
290
+ assert "Claude API error" in call_args[0][1]
291
+
292
+
293
+ class TestMCPConfigInSugarLoop:
294
+ """Test MCP config flows through SugarLoop to executor."""
295
+
296
+ def test_mcp_servers_in_config_passed_to_executor(self, temp_dir):
297
+ """Verify MCP servers from config are passed to executor."""
298
+ config_path = temp_dir / ".sugar" / "config.yaml"
299
+ config_path.parent.mkdir()
300
+
301
+ config_data = {
302
+ "sugar": {
303
+ "dry_run": True,
304
+ "loop_interval": 300,
305
+ "max_concurrent_work": 3,
306
+ "claude": {
307
+ "command": "claude",
308
+ "mcp_servers": {
309
+ "playwright": {
310
+ "command": "npx",
311
+ "args": ["-y", "@anthropic/mcp-server-playwright"],
312
+ }
313
+ },
314
+ },
315
+ "storage": {"database": "sugar.db"},
316
+ "discovery": {
317
+ "error_logs": {"enabled": False},
318
+ "github": {"enabled": False},
319
+ "code_quality": {"enabled": False, "root_path": "."},
320
+ "test_coverage": {"enabled": False, "root_path": "."},
321
+ },
322
+ }
323
+ }
324
+
325
+ with open(config_path, "w") as f:
326
+ yaml.dump(config_data, f)
327
+
328
+ with (
329
+ patch("sugar.core.loop.WorkQueue"),
330
+ patch("sugar.core.loop.ClaudeWrapper"),
331
+ patch("sugar.core.loop.AgentSDKExecutor") as mock_executor,
332
+ patch("sugar.core.loop.ErrorLogMonitor"),
333
+ ):
334
+ SugarLoop(str(config_path))
335
+
336
+ # Verify executor was called with config containing mcp_servers
337
+ call_args = mock_executor.call_args
338
+ config_passed = call_args[0][0]
339
+ assert "mcp_servers" in config_passed
340
+ assert "playwright" in config_passed["mcp_servers"]
341
+
342
+
343
+ class TestEndToEndExecution:
344
+ """End-to-end tests for the full execution flow."""
345
+
346
+ @pytest.fixture
347
+ def full_config(self, temp_dir):
348
+ """Create a complete config for end-to-end testing."""
349
+ config_path = temp_dir / ".sugar" / "config.yaml"
350
+ config_path.parent.mkdir()
351
+
352
+ config_data = {
353
+ "sugar": {
354
+ "dry_run": True,
355
+ "loop_interval": 60,
356
+ "max_concurrent_work": 1,
357
+ "claude": {
358
+ "command": "claude",
359
+ "timeout": 60,
360
+ "executor": "sdk",
361
+ "mcp_servers": {
362
+ "playwright": {
363
+ "command": "npx",
364
+ "args": ["-y", "@anthropic/mcp-server-playwright"],
365
+ }
366
+ },
367
+ },
368
+ "storage": {"database": str(temp_dir / "sugar.db")},
369
+ "discovery": {
370
+ "error_logs": {"enabled": False},
371
+ "github": {"enabled": False},
372
+ "code_quality": {"enabled": False, "root_path": "."},
373
+ "test_coverage": {"enabled": False, "root_path": "."},
374
+ },
375
+ }
376
+ }
377
+
378
+ with open(config_path, "w") as f:
379
+ yaml.dump(config_data, f)
380
+
381
+ return config_path
382
+
383
+ @pytest.mark.asyncio
384
+ async def test_full_success_flow(self, full_config, caplog):
385
+ """Test complete flow from task pickup to completion."""
386
+ with (
387
+ patch("sugar.core.loop.WorkQueue") as mock_queue_class,
388
+ patch("sugar.core.loop.AgentSDKExecutor") as mock_executor_class,
389
+ patch("sugar.core.loop.ErrorLogMonitor"),
390
+ patch("sugar.core.loop.WorkflowOrchestrator") as mock_workflow_class,
391
+ ):
392
+ # Set up mocks
393
+ mock_queue = AsyncMock()
394
+ mock_queue_class.return_value = mock_queue
395
+
396
+ mock_executor = AsyncMock()
397
+ mock_executor_class.return_value = mock_executor
398
+
399
+ mock_workflow = AsyncMock()
400
+ mock_workflow_class.return_value = mock_workflow
401
+
402
+ # Configure task and result
403
+ test_task = {
404
+ "id": "e2e-task-1",
405
+ "type": "test",
406
+ "title": "Run browser test",
407
+ "priority": 5,
408
+ }
409
+
410
+ mock_queue.get_next_work = AsyncMock(side_effect=[test_task, None])
411
+ mock_queue.complete_work = AsyncMock()
412
+ mock_queue.fail_work = AsyncMock()
413
+
414
+ mock_executor.execute_work = AsyncMock(
415
+ return_value={
416
+ "success": True,
417
+ "result": "Browser test completed",
418
+ "files_changed": [],
419
+ }
420
+ )
421
+
422
+ mock_workflow.prepare_work_execution = AsyncMock(return_value={})
423
+ mock_workflow.complete_work_execution = AsyncMock(return_value=True)
424
+
425
+ with caplog.at_level(logging.INFO):
426
+ loop = SugarLoop(str(full_config))
427
+ loop.work_queue = mock_queue
428
+ loop.executor = mock_executor
429
+ loop.workflow_orchestrator = mock_workflow
430
+
431
+ await loop._execute_work()
432
+
433
+ # Verify success flow
434
+ mock_executor.execute_work.assert_called_once_with(test_task)
435
+ mock_queue.complete_work.assert_called_once()
436
+ mock_queue.fail_work.assert_not_called()
437
+
438
+ # Verify completion was logged
439
+ assert "Work completed" in caplog.text
440
+
441
+ @pytest.mark.asyncio
442
+ async def test_full_failure_flow(self, full_config, caplog):
443
+ """Test complete flow from task pickup to failure."""
444
+ with (
445
+ patch("sugar.core.loop.WorkQueue") as mock_queue_class,
446
+ patch("sugar.core.loop.AgentSDKExecutor") as mock_executor_class,
447
+ patch("sugar.core.loop.ErrorLogMonitor"),
448
+ patch("sugar.core.loop.WorkflowOrchestrator") as mock_workflow_class,
449
+ ):
450
+ # Set up mocks
451
+ mock_queue = AsyncMock()
452
+ mock_queue_class.return_value = mock_queue
453
+
454
+ mock_executor = AsyncMock()
455
+ mock_executor_class.return_value = mock_executor
456
+
457
+ mock_workflow = AsyncMock()
458
+ mock_workflow_class.return_value = mock_workflow
459
+
460
+ # Configure task and failed result
461
+ test_task = {
462
+ "id": "e2e-task-fail",
463
+ "type": "test",
464
+ "title": "Run browser test",
465
+ "priority": 5,
466
+ }
467
+
468
+ mock_queue.get_next_work = AsyncMock(side_effect=[test_task, None])
469
+ mock_queue.complete_work = AsyncMock()
470
+ mock_queue.fail_work = AsyncMock()
471
+
472
+ mock_executor.execute_work = AsyncMock(
473
+ return_value={
474
+ "success": False,
475
+ "error": "Browser automation failed: element not found",
476
+ }
477
+ )
478
+
479
+ mock_workflow.prepare_work_execution = AsyncMock(return_value={})
480
+
481
+ with caplog.at_level(logging.WARNING):
482
+ loop = SugarLoop(str(full_config))
483
+ loop.work_queue = mock_queue
484
+ loop.executor = mock_executor
485
+ loop.workflow_orchestrator = mock_workflow
486
+ loop._handle_failed_workflow = AsyncMock()
487
+
488
+ await loop._execute_work()
489
+
490
+ # Verify failure flow
491
+ mock_executor.execute_work.assert_called_once_with(test_task)
492
+ mock_queue.fail_work.assert_called_once()
493
+ mock_queue.complete_work.assert_not_called()
494
+
495
+ # Verify failure was logged
496
+ assert (
497
+ "Task execution failed" in caplog.text or "e2e-task-fail" in caplog.text
498
+ )
File without changes
File without changes
File without changes
File without changes
File without changes