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.
- {sugarai-3.3.0/sugarai.egg-info → sugarai-3.3.1.dev1}/PKG-INFO +4 -4
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/README.md +2 -2
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/pyproject.toml +2 -2
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/base.py +72 -56
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/core/loop.py +20 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/agent_sdk_executor.py +8 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1/sugarai.egg-info}/PKG-INFO +4 -4
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/SOURCES.txt +1 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_agent.py +24 -1
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_core_loop.py +48 -0
- sugarai-3.3.1.dev1/tests/test_mcp_integration.py +498 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/LICENSE +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/setup.cfg +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/__version__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/hooks.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/subagent_manager.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/agent/tools.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/api_keys.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/tiers.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/billing/usage.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/config/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/config/issue_responder_config.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/config/prompt_config.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/core/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/code_quality.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/error_monitor.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/github_watcher.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/discovery/test_coverage.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/base.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/claude_wrapper.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/executor/structured_request.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/integrations/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/integrations/github.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/learning/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/learning/adaptive_scheduler.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/learning/feedback_processor.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/main.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/mcp/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/mcp/server.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/orchestration/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/orchestration/agent_router.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/orchestration/task_orchestrator.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/base.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/default.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/profiles/issue_responder.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/coordinator.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/diff_validator.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/evidence.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/failure_handler.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/functional_verifier.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/preflight_checks.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/success_criteria.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/test_validator.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/quality_gates/truth_enforcer.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/config.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/profile.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/ralph/validator.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/issue_response_manager.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/task_type_manager.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/storage/work_queue.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/capability_scanner.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/complexity_analyzer.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/triage/triage_analyzer.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/utils/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/utils/git_operations.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/utils/toon_encoder.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/workflow/__init__.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugar/workflow/orchestrator.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/dependency_links.txt +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/entry_points.txt +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/requires.txt +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/sugarai.egg-info/top_level.txt +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_billing.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_cli.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_complexity_analyzer.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_github_client.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_goose_integration.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_hold_functionality.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_hooks.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_issue_responder_cli.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_issue_response_integration.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_mcp_server.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_orchestration.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_profiles.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_prompt_config.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_quality_gates.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_ralph.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_storage.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_subagent_manager.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_task_types.py +0 -0
- {sugarai-3.3.0 → sugarai-3.3.1.dev1}/tests/test_toon_encoder.py +0 -0
- {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.
|
|
4
|
-
Summary: Sugar 🍰 -
|
|
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
|
-
|
|
61
|
+
Vibe coding while you sleep.
|
|
62
62
|
|
|
63
|
-
|
|
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
|
-
|
|
3
|
+
Vibe coding while you sleep.
|
|
4
4
|
|
|
5
|
-
|
|
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.
|
|
8
|
-
description = "Sugar 🍰 -
|
|
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 =
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if isinstance(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
"
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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.
|
|
4
|
-
Summary: Sugar 🍰 -
|
|
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
|
-
|
|
61
|
+
Vibe coding while you sleep.
|
|
62
62
|
|
|
63
|
-
|
|
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
|
|
|
@@ -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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|