draft-board 0.1.0-beta.0
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.
- package/app/backend/.env.example +9 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_patch.txt +195 -0
- package/app/backend/.smartkanban/evidence/8b383839-cbec-45af-86ee-c7708d075cbe/bddf2ed5-2e21-4d46-a62b-10b87f1642a6_stat.txt +6 -0
- package/app/backend/CURL_EXAMPLES.md +335 -0
- package/app/backend/ENV_SETUP.md +65 -0
- package/app/backend/alembic/env.py +71 -0
- package/app/backend/alembic/script.py.mako +28 -0
- package/app/backend/alembic/versions/001_initial_schema.py +104 -0
- package/app/backend/alembic/versions/002_add_jobs_table.py +52 -0
- package/app/backend/alembic/versions/003_add_workspace_table.py +48 -0
- package/app/backend/alembic/versions/004_add_evidence_table.py +56 -0
- package/app/backend/alembic/versions/005_add_verification_commands.py +32 -0
- package/app/backend/alembic/versions/006_add_planner_lock_table.py +39 -0
- package/app/backend/alembic/versions/007_add_revision_review_tables.py +126 -0
- package/app/backend/alembic/versions/008_add_revision_idempotency_and_traceability.py +52 -0
- package/app/backend/alembic/versions/009_add_job_health_fields.py +46 -0
- package/app/backend/alembic/versions/010_add_review_comment_line_content.py +36 -0
- package/app/backend/alembic/versions/011_add_analysis_cache.py +47 -0
- package/app/backend/alembic/versions/012_add_boards_table.py +102 -0
- package/app/backend/alembic/versions/013_add_ticket_blocking.py +45 -0
- package/app/backend/alembic/versions/014_add_agent_sessions.py +220 -0
- package/app/backend/alembic/versions/015_add_ticket_sort_order.py +33 -0
- package/app/backend/alembic/versions/03220f0b93ae_add_pr_fields_to_ticket.py +49 -0
- package/app/backend/alembic/versions/0c2d89fff3b1_seed_board_configs_from_yaml.py +206 -0
- package/app/backend/alembic/versions/3348e5cf54c1_add_merge_checklist_table.py +67 -0
- package/app/backend/alembic/versions/357c780ee445_add_goal_status.py +34 -0
- package/app/backend/alembic/versions/553340b7e26c_add_autonomy_fields_to_goal.py +65 -0
- package/app/backend/alembic/versions/774dc335c679_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/7b307e847cbd_merge_heads.py +23 -0
- package/app/backend/alembic/versions/82ecd978cc70_add_missing_indexes.py +48 -0
- package/app/backend/alembic/versions/8ef5054dc280_add_normalized_log_entries.py +173 -0
- package/app/backend/alembic/versions/8f3e2bd8ea3b_merge_migration_heads.py +23 -0
- package/app/backend/alembic/versions/9d17f0698d3b_add_config_column_to_boards_table.py +30 -0
- package/app/backend/alembic/versions/add_agent_conversation_history.py +72 -0
- package/app/backend/alembic/versions/add_job_variant.py +34 -0
- package/app/backend/alembic/versions/add_performance_indexes.py +95 -0
- package/app/backend/alembic/versions/add_repos_and_board_repos.py +174 -0
- package/app/backend/alembic/versions/add_session_id_to_jobs.py +27 -0
- package/app/backend/alembic/versions/add_sqlite_backend_tables.py +104 -0
- package/app/backend/alembic/versions/b10fb0b62240_add_diff_content_to_revisions.py +34 -0
- package/app/backend/alembic.ini +89 -0
- package/app/backend/app/__init__.py +3 -0
- package/app/backend/app/data_dir.py +85 -0
- package/app/backend/app/database.py +70 -0
- package/app/backend/app/database_sync.py +64 -0
- package/app/backend/app/dependencies/__init__.py +5 -0
- package/app/backend/app/dependencies/auth.py +80 -0
- package/app/backend/app/dependencies.py +43 -0
- package/app/backend/app/exceptions.py +178 -0
- package/app/backend/app/executors/__init__.py +1 -0
- package/app/backend/app/executors/adapters/__init__.py +1 -0
- package/app/backend/app/executors/adapters/aider.py +152 -0
- package/app/backend/app/executors/adapters/amazon_q.py +103 -0
- package/app/backend/app/executors/adapters/amp.py +123 -0
- package/app/backend/app/executors/adapters/claude.py +177 -0
- package/app/backend/app/executors/adapters/cline.py +127 -0
- package/app/backend/app/executors/adapters/codex.py +167 -0
- package/app/backend/app/executors/adapters/copilot.py +202 -0
- package/app/backend/app/executors/adapters/cursor.py +87 -0
- package/app/backend/app/executors/adapters/droid.py +123 -0
- package/app/backend/app/executors/adapters/gemini.py +132 -0
- package/app/backend/app/executors/adapters/goose.py +131 -0
- package/app/backend/app/executors/adapters/opencode.py +123 -0
- package/app/backend/app/executors/adapters/qwen.py +123 -0
- package/app/backend/app/executors/plugins/__init__.py +1 -0
- package/app/backend/app/executors/registry.py +202 -0
- package/app/backend/app/executors/spec.py +226 -0
- package/app/backend/app/main.py +486 -0
- package/app/backend/app/middleware/__init__.py +13 -0
- package/app/backend/app/middleware/idempotency.py +426 -0
- package/app/backend/app/middleware/rate_limit.py +312 -0
- package/app/backend/app/middleware/security_headers.py +43 -0
- package/app/backend/app/middleware/timeout.py +37 -0
- package/app/backend/app/models/__init__.py +56 -0
- package/app/backend/app/models/agent_conversation_history.py +56 -0
- package/app/backend/app/models/agent_session.py +127 -0
- package/app/backend/app/models/analysis_cache.py +49 -0
- package/app/backend/app/models/base.py +9 -0
- package/app/backend/app/models/board.py +79 -0
- package/app/backend/app/models/board_repo.py +68 -0
- package/app/backend/app/models/cost_budget.py +42 -0
- package/app/backend/app/models/enums.py +40 -0
- package/app/backend/app/models/evidence.py +132 -0
- package/app/backend/app/models/goal.py +102 -0
- package/app/backend/app/models/idempotency_entry.py +30 -0
- package/app/backend/app/models/job.py +163 -0
- package/app/backend/app/models/job_queue.py +39 -0
- package/app/backend/app/models/kv_store.py +28 -0
- package/app/backend/app/models/merge_checklist.py +87 -0
- package/app/backend/app/models/normalized_log.py +100 -0
- package/app/backend/app/models/planner_lock.py +43 -0
- package/app/backend/app/models/rate_limit_entry.py +25 -0
- package/app/backend/app/models/repo.py +66 -0
- package/app/backend/app/models/review_comment.py +91 -0
- package/app/backend/app/models/review_summary.py +69 -0
- package/app/backend/app/models/revision.py +130 -0
- package/app/backend/app/models/ticket.py +223 -0
- package/app/backend/app/models/ticket_event.py +83 -0
- package/app/backend/app/models/user.py +47 -0
- package/app/backend/app/models/workspace.py +71 -0
- package/app/backend/app/redis_client.py +119 -0
- package/app/backend/app/routers/__init__.py +29 -0
- package/app/backend/app/routers/agents.py +296 -0
- package/app/backend/app/routers/auth.py +94 -0
- package/app/backend/app/routers/board.py +885 -0
- package/app/backend/app/routers/dashboard.py +351 -0
- package/app/backend/app/routers/debug.py +528 -0
- package/app/backend/app/routers/evidence.py +96 -0
- package/app/backend/app/routers/executors.py +324 -0
- package/app/backend/app/routers/goals.py +574 -0
- package/app/backend/app/routers/jobs.py +448 -0
- package/app/backend/app/routers/maintenance.py +172 -0
- package/app/backend/app/routers/merge.py +360 -0
- package/app/backend/app/routers/planner.py +537 -0
- package/app/backend/app/routers/pull_requests.py +382 -0
- package/app/backend/app/routers/repos.py +263 -0
- package/app/backend/app/routers/revisions.py +939 -0
- package/app/backend/app/routers/settings.py +267 -0
- package/app/backend/app/routers/tickets.py +2003 -0
- package/app/backend/app/routers/webhooks.py +143 -0
- package/app/backend/app/routers/websocket.py +249 -0
- package/app/backend/app/schemas/__init__.py +109 -0
- package/app/backend/app/schemas/board.py +87 -0
- package/app/backend/app/schemas/common.py +33 -0
- package/app/backend/app/schemas/evidence.py +87 -0
- package/app/backend/app/schemas/goal.py +90 -0
- package/app/backend/app/schemas/job.py +97 -0
- package/app/backend/app/schemas/merge.py +139 -0
- package/app/backend/app/schemas/planner.py +500 -0
- package/app/backend/app/schemas/repo.py +187 -0
- package/app/backend/app/schemas/review.py +137 -0
- package/app/backend/app/schemas/revision.py +114 -0
- package/app/backend/app/schemas/ticket.py +238 -0
- package/app/backend/app/schemas/ticket_event.py +72 -0
- package/app/backend/app/schemas/workspace.py +19 -0
- package/app/backend/app/services/__init__.py +31 -0
- package/app/backend/app/services/agent_memory_service.py +223 -0
- package/app/backend/app/services/agent_registry.py +346 -0
- package/app/backend/app/services/agent_session_manager.py +318 -0
- package/app/backend/app/services/agent_session_service.py +219 -0
- package/app/backend/app/services/agent_tools.py +379 -0
- package/app/backend/app/services/auth_service.py +98 -0
- package/app/backend/app/services/autonomy_service.py +380 -0
- package/app/backend/app/services/board_repo_service.py +201 -0
- package/app/backend/app/services/board_service.py +326 -0
- package/app/backend/app/services/cleanup_service.py +1085 -0
- package/app/backend/app/services/config_service.py +908 -0
- package/app/backend/app/services/context_gatherer.py +557 -0
- package/app/backend/app/services/cost_tracking_service.py +293 -0
- package/app/backend/app/services/cursor_log_normalizer.py +536 -0
- package/app/backend/app/services/delivery_pipeline.py +440 -0
- package/app/backend/app/services/executor_service.py +634 -0
- package/app/backend/app/services/git_host/__init__.py +11 -0
- package/app/backend/app/services/git_host/factory.py +87 -0
- package/app/backend/app/services/git_host/github.py +270 -0
- package/app/backend/app/services/git_host/gitlab.py +194 -0
- package/app/backend/app/services/git_host/protocol.py +75 -0
- package/app/backend/app/services/git_merge_simple.py +346 -0
- package/app/backend/app/services/git_ops.py +384 -0
- package/app/backend/app/services/github_service.py +233 -0
- package/app/backend/app/services/goal_service.py +113 -0
- package/app/backend/app/services/job_service.py +423 -0
- package/app/backend/app/services/job_watchdog_service.py +424 -0
- package/app/backend/app/services/langchain_adapter.py +122 -0
- package/app/backend/app/services/llm_provider_clients.py +351 -0
- package/app/backend/app/services/llm_service.py +285 -0
- package/app/backend/app/services/log_normalizer.py +342 -0
- package/app/backend/app/services/log_stream_service.py +276 -0
- package/app/backend/app/services/merge_checklist_service.py +264 -0
- package/app/backend/app/services/merge_service.py +784 -0
- package/app/backend/app/services/orchestrator_log.py +84 -0
- package/app/backend/app/services/planner_service.py +1662 -0
- package/app/backend/app/services/planner_tick_sync.py +1040 -0
- package/app/backend/app/services/queued_message_service.py +156 -0
- package/app/backend/app/services/reliability_wrapper.py +389 -0
- package/app/backend/app/services/repo_discovery_service.py +318 -0
- package/app/backend/app/services/review_service.py +334 -0
- package/app/backend/app/services/revision_service.py +389 -0
- package/app/backend/app/services/safe_autopilot.py +510 -0
- package/app/backend/app/services/sqlite_worker.py +372 -0
- package/app/backend/app/services/task_dispatch.py +135 -0
- package/app/backend/app/services/ticket_generation_service.py +1781 -0
- package/app/backend/app/services/ticket_service.py +486 -0
- package/app/backend/app/services/udar_planner_service.py +1007 -0
- package/app/backend/app/services/webhook_service.py +126 -0
- package/app/backend/app/services/workspace_service.py +465 -0
- package/app/backend/app/services/worktree_file_service.py +92 -0
- package/app/backend/app/services/worktree_validator.py +213 -0
- package/app/backend/app/sqlite_kv.py +278 -0
- package/app/backend/app/state_machine.py +128 -0
- package/app/backend/app/templates/__init__.py +5 -0
- package/app/backend/app/templates/registry.py +243 -0
- package/app/backend/app/utils/__init__.py +5 -0
- package/app/backend/app/utils/artifact_reader.py +87 -0
- package/app/backend/app/utils/circuit_breaker.py +229 -0
- package/app/backend/app/utils/db_retry.py +136 -0
- package/app/backend/app/utils/ignored_fields.py +123 -0
- package/app/backend/app/utils/validators.py +54 -0
- package/app/backend/app/websocket/__init__.py +5 -0
- package/app/backend/app/websocket/manager.py +179 -0
- package/app/backend/app/websocket/state_tracker.py +113 -0
- package/app/backend/app/worker.py +3190 -0
- package/app/backend/calculator_tickets.json +40 -0
- package/app/backend/canary_tests.sh +591 -0
- package/app/backend/celerybeat-schedule +0 -0
- package/app/backend/celerybeat-schedule-shm +0 -0
- package/app/backend/celerybeat-schedule-wal +0 -0
- package/app/backend/logs/.gitkeep +3 -0
- package/app/backend/multiplication_division_implementation_tickets.json +55 -0
- package/app/backend/multiplication_division_tickets.json +42 -0
- package/app/backend/pyproject.toml +45 -0
- package/app/backend/requirements-dev.txt +8 -0
- package/app/backend/requirements.txt +20 -0
- package/app/backend/run.sh +30 -0
- package/app/backend/run_with_logs.sh +10 -0
- package/app/backend/scientific_calculator_tickets.json +40 -0
- package/app/backend/scripts/extract_openapi.py +21 -0
- package/app/backend/scripts/seed_demo.py +187 -0
- package/app/backend/setup_demo_review.py +302 -0
- package/app/backend/test_actual_parse.py +41 -0
- package/app/backend/test_agent_streaming.py +61 -0
- package/app/backend/test_parse.py +51 -0
- package/app/backend/test_streaming.py +51 -0
- package/app/backend/test_subprocess_streaming.py +50 -0
- package/app/backend/tests/__init__.py +1 -0
- package/app/backend/tests/conftest.py +46 -0
- package/app/backend/tests/test_auth.py +341 -0
- package/app/backend/tests/test_autonomy_service.py +391 -0
- package/app/backend/tests/test_cleanup_service_safety.py +417 -0
- package/app/backend/tests/test_middleware.py +279 -0
- package/app/backend/tests/test_planner_providers.py +290 -0
- package/app/backend/tests/test_planner_unblock.py +183 -0
- package/app/backend/tests/test_revision_invariants.py +618 -0
- package/app/backend/tests/test_sqlite_kv.py +290 -0
- package/app/backend/tests/test_sqlite_worker.py +353 -0
- package/app/backend/tests/test_task_dispatch.py +100 -0
- package/app/backend/tests/test_ticket_validation.py +304 -0
- package/app/backend/tests/test_udar_agent.py +693 -0
- package/app/backend/tests/test_webhook_service.py +184 -0
- package/app/backend/tickets_output.json +59 -0
- package/app/backend/user_management_tickets.json +50 -0
- package/app/backend/uvicorn.log +0 -0
- package/app/draft.yaml +313 -0
- package/app/frontend/dist/assets/index-LcjCczu5.js +155 -0
- package/app/frontend/dist/assets/index-_FP_279e.css +1 -0
- package/app/frontend/dist/index.html +14 -0
- package/app/frontend/dist/vite.svg +1 -0
- package/app/frontend/package.json +101 -0
- package/bin/cli.js +527 -0
- package/package.json +37 -0
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"""Tests for UDAR agent (Phase 1: Foundation).
|
|
2
|
+
|
|
3
|
+
Tests basic functionality of tools and LangGraph workflow compilation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from unittest.mock import AsyncMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from app.models.goal import Goal
|
|
12
|
+
from app.models.ticket import Ticket
|
|
13
|
+
from app.services.agent_tools import analyze_codebase, get_goal_context, search_tickets
|
|
14
|
+
from app.services.langchain_adapter import LangChainLLMAdapter
|
|
15
|
+
from app.services.udar_planner_service import UDARPlannerService
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_analyze_codebase_tool():
|
|
20
|
+
"""Test analyze_codebase tool returns valid JSON."""
|
|
21
|
+
# Use current repo for testing
|
|
22
|
+
repo_path = str(Path(__file__).parent.parent.parent)
|
|
23
|
+
|
|
24
|
+
result = await analyze_codebase.ainvoke({"repo_root": repo_path})
|
|
25
|
+
|
|
26
|
+
# Should return valid JSON
|
|
27
|
+
import json
|
|
28
|
+
|
|
29
|
+
parsed = json.loads(result)
|
|
30
|
+
|
|
31
|
+
assert "project_type" in parsed
|
|
32
|
+
assert "file_count" in parsed
|
|
33
|
+
assert parsed["file_count"] > 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_search_tickets_tool(db):
|
|
38
|
+
"""Test search_tickets tool queries database correctly."""
|
|
39
|
+
# Create test goal and tickets
|
|
40
|
+
goal = Goal(
|
|
41
|
+
id="test-goal-1",
|
|
42
|
+
title="Test Goal",
|
|
43
|
+
description="Test",
|
|
44
|
+
)
|
|
45
|
+
db.add(goal)
|
|
46
|
+
|
|
47
|
+
ticket1 = Ticket(
|
|
48
|
+
id="test-ticket-1",
|
|
49
|
+
goal_id=goal.id,
|
|
50
|
+
title="Implement authentication",
|
|
51
|
+
description="Add OAuth2",
|
|
52
|
+
state="planned",
|
|
53
|
+
priority=90,
|
|
54
|
+
)
|
|
55
|
+
ticket2 = Ticket(
|
|
56
|
+
id="test-ticket-2",
|
|
57
|
+
goal_id=goal.id,
|
|
58
|
+
title="Add tests",
|
|
59
|
+
description="Test coverage",
|
|
60
|
+
state="done",
|
|
61
|
+
priority=50,
|
|
62
|
+
)
|
|
63
|
+
db.add(ticket1)
|
|
64
|
+
db.add(ticket2)
|
|
65
|
+
await db.commit()
|
|
66
|
+
|
|
67
|
+
# Test search
|
|
68
|
+
result = await search_tickets.ainvoke(
|
|
69
|
+
{
|
|
70
|
+
"db": db,
|
|
71
|
+
"goal_id": goal.id,
|
|
72
|
+
"query": "auth",
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Should return valid JSON with matching tickets
|
|
77
|
+
import json
|
|
78
|
+
|
|
79
|
+
parsed = json.loads(result)
|
|
80
|
+
|
|
81
|
+
assert parsed["total"] == 1
|
|
82
|
+
assert parsed["tickets"][0]["title"] == "Implement authentication"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@pytest.mark.asyncio
|
|
86
|
+
async def test_get_goal_context_tool(db):
|
|
87
|
+
"""Test get_goal_context tool retrieves goal details."""
|
|
88
|
+
# Create test goal
|
|
89
|
+
goal = Goal(
|
|
90
|
+
id="test-goal-2",
|
|
91
|
+
title="Add feature X",
|
|
92
|
+
description="Detailed description",
|
|
93
|
+
)
|
|
94
|
+
db.add(goal)
|
|
95
|
+
await db.commit()
|
|
96
|
+
|
|
97
|
+
# Test retrieval
|
|
98
|
+
result = await get_goal_context.ainvoke(
|
|
99
|
+
{
|
|
100
|
+
"db": db,
|
|
101
|
+
"goal_id": goal.id,
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Should return valid JSON with goal details
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
parsed = json.loads(result)
|
|
109
|
+
|
|
110
|
+
assert parsed["id"] == goal.id
|
|
111
|
+
assert parsed["title"] == "Add feature X"
|
|
112
|
+
assert "ticket_counts" in parsed
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_langchain_adapter_properties():
|
|
116
|
+
"""Test LangChainLLMAdapter class exists and can be imported."""
|
|
117
|
+
# Just test that the adapter class exists
|
|
118
|
+
# Full integration testing requires actual LLMService instance
|
|
119
|
+
assert LangChainLLMAdapter is not None
|
|
120
|
+
assert hasattr(LangChainLLMAdapter, "_llm_type")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_udar_workflow_compiles():
|
|
125
|
+
"""Test UDAR LangGraph workflow compiles without errors."""
|
|
126
|
+
from unittest.mock import MagicMock
|
|
127
|
+
|
|
128
|
+
# Mock database
|
|
129
|
+
mock_db = MagicMock()
|
|
130
|
+
|
|
131
|
+
# Create service (this will compile the workflow)
|
|
132
|
+
service = UDARPlannerService(db=mock_db)
|
|
133
|
+
|
|
134
|
+
# Verify workflow compiled
|
|
135
|
+
assert service.agent is not None
|
|
136
|
+
|
|
137
|
+
# Verify all nodes exist in graph
|
|
138
|
+
# Note: This is a basic test - full workflow testing requires mocking LLM
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_udar_state_initialization():
|
|
143
|
+
"""Test UDAR state can be initialized with correct types."""
|
|
144
|
+
from app.services.udar_planner_service import UDARState
|
|
145
|
+
|
|
146
|
+
# Create state
|
|
147
|
+
state: UDARState = {
|
|
148
|
+
"goal_id": "test-goal",
|
|
149
|
+
"goal_title": "Test Goal",
|
|
150
|
+
"goal_description": "Test description",
|
|
151
|
+
"repo_root": "/path/to/repo",
|
|
152
|
+
"trigger": "initial_generation",
|
|
153
|
+
"codebase_summary": None,
|
|
154
|
+
"existing_tickets": [],
|
|
155
|
+
"existing_ticket_count": 0,
|
|
156
|
+
"project_type": None,
|
|
157
|
+
"proposed_tickets": [],
|
|
158
|
+
"reasoning": "",
|
|
159
|
+
"should_generate_new": False,
|
|
160
|
+
"llm_calls_made": 0,
|
|
161
|
+
"validated_tickets": [],
|
|
162
|
+
"validation_results": [],
|
|
163
|
+
"final_tickets": [],
|
|
164
|
+
"review_summary": "",
|
|
165
|
+
"phase": "init",
|
|
166
|
+
"iteration": 0,
|
|
167
|
+
"errors": [],
|
|
168
|
+
"total_input_tokens": 0,
|
|
169
|
+
"total_output_tokens": 0,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
# Verify required keys exist
|
|
173
|
+
assert "goal_id" in state
|
|
174
|
+
assert "llm_calls_made" in state
|
|
175
|
+
assert state["llm_calls_made"] == 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@pytest.mark.asyncio
|
|
179
|
+
async def test_analyze_ticket_changes_tool(db):
|
|
180
|
+
"""Test analyze_ticket_changes tool parses diffs correctly."""
|
|
181
|
+
import json
|
|
182
|
+
|
|
183
|
+
from app.models.revision import Revision
|
|
184
|
+
from app.services.agent_tools import analyze_ticket_changes
|
|
185
|
+
|
|
186
|
+
# Create test ticket with revision
|
|
187
|
+
goal = Goal(
|
|
188
|
+
id="test-goal-3",
|
|
189
|
+
title="Test Goal",
|
|
190
|
+
description="Test",
|
|
191
|
+
)
|
|
192
|
+
db.add(goal)
|
|
193
|
+
|
|
194
|
+
ticket = Ticket(
|
|
195
|
+
id="test-ticket-3",
|
|
196
|
+
goal_id=goal.id,
|
|
197
|
+
title="Add authentication",
|
|
198
|
+
description="Implement OAuth2",
|
|
199
|
+
state="done",
|
|
200
|
+
priority=90,
|
|
201
|
+
)
|
|
202
|
+
db.add(ticket)
|
|
203
|
+
|
|
204
|
+
# Create a job for the revision (required FK)
|
|
205
|
+
from app.models.job import Job
|
|
206
|
+
|
|
207
|
+
job = Job(
|
|
208
|
+
id="test-job-changes",
|
|
209
|
+
ticket_id=ticket.id,
|
|
210
|
+
kind="execute",
|
|
211
|
+
status="succeeded",
|
|
212
|
+
)
|
|
213
|
+
db.add(job)
|
|
214
|
+
|
|
215
|
+
# Create revision without diff content (no evidence in this test)
|
|
216
|
+
revision = Revision(
|
|
217
|
+
ticket_id=ticket.id,
|
|
218
|
+
job_id=job.id,
|
|
219
|
+
number=1,
|
|
220
|
+
status="approved",
|
|
221
|
+
)
|
|
222
|
+
db.add(revision)
|
|
223
|
+
await db.commit()
|
|
224
|
+
|
|
225
|
+
# Test tool
|
|
226
|
+
result = await analyze_ticket_changes.ainvoke(
|
|
227
|
+
{
|
|
228
|
+
"db": db,
|
|
229
|
+
"ticket_id": ticket.id,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Should return valid JSON
|
|
234
|
+
parsed = json.loads(result)
|
|
235
|
+
|
|
236
|
+
assert parsed["ticket_id"] == ticket.id
|
|
237
|
+
assert parsed["has_revision"] is True
|
|
238
|
+
assert parsed["verification_passed"] is True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.asyncio
|
|
242
|
+
async def test_udar_config_loads():
|
|
243
|
+
"""Test UDAR config loads from YAML correctly."""
|
|
244
|
+
from app.services.config_service import ConfigService
|
|
245
|
+
|
|
246
|
+
config = ConfigService().load_config()
|
|
247
|
+
|
|
248
|
+
assert hasattr(config.planner_config, "udar")
|
|
249
|
+
assert hasattr(config.planner_config.udar, "enabled")
|
|
250
|
+
assert hasattr(config.planner_config.udar, "replan_batch_size")
|
|
251
|
+
assert config.planner_config.udar.replan_batch_size == 5 # Default value
|
|
252
|
+
assert config.planner_config.udar.max_self_correction_iterations == 1 # Default
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@pytest.mark.asyncio
|
|
256
|
+
async def test_agent_memory_service(db):
|
|
257
|
+
"""Test agent memory service saves and loads checkpoints."""
|
|
258
|
+
from app.services.agent_memory_service import AgentMemoryService
|
|
259
|
+
|
|
260
|
+
memory_service = AgentMemoryService(db)
|
|
261
|
+
|
|
262
|
+
# Create test goal
|
|
263
|
+
goal = Goal(
|
|
264
|
+
id="test-goal-memory",
|
|
265
|
+
title="Test Goal",
|
|
266
|
+
description="Test memory",
|
|
267
|
+
)
|
|
268
|
+
db.add(goal)
|
|
269
|
+
await db.commit()
|
|
270
|
+
|
|
271
|
+
# Save checkpoint
|
|
272
|
+
test_state = {
|
|
273
|
+
"goal_id": goal.id,
|
|
274
|
+
"phase": "review",
|
|
275
|
+
"iteration": 0,
|
|
276
|
+
"proposed_tickets": [{"title": "Test Ticket"}],
|
|
277
|
+
"validated_tickets": [{"title": "Test Ticket"}],
|
|
278
|
+
"reasoning": "Test reasoning for memory checkpoint",
|
|
279
|
+
"llm_calls_made": 1,
|
|
280
|
+
"trigger": "initial_generation",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await memory_service.save_checkpoint(
|
|
284
|
+
goal_id=goal.id,
|
|
285
|
+
checkpoint_id="test-checkpoint-1",
|
|
286
|
+
state=test_state,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Load checkpoint
|
|
290
|
+
loaded = await memory_service.load_checkpoint(goal.id)
|
|
291
|
+
|
|
292
|
+
assert loaded is not None
|
|
293
|
+
assert loaded["phase"] == "review"
|
|
294
|
+
assert loaded["llm_calls_made"] == 1
|
|
295
|
+
assert loaded["tickets_proposed"] == 1
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@pytest.mark.asyncio
|
|
299
|
+
async def test_agent_memory_cleanup(db):
|
|
300
|
+
"""Test agent memory cleanup deletes old checkpoints."""
|
|
301
|
+
|
|
302
|
+
from app.services.agent_memory_service import AgentMemoryService
|
|
303
|
+
|
|
304
|
+
memory_service = AgentMemoryService(db)
|
|
305
|
+
|
|
306
|
+
# Create test goal
|
|
307
|
+
goal = Goal(
|
|
308
|
+
id="test-goal-cleanup",
|
|
309
|
+
title="Test Goal",
|
|
310
|
+
description="Test cleanup",
|
|
311
|
+
)
|
|
312
|
+
db.add(goal)
|
|
313
|
+
await db.commit()
|
|
314
|
+
|
|
315
|
+
# Save checkpoint
|
|
316
|
+
test_state = {
|
|
317
|
+
"goal_id": goal.id,
|
|
318
|
+
"phase": "review",
|
|
319
|
+
"iteration": 0,
|
|
320
|
+
"proposed_tickets": [],
|
|
321
|
+
"validated_tickets": [],
|
|
322
|
+
"reasoning": "Test",
|
|
323
|
+
"llm_calls_made": 0,
|
|
324
|
+
"trigger": "initial_generation",
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await memory_service.save_checkpoint(
|
|
328
|
+
goal_id=goal.id,
|
|
329
|
+
checkpoint_id="test-checkpoint-old",
|
|
330
|
+
state=test_state,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Cleanup old checkpoints (0 days = everything)
|
|
334
|
+
deleted_count = await memory_service.cleanup_old_checkpoints(days=0)
|
|
335
|
+
|
|
336
|
+
assert deleted_count >= 1
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@pytest.mark.asyncio
|
|
340
|
+
async def test_self_correction_conditional_edge():
|
|
341
|
+
"""Test self-correction conditional edge logic."""
|
|
342
|
+
from unittest.mock import MagicMock
|
|
343
|
+
|
|
344
|
+
from app.services.udar_planner_service import UDARPlannerService, UDARState
|
|
345
|
+
|
|
346
|
+
# Mock database
|
|
347
|
+
mock_db = MagicMock()
|
|
348
|
+
service = UDARPlannerService(db=mock_db)
|
|
349
|
+
|
|
350
|
+
# Test case 1: No failures, should proceed
|
|
351
|
+
state_no_failures: UDARState = {
|
|
352
|
+
"goal_id": "test",
|
|
353
|
+
"goal_title": "Test",
|
|
354
|
+
"goal_description": "Test",
|
|
355
|
+
"repo_root": ".",
|
|
356
|
+
"trigger": "initial_generation",
|
|
357
|
+
"codebase_summary": None,
|
|
358
|
+
"existing_tickets": [],
|
|
359
|
+
"existing_ticket_count": 0,
|
|
360
|
+
"project_type": None,
|
|
361
|
+
"proposed_tickets": [],
|
|
362
|
+
"reasoning": "",
|
|
363
|
+
"should_generate_new": False,
|
|
364
|
+
"llm_calls_made": 0,
|
|
365
|
+
"validated_tickets": [],
|
|
366
|
+
"validation_results": [
|
|
367
|
+
{"ticket_title": "Test", "is_valid": True, "reason": "Valid"}
|
|
368
|
+
],
|
|
369
|
+
"final_tickets": [],
|
|
370
|
+
"review_summary": "",
|
|
371
|
+
"phase": "validate",
|
|
372
|
+
"iteration": 0,
|
|
373
|
+
"errors": [],
|
|
374
|
+
"total_input_tokens": 0,
|
|
375
|
+
"total_output_tokens": 0,
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
result = service._should_retry(state_no_failures)
|
|
379
|
+
assert result == "proceed" # No failures, should proceed
|
|
380
|
+
|
|
381
|
+
# Test case 2: Failures but max iterations reached
|
|
382
|
+
state_max_iterations: UDARState = {
|
|
383
|
+
**state_no_failures,
|
|
384
|
+
"validation_results": [
|
|
385
|
+
{"ticket_title": "Test", "is_valid": False, "reason": "Duplicate"}
|
|
386
|
+
],
|
|
387
|
+
"iteration": 1, # Already at max (default is 1)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
result = service._should_retry(state_max_iterations)
|
|
391
|
+
assert result == "proceed" # Max iterations reached, should proceed
|
|
392
|
+
|
|
393
|
+
# Test case 3: Failures and under iteration limit
|
|
394
|
+
state_can_retry: UDARState = {
|
|
395
|
+
**state_no_failures,
|
|
396
|
+
"validation_results": [
|
|
397
|
+
{"ticket_title": "Test", "is_valid": False, "reason": "Duplicate"}
|
|
398
|
+
],
|
|
399
|
+
"iteration": 0, # Under max
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
result = service._should_retry(state_can_retry)
|
|
403
|
+
assert result == "retry" # Can retry
|
|
404
|
+
assert state_can_retry["iteration"] == 1 # Iteration incremented
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Phase 1 verification checklist:
|
|
408
|
+
# [x] Tools can be called independently
|
|
409
|
+
# [x] LangGraph state graph compiles
|
|
410
|
+
# [ ] No disruption to existing ticket generation (requires integration test)
|
|
411
|
+
# [x] Unit tests for tools pass
|
|
412
|
+
|
|
413
|
+
# Phase 3 verification checklist:
|
|
414
|
+
# [x] analyze_ticket_changes tool works
|
|
415
|
+
# [x] UDAR config loads with replanning settings
|
|
416
|
+
# [ ] Replanning batches tickets correctly (requires integration test)
|
|
417
|
+
# [ ] Only calls LLM for significant changes (requires integration test)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# Phase 5: Production Hardening Tests
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@pytest.mark.asyncio
|
|
424
|
+
async def test_udar_timeout_fallback(db):
|
|
425
|
+
"""Test UDAR falls back to legacy on timeout."""
|
|
426
|
+
|
|
427
|
+
from app.exceptions import LLMTimeoutError
|
|
428
|
+
from app.models.board import Board
|
|
429
|
+
from app.models.goal import Goal
|
|
430
|
+
from app.services.udar_planner_service import UDARPlannerService
|
|
431
|
+
|
|
432
|
+
# Create test board and goal
|
|
433
|
+
board = Board(
|
|
434
|
+
id="test-board",
|
|
435
|
+
name="Test Board",
|
|
436
|
+
repo_root=".",
|
|
437
|
+
)
|
|
438
|
+
db.add(board)
|
|
439
|
+
|
|
440
|
+
goal = Goal(
|
|
441
|
+
id="test-goal-timeout",
|
|
442
|
+
title="Test Goal",
|
|
443
|
+
description="Test timeout handling",
|
|
444
|
+
board_id=board.id,
|
|
445
|
+
)
|
|
446
|
+
db.add(goal)
|
|
447
|
+
await db.commit()
|
|
448
|
+
|
|
449
|
+
service = UDARPlannerService(db)
|
|
450
|
+
|
|
451
|
+
mock_fallback_result = {
|
|
452
|
+
"tickets": [],
|
|
453
|
+
"summary": "Fallback result",
|
|
454
|
+
"llm_calls_made": 1,
|
|
455
|
+
"phases_completed": ["legacy"],
|
|
456
|
+
"errors": ["UDAR fallback: timeout"],
|
|
457
|
+
"used_legacy_fallback": True,
|
|
458
|
+
"fallback_reason": "timeout",
|
|
459
|
+
"cost_tracking": {"input_tokens": 0, "output_tokens": 0},
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
with (
|
|
463
|
+
patch.object(service.agent, "ainvoke", side_effect=TimeoutError()),
|
|
464
|
+
patch.object(
|
|
465
|
+
service,
|
|
466
|
+
"_fallback_to_legacy",
|
|
467
|
+
new_callable=AsyncMock,
|
|
468
|
+
return_value=mock_fallback_result,
|
|
469
|
+
),
|
|
470
|
+
):
|
|
471
|
+
# With fallback enabled (default), should return legacy result
|
|
472
|
+
result = await service.generate_from_goal(
|
|
473
|
+
goal.id, fallback_to_legacy=True, timeout_seconds=1
|
|
474
|
+
)
|
|
475
|
+
assert result["used_legacy_fallback"] is True
|
|
476
|
+
assert result["fallback_reason"] == "timeout"
|
|
477
|
+
|
|
478
|
+
with patch.object(service.agent, "ainvoke", side_effect=TimeoutError()):
|
|
479
|
+
# With fallback disabled, should raise exception
|
|
480
|
+
with pytest.raises(LLMTimeoutError):
|
|
481
|
+
await service.generate_from_goal(
|
|
482
|
+
goal.id, fallback_to_legacy=False, timeout_seconds=1
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
@pytest.mark.asyncio
|
|
487
|
+
async def test_udar_tool_error_fallback(db):
|
|
488
|
+
"""Test UDAR falls back to legacy on tool execution error."""
|
|
489
|
+
from app.exceptions import ToolExecutionError
|
|
490
|
+
from app.models.board import Board
|
|
491
|
+
from app.models.goal import Goal
|
|
492
|
+
from app.services.udar_planner_service import UDARPlannerService
|
|
493
|
+
|
|
494
|
+
# Create test board and goal
|
|
495
|
+
board = Board(
|
|
496
|
+
id="test-board-2",
|
|
497
|
+
name="Test Board",
|
|
498
|
+
repo_root=".",
|
|
499
|
+
)
|
|
500
|
+
db.add(board)
|
|
501
|
+
|
|
502
|
+
goal = Goal(
|
|
503
|
+
id="test-goal-tool-error",
|
|
504
|
+
title="Test Goal",
|
|
505
|
+
description="Test tool error handling",
|
|
506
|
+
board_id=board.id,
|
|
507
|
+
)
|
|
508
|
+
db.add(goal)
|
|
509
|
+
await db.commit()
|
|
510
|
+
|
|
511
|
+
service = UDARPlannerService(db)
|
|
512
|
+
|
|
513
|
+
mock_fallback_result = {
|
|
514
|
+
"tickets": [],
|
|
515
|
+
"summary": "Fallback result",
|
|
516
|
+
"llm_calls_made": 1,
|
|
517
|
+
"phases_completed": ["legacy"],
|
|
518
|
+
"errors": ["UDAR fallback: tool_error:analyze_codebase"],
|
|
519
|
+
"used_legacy_fallback": True,
|
|
520
|
+
"fallback_reason": "tool_error:analyze_codebase",
|
|
521
|
+
"cost_tracking": {"input_tokens": 0, "output_tokens": 0},
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Mock tool execution failure and fallback
|
|
525
|
+
with (
|
|
526
|
+
patch.object(
|
|
527
|
+
service.agent,
|
|
528
|
+
"ainvoke",
|
|
529
|
+
side_effect=ToolExecutionError(
|
|
530
|
+
"analyze_codebase", "File not found", "understand"
|
|
531
|
+
),
|
|
532
|
+
),
|
|
533
|
+
patch.object(
|
|
534
|
+
service,
|
|
535
|
+
"_fallback_to_legacy",
|
|
536
|
+
new_callable=AsyncMock,
|
|
537
|
+
return_value=mock_fallback_result,
|
|
538
|
+
),
|
|
539
|
+
):
|
|
540
|
+
result = await service.generate_from_goal(goal.id, fallback_to_legacy=True)
|
|
541
|
+
assert result["used_legacy_fallback"] is True
|
|
542
|
+
assert "tool_error" in result["fallback_reason"]
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@pytest.mark.asyncio
|
|
546
|
+
async def test_udar_cost_tracking(db, caplog):
|
|
547
|
+
"""Test UDAR cost tracking logs costs without crashing.
|
|
548
|
+
|
|
549
|
+
Note: AgentSession requires ticket_id (FK to tickets), so UDAR
|
|
550
|
+
goal-level sessions are logged but not persisted to the DB.
|
|
551
|
+
"""
|
|
552
|
+
import logging
|
|
553
|
+
|
|
554
|
+
from app.models.board import Board
|
|
555
|
+
from app.models.goal import Goal
|
|
556
|
+
from app.services.udar_planner_service import UDARPlannerService
|
|
557
|
+
|
|
558
|
+
# Create test board and goal
|
|
559
|
+
board = Board(
|
|
560
|
+
id="test-board-3",
|
|
561
|
+
name="Test Board",
|
|
562
|
+
repo_root=".",
|
|
563
|
+
)
|
|
564
|
+
db.add(board)
|
|
565
|
+
|
|
566
|
+
goal = Goal(
|
|
567
|
+
id="test-goal-cost",
|
|
568
|
+
title="Test Goal",
|
|
569
|
+
description="Test cost tracking",
|
|
570
|
+
board_id=board.id,
|
|
571
|
+
)
|
|
572
|
+
db.add(goal)
|
|
573
|
+
await db.commit()
|
|
574
|
+
|
|
575
|
+
service = UDARPlannerService(db)
|
|
576
|
+
|
|
577
|
+
# Mock successful state with token counts
|
|
578
|
+
test_state = {
|
|
579
|
+
"goal_id": goal.id,
|
|
580
|
+
"phase": "review",
|
|
581
|
+
"final_tickets": [],
|
|
582
|
+
"review_summary": "Test",
|
|
583
|
+
"llm_calls_made": 1,
|
|
584
|
+
"errors": [],
|
|
585
|
+
"total_input_tokens": 1000,
|
|
586
|
+
"total_output_tokens": 500,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Call cost tracking - should not raise, should log cost info
|
|
590
|
+
with caplog.at_level(logging.INFO):
|
|
591
|
+
await service._track_agent_session(goal.id, test_state)
|
|
592
|
+
|
|
593
|
+
# Verify cost was logged
|
|
594
|
+
assert "1000 input tokens" in caplog.text
|
|
595
|
+
assert "500 output tokens" in caplog.text
|
|
596
|
+
assert goal.id in caplog.text
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
@pytest.mark.asyncio
|
|
600
|
+
async def test_phase5_config_loads():
|
|
601
|
+
"""Test Phase 5 configuration options load correctly."""
|
|
602
|
+
from app.services.config_service import ConfigService
|
|
603
|
+
|
|
604
|
+
config = ConfigService().load_config()
|
|
605
|
+
|
|
606
|
+
# Verify Phase 5 settings exist and have defaults
|
|
607
|
+
assert hasattr(config.planner_config.udar, "fallback_to_legacy")
|
|
608
|
+
assert config.planner_config.udar.fallback_to_legacy is True
|
|
609
|
+
|
|
610
|
+
assert hasattr(config.planner_config.udar, "timeout_seconds")
|
|
611
|
+
assert config.planner_config.udar.timeout_seconds == 120
|
|
612
|
+
|
|
613
|
+
assert hasattr(config.planner_config.udar, "enable_cost_tracking")
|
|
614
|
+
assert config.planner_config.udar.enable_cost_tracking is True
|
|
615
|
+
|
|
616
|
+
assert hasattr(config.planner_config.udar, "max_retries_on_error")
|
|
617
|
+
assert config.planner_config.udar.max_retries_on_error == 0
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
@pytest.mark.asyncio
|
|
621
|
+
async def test_udar_graceful_degradation(db):
|
|
622
|
+
"""Test UDAR degrades gracefully on unexpected errors."""
|
|
623
|
+
from app.exceptions import UDARAgentError
|
|
624
|
+
from app.models.board import Board
|
|
625
|
+
from app.models.goal import Goal
|
|
626
|
+
from app.services.udar_planner_service import UDARPlannerService
|
|
627
|
+
|
|
628
|
+
# Create test board and goal
|
|
629
|
+
board = Board(
|
|
630
|
+
id="test-board-4",
|
|
631
|
+
name="Test Board",
|
|
632
|
+
repo_root=".",
|
|
633
|
+
)
|
|
634
|
+
db.add(board)
|
|
635
|
+
|
|
636
|
+
goal = Goal(
|
|
637
|
+
id="test-goal-degradation",
|
|
638
|
+
title="Test Goal",
|
|
639
|
+
description="Test graceful degradation",
|
|
640
|
+
board_id=board.id,
|
|
641
|
+
)
|
|
642
|
+
db.add(goal)
|
|
643
|
+
await db.commit()
|
|
644
|
+
|
|
645
|
+
service = UDARPlannerService(db)
|
|
646
|
+
|
|
647
|
+
mock_fallback_result = {
|
|
648
|
+
"tickets": [],
|
|
649
|
+
"summary": "Fallback result",
|
|
650
|
+
"llm_calls_made": 1,
|
|
651
|
+
"phases_completed": ["legacy"],
|
|
652
|
+
"errors": ["UDAR fallback: unexpected_error"],
|
|
653
|
+
"used_legacy_fallback": True,
|
|
654
|
+
"fallback_reason": "unexpected_error",
|
|
655
|
+
"cost_tracking": {"input_tokens": 0, "output_tokens": 0},
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
# With fallback enabled, should handle gracefully
|
|
659
|
+
with (
|
|
660
|
+
patch.object(
|
|
661
|
+
service.agent,
|
|
662
|
+
"ainvoke",
|
|
663
|
+
side_effect=RuntimeError("Unexpected error in LangGraph"),
|
|
664
|
+
),
|
|
665
|
+
patch.object(
|
|
666
|
+
service,
|
|
667
|
+
"_fallback_to_legacy",
|
|
668
|
+
new_callable=AsyncMock,
|
|
669
|
+
return_value=mock_fallback_result,
|
|
670
|
+
),
|
|
671
|
+
):
|
|
672
|
+
result = await service.generate_from_goal(goal.id, fallback_to_legacy=True)
|
|
673
|
+
assert result["used_legacy_fallback"] is True
|
|
674
|
+
assert result["fallback_reason"] == "unexpected_error"
|
|
675
|
+
|
|
676
|
+
# With fallback disabled, should raise UDARAgentError
|
|
677
|
+
with patch.object(
|
|
678
|
+
service.agent,
|
|
679
|
+
"ainvoke",
|
|
680
|
+
side_effect=RuntimeError("Unexpected error in LangGraph"),
|
|
681
|
+
):
|
|
682
|
+
with pytest.raises(UDARAgentError):
|
|
683
|
+
await service.generate_from_goal(goal.id, fallback_to_legacy=False)
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
# Phase 5 verification checklist:
|
|
687
|
+
# [x] Timeout handling with fallback to legacy
|
|
688
|
+
# [x] Tool execution error handling with fallback
|
|
689
|
+
# [x] Cost tracking in AgentSession
|
|
690
|
+
# [x] Phase 5 configuration loads correctly
|
|
691
|
+
# [x] Graceful degradation on unexpected errors
|
|
692
|
+
# [ ] Rate limiting enforced for UDAR endpoints (requires HTTP test)
|
|
693
|
+
# [ ] Telemetry/monitoring metrics (requires Prometheus integration)
|