kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of kubiya-control-plane-api might be problematic. Click here for more details.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Test suite for worker files"""
|
|
File without changes
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
"""End-to-end tests for full execution flow
|
|
2
|
+
|
|
3
|
+
These tests verify the complete flow from Control Plane → Worker → Database → UI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import os
|
|
8
|
+
import asyncio
|
|
9
|
+
from unittest.mock import Mock, patch, MagicMock, AsyncMock
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
14
|
+
|
|
15
|
+
from control_plane_api.worker.control_plane_client import ControlPlaneClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MockRedis:
|
|
19
|
+
"""Mock Redis client for E2E tests"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.store = {}
|
|
23
|
+
|
|
24
|
+
async def set(self, key: str, value: str, ex: int = None):
|
|
25
|
+
"""Mock Redis set"""
|
|
26
|
+
self.store[key] = {"value": value, "ttl": ex}
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
async def get(self, key: str):
|
|
30
|
+
"""Mock Redis get"""
|
|
31
|
+
if key in self.store:
|
|
32
|
+
return self.store[key]["value"]
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
async def exists(self, key: str):
|
|
36
|
+
"""Mock Redis exists"""
|
|
37
|
+
return key in self.store
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MockDatabase:
|
|
41
|
+
"""Mock database for E2E tests"""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self.executions = {}
|
|
45
|
+
self.sessions = []
|
|
46
|
+
|
|
47
|
+
def insert_execution(self, execution_id: str, data: dict):
|
|
48
|
+
"""Mock execution insert"""
|
|
49
|
+
self.executions[execution_id] = data
|
|
50
|
+
|
|
51
|
+
def update_execution(self, execution_id: str, updates: dict):
|
|
52
|
+
"""Mock execution update"""
|
|
53
|
+
if execution_id in self.executions:
|
|
54
|
+
self.executions[execution_id].update(updates)
|
|
55
|
+
|
|
56
|
+
def get_execution(self, execution_id: str):
|
|
57
|
+
"""Mock execution get"""
|
|
58
|
+
return self.executions.get(execution_id)
|
|
59
|
+
|
|
60
|
+
def insert_session(self, session_id: str, data: dict):
|
|
61
|
+
"""Mock session insert"""
|
|
62
|
+
self.sessions.append({"session_id": session_id, **data})
|
|
63
|
+
|
|
64
|
+
def get_session(self, session_id: str):
|
|
65
|
+
"""Mock session get"""
|
|
66
|
+
for session in self.sessions:
|
|
67
|
+
if session["session_id"] == session_id:
|
|
68
|
+
return session
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def mock_redis():
|
|
74
|
+
"""Fixture providing mock Redis"""
|
|
75
|
+
return MockRedis()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def mock_db():
|
|
80
|
+
"""Fixture providing mock Database"""
|
|
81
|
+
return MockDatabase()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.fixture
|
|
85
|
+
def control_plane_client():
|
|
86
|
+
"""Fixture providing real ControlPlaneClient for E2E testing"""
|
|
87
|
+
return ControlPlaneClient(
|
|
88
|
+
base_url="http://localhost:8000",
|
|
89
|
+
api_key="test_e2e_key"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TestAgentExecutionFlow:
|
|
94
|
+
"""Test complete agent execution flow"""
|
|
95
|
+
|
|
96
|
+
@pytest.mark.asyncio
|
|
97
|
+
async def test_complete_agent_execution_flow(self, mock_redis, mock_db, control_plane_client):
|
|
98
|
+
"""
|
|
99
|
+
Test full agent execution flow:
|
|
100
|
+
1. Control Plane receives request
|
|
101
|
+
2. Worker picks up task
|
|
102
|
+
3. Worker executes agent
|
|
103
|
+
4. Worker streams events to Control Plane
|
|
104
|
+
5. Worker persists session
|
|
105
|
+
6. Control Plane updates database
|
|
106
|
+
7. UI receives SSE events
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
execution_id = "e2e_agent_exec_123"
|
|
110
|
+
|
|
111
|
+
# Step 1: Control Plane creates execution
|
|
112
|
+
mock_db.insert_execution(execution_id, {
|
|
113
|
+
"status": "pending",
|
|
114
|
+
"execution_type": "AGENT",
|
|
115
|
+
"agent_id": "agent_456"
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Step 2: Mock HTTP responses for worker activity
|
|
119
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
120
|
+
with patch.object(control_plane_client._client, 'get') as mock_get:
|
|
121
|
+
# Mock event publishing (streaming)
|
|
122
|
+
mock_post_response = Mock()
|
|
123
|
+
mock_post_response.status_code = 200
|
|
124
|
+
mock_post.return_value = mock_post_response
|
|
125
|
+
|
|
126
|
+
# Mock skill fetching
|
|
127
|
+
mock_get_response = Mock()
|
|
128
|
+
mock_get_response.status_code = 200
|
|
129
|
+
mock_get_response.json = Mock(return_value=[
|
|
130
|
+
{"type": "file_system", "name": "File Tools", "enabled": True}
|
|
131
|
+
])
|
|
132
|
+
mock_get.return_value = mock_get_response
|
|
133
|
+
|
|
134
|
+
# Step 3: Worker caches metadata
|
|
135
|
+
result = control_plane_client.cache_metadata(execution_id, "AGENT")
|
|
136
|
+
assert result is True
|
|
137
|
+
|
|
138
|
+
# Verify metadata event was published
|
|
139
|
+
assert mock_post.called
|
|
140
|
+
metadata_call = mock_post.call_args_list[0]
|
|
141
|
+
assert "metadata" in str(metadata_call)
|
|
142
|
+
|
|
143
|
+
# Step 4: Worker fetches skills
|
|
144
|
+
skills = control_plane_client.get_skills("agent_456")
|
|
145
|
+
assert len(skills) == 1
|
|
146
|
+
assert skills[0]["type"] == "file_system"
|
|
147
|
+
|
|
148
|
+
# Step 5: Worker streams message chunks
|
|
149
|
+
chunks = ["Hello ", "from ", "agent!"]
|
|
150
|
+
for chunk in chunks:
|
|
151
|
+
result = control_plane_client.publish_event(
|
|
152
|
+
execution_id=execution_id,
|
|
153
|
+
event_type="message_chunk",
|
|
154
|
+
data={"content": chunk, "role": "assistant"}
|
|
155
|
+
)
|
|
156
|
+
assert result is True
|
|
157
|
+
|
|
158
|
+
# Verify all chunks were published
|
|
159
|
+
assert mock_post.call_count >= 4 # metadata + 3 chunks
|
|
160
|
+
|
|
161
|
+
# Step 6: Worker persists session
|
|
162
|
+
messages = [
|
|
163
|
+
{"role": "user", "content": "Hello"},
|
|
164
|
+
{"role": "assistant", "content": "Hello from agent!"}
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
result = control_plane_client.persist_session(
|
|
168
|
+
execution_id=execution_id,
|
|
169
|
+
session_id=execution_id,
|
|
170
|
+
user_id="user_789",
|
|
171
|
+
messages=messages
|
|
172
|
+
)
|
|
173
|
+
assert result is True
|
|
174
|
+
|
|
175
|
+
# Step 7: Verify Control Plane would update database
|
|
176
|
+
mock_db.update_execution(execution_id, {
|
|
177
|
+
"status": "completed",
|
|
178
|
+
"response": "Hello from agent!",
|
|
179
|
+
"usage": {"input_tokens": 10, "output_tokens": 20}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
execution = mock_db.get_execution(execution_id)
|
|
183
|
+
assert execution["status"] == "completed"
|
|
184
|
+
assert execution["response"] == "Hello from agent!"
|
|
185
|
+
|
|
186
|
+
@pytest.mark.asyncio
|
|
187
|
+
async def test_agent_execution_with_tool_calls(self, mock_redis, mock_db, control_plane_client):
|
|
188
|
+
"""Test agent execution flow including tool calls"""
|
|
189
|
+
|
|
190
|
+
execution_id = "e2e_agent_tools_123"
|
|
191
|
+
|
|
192
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
193
|
+
mock_response = Mock()
|
|
194
|
+
mock_response.status_code = 200
|
|
195
|
+
mock_post.return_value = mock_response
|
|
196
|
+
|
|
197
|
+
# Cache metadata
|
|
198
|
+
control_plane_client.cache_metadata(execution_id, "AGENT")
|
|
199
|
+
|
|
200
|
+
# Stream tool started event
|
|
201
|
+
control_plane_client.publish_event(
|
|
202
|
+
execution_id=execution_id,
|
|
203
|
+
event_type="tool_started",
|
|
204
|
+
data={
|
|
205
|
+
"tool_name": "read_file",
|
|
206
|
+
"tool_execution_id": "tool_1",
|
|
207
|
+
"tool_arguments": {"path": "test.txt"}
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Stream tool completed event
|
|
212
|
+
control_plane_client.publish_event(
|
|
213
|
+
execution_id=execution_id,
|
|
214
|
+
event_type="tool_completed",
|
|
215
|
+
data={
|
|
216
|
+
"tool_name": "read_file",
|
|
217
|
+
"tool_execution_id": "tool_1",
|
|
218
|
+
"status": "success"
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Verify events were published
|
|
223
|
+
assert mock_post.call_count >= 3 # metadata + tool_started + tool_completed
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class TestTeamExecutionFlow:
|
|
227
|
+
"""Test complete team execution flow"""
|
|
228
|
+
|
|
229
|
+
@pytest.mark.asyncio
|
|
230
|
+
async def test_complete_team_execution_flow(self, mock_redis, mock_db, control_plane_client):
|
|
231
|
+
"""
|
|
232
|
+
Test full team execution flow:
|
|
233
|
+
1. Control Plane receives team request
|
|
234
|
+
2. Worker picks up task
|
|
235
|
+
3. Worker executes team coordination
|
|
236
|
+
4. Worker streams team leader and member events
|
|
237
|
+
5. Worker persists team session
|
|
238
|
+
6. Control Plane updates database
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
execution_id = "e2e_team_exec_123"
|
|
242
|
+
|
|
243
|
+
# Step 1: Control Plane creates execution
|
|
244
|
+
mock_db.insert_execution(execution_id, {
|
|
245
|
+
"status": "pending",
|
|
246
|
+
"execution_type": "TEAM",
|
|
247
|
+
"team_id": "team_456"
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
# Step 2: Mock HTTP responses
|
|
251
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
252
|
+
mock_response = Mock()
|
|
253
|
+
mock_response.status_code = 200
|
|
254
|
+
mock_post.return_value = mock_response
|
|
255
|
+
|
|
256
|
+
# Step 3: Worker caches metadata
|
|
257
|
+
result = control_plane_client.cache_metadata(execution_id, "TEAM")
|
|
258
|
+
assert result is True
|
|
259
|
+
|
|
260
|
+
# Step 4: Worker streams team leader message
|
|
261
|
+
control_plane_client.publish_event(
|
|
262
|
+
execution_id=execution_id,
|
|
263
|
+
event_type="message_chunk",
|
|
264
|
+
data={
|
|
265
|
+
"role": "assistant",
|
|
266
|
+
"content": "Team leader response",
|
|
267
|
+
"source": "team_leader"
|
|
268
|
+
}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Step 5: Worker streams member message
|
|
272
|
+
control_plane_client.publish_event(
|
|
273
|
+
execution_id=execution_id,
|
|
274
|
+
event_type="member_message_chunk",
|
|
275
|
+
data={
|
|
276
|
+
"role": "assistant",
|
|
277
|
+
"content": "Member response",
|
|
278
|
+
"source": "team_member",
|
|
279
|
+
"member_name": "Agent 1"
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Step 6: Worker persists team session
|
|
284
|
+
messages = [
|
|
285
|
+
{"role": "user", "content": "Team task"},
|
|
286
|
+
{"role": "assistant", "content": "Team leader response"},
|
|
287
|
+
{"role": "assistant", "content": "Member response", "member_name": "Agent 1"}
|
|
288
|
+
]
|
|
289
|
+
|
|
290
|
+
result = control_plane_client.persist_session(
|
|
291
|
+
execution_id=execution_id,
|
|
292
|
+
session_id=execution_id,
|
|
293
|
+
user_id="user_789",
|
|
294
|
+
messages=messages,
|
|
295
|
+
metadata={"team_id": "team_456"}
|
|
296
|
+
)
|
|
297
|
+
assert result is True
|
|
298
|
+
|
|
299
|
+
# Step 7: Verify database update
|
|
300
|
+
mock_db.update_execution(execution_id, {
|
|
301
|
+
"status": "completed",
|
|
302
|
+
"response": "Team leader response"
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
execution = mock_db.get_execution(execution_id)
|
|
306
|
+
assert execution["status"] == "completed"
|
|
307
|
+
|
|
308
|
+
@pytest.mark.asyncio
|
|
309
|
+
async def test_team_execution_with_hitl(self, mock_redis, mock_db, control_plane_client):
|
|
310
|
+
"""Test team execution with Human-in-the-Loop (multiple turns)"""
|
|
311
|
+
|
|
312
|
+
execution_id = "e2e_team_hitl_123"
|
|
313
|
+
|
|
314
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
315
|
+
mock_response = Mock()
|
|
316
|
+
mock_response.status_code = 200
|
|
317
|
+
mock_post.return_value = mock_response
|
|
318
|
+
|
|
319
|
+
# Turn 1: Initial request
|
|
320
|
+
control_plane_client.cache_metadata(execution_id, "TEAM")
|
|
321
|
+
|
|
322
|
+
control_plane_client.publish_event(
|
|
323
|
+
execution_id=execution_id,
|
|
324
|
+
event_type="message_chunk",
|
|
325
|
+
data={"content": "Turn 1 response"}
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
messages_turn_1 = [
|
|
329
|
+
{"role": "user", "content": "Initial request"},
|
|
330
|
+
{"role": "assistant", "content": "Turn 1 response"}
|
|
331
|
+
]
|
|
332
|
+
|
|
333
|
+
control_plane_client.persist_session(
|
|
334
|
+
execution_id=execution_id,
|
|
335
|
+
session_id=execution_id,
|
|
336
|
+
user_id="user_789",
|
|
337
|
+
messages=messages_turn_1
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# Turn 2: Follow-up request
|
|
341
|
+
control_plane_client.publish_event(
|
|
342
|
+
execution_id=execution_id,
|
|
343
|
+
event_type="message_chunk",
|
|
344
|
+
data={"content": "Turn 2 response"}
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
messages_turn_2 = [
|
|
348
|
+
{"role": "user", "content": "Initial request"},
|
|
349
|
+
{"role": "assistant", "content": "Turn 1 response"},
|
|
350
|
+
{"role": "user", "content": "Follow-up question"},
|
|
351
|
+
{"role": "assistant", "content": "Turn 2 response"}
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
control_plane_client.persist_session(
|
|
355
|
+
execution_id=execution_id,
|
|
356
|
+
session_id=execution_id,
|
|
357
|
+
user_id="user_789",
|
|
358
|
+
messages=messages_turn_2
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Verify both turns were persisted
|
|
362
|
+
assert mock_post.call_count >= 4 # metadata + 2 chunks + 2 persist calls
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class TestSessionPersistence:
|
|
366
|
+
"""Test session persistence and history retrieval"""
|
|
367
|
+
|
|
368
|
+
@pytest.mark.asyncio
|
|
369
|
+
async def test_session_history_persists_when_worker_offline(self, mock_db, control_plane_client):
|
|
370
|
+
"""
|
|
371
|
+
Test that session history is available even when worker is offline:
|
|
372
|
+
1. Worker executes and persists session
|
|
373
|
+
2. Worker goes offline
|
|
374
|
+
3. UI requests execution history
|
|
375
|
+
4. Control Plane loads from database (not Redis)
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
execution_id = "e2e_session_persist_123"
|
|
379
|
+
session_id = execution_id
|
|
380
|
+
|
|
381
|
+
# Step 1: Worker persists session
|
|
382
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
383
|
+
mock_response = Mock()
|
|
384
|
+
mock_response.status_code = 201
|
|
385
|
+
mock_post.return_value = mock_response
|
|
386
|
+
|
|
387
|
+
messages = [
|
|
388
|
+
{"role": "user", "content": "Hello", "timestamp": "2024-01-01T00:00:00Z"},
|
|
389
|
+
{"role": "assistant", "content": "Hi!", "timestamp": "2024-01-01T00:00:01Z"}
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
result = control_plane_client.persist_session(
|
|
393
|
+
execution_id=execution_id,
|
|
394
|
+
session_id=session_id,
|
|
395
|
+
user_id="user_123",
|
|
396
|
+
messages=messages
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
assert result is True
|
|
400
|
+
|
|
401
|
+
# Step 2: Simulate Control Plane storing in database
|
|
402
|
+
mock_db.insert_session(session_id, {
|
|
403
|
+
"user_id": "user_123",
|
|
404
|
+
"messages": messages,
|
|
405
|
+
"execution_id": execution_id
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
# Step 3: Worker goes offline (no Redis events)
|
|
409
|
+
|
|
410
|
+
# Step 4: Control Plane loads from database
|
|
411
|
+
session_data = mock_db.get_session(session_id)
|
|
412
|
+
assert session_data is not None
|
|
413
|
+
assert len(session_data["messages"]) == 2
|
|
414
|
+
assert session_data["user_id"] == "user_123"
|
|
415
|
+
|
|
416
|
+
@pytest.mark.asyncio
|
|
417
|
+
async def test_multi_user_session_isolation(self, mock_db, control_plane_client):
|
|
418
|
+
"""Test that sessions are properly isolated by user_id"""
|
|
419
|
+
|
|
420
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
421
|
+
mock_response = Mock()
|
|
422
|
+
mock_response.status_code = 201
|
|
423
|
+
mock_post.return_value = mock_response
|
|
424
|
+
|
|
425
|
+
# User 1 session
|
|
426
|
+
control_plane_client.persist_session(
|
|
427
|
+
execution_id="exec_user1",
|
|
428
|
+
session_id="session_user1",
|
|
429
|
+
user_id="user_1",
|
|
430
|
+
messages=[{"role": "user", "content": "User 1 message"}]
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# User 2 session
|
|
434
|
+
control_plane_client.persist_session(
|
|
435
|
+
execution_id="exec_user2",
|
|
436
|
+
session_id="session_user2",
|
|
437
|
+
user_id="user_2",
|
|
438
|
+
messages=[{"role": "user", "content": "User 2 message"}]
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# Store in database
|
|
442
|
+
mock_db.insert_session("session_user1", {
|
|
443
|
+
"user_id": "user_1",
|
|
444
|
+
"messages": [{"role": "user", "content": "User 1 message"}]
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
mock_db.insert_session("session_user2", {
|
|
448
|
+
"user_id": "user_2",
|
|
449
|
+
"messages": [{"role": "user", "content": "User 2 message"}]
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
# Verify isolation
|
|
453
|
+
session1 = mock_db.get_session("session_user1")
|
|
454
|
+
session2 = mock_db.get_session("session_user2")
|
|
455
|
+
|
|
456
|
+
assert session1["user_id"] == "user_1"
|
|
457
|
+
assert session2["user_id"] == "user_2"
|
|
458
|
+
assert session1["messages"][0]["content"] == "User 1 message"
|
|
459
|
+
assert session2["messages"][0]["content"] == "User 2 message"
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
class TestErrorHandlingE2E:
|
|
463
|
+
"""Test end-to-end error handling"""
|
|
464
|
+
|
|
465
|
+
@pytest.mark.asyncio
|
|
466
|
+
async def test_execution_failure_updates_status(self, mock_db, control_plane_client):
|
|
467
|
+
"""Test that execution failures are properly recorded"""
|
|
468
|
+
|
|
469
|
+
execution_id = "e2e_error_123"
|
|
470
|
+
|
|
471
|
+
mock_db.insert_execution(execution_id, {
|
|
472
|
+
"status": "pending"
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
# Simulate execution failure
|
|
476
|
+
# In real scenario, this would come from activity error handling
|
|
477
|
+
|
|
478
|
+
mock_db.update_execution(execution_id, {
|
|
479
|
+
"status": "failed",
|
|
480
|
+
"error_message": "Agent execution failed: Model timeout"
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
execution = mock_db.get_execution(execution_id)
|
|
484
|
+
assert execution["status"] == "failed"
|
|
485
|
+
assert "timeout" in execution["error_message"]
|
|
486
|
+
|
|
487
|
+
@pytest.mark.asyncio
|
|
488
|
+
async def test_network_failure_during_streaming(self, control_plane_client):
|
|
489
|
+
"""Test handling of network failures during event streaming"""
|
|
490
|
+
|
|
491
|
+
execution_id = "e2e_network_fail_123"
|
|
492
|
+
|
|
493
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
494
|
+
# Simulate network failure
|
|
495
|
+
import httpx
|
|
496
|
+
mock_post.side_effect = httpx.ConnectError("Connection failed")
|
|
497
|
+
|
|
498
|
+
# Should not raise exception
|
|
499
|
+
result = control_plane_client.publish_event(
|
|
500
|
+
execution_id=execution_id,
|
|
501
|
+
event_type="message_chunk",
|
|
502
|
+
data={"content": "test"}
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
# Should return False but not crash
|
|
506
|
+
assert result is False
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
class TestPerformanceE2E:
|
|
510
|
+
"""Test performance characteristics end-to-end"""
|
|
511
|
+
|
|
512
|
+
@pytest.mark.asyncio
|
|
513
|
+
async def test_high_frequency_event_streaming(self, control_plane_client):
|
|
514
|
+
"""Test that high-frequency event streaming works reliably"""
|
|
515
|
+
|
|
516
|
+
execution_id = "e2e_perf_123"
|
|
517
|
+
|
|
518
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
519
|
+
mock_response = Mock()
|
|
520
|
+
mock_response.status_code = 200
|
|
521
|
+
mock_post.return_value = mock_response
|
|
522
|
+
|
|
523
|
+
# Stream 100 events rapidly
|
|
524
|
+
for i in range(100):
|
|
525
|
+
result = control_plane_client.publish_event(
|
|
526
|
+
execution_id=execution_id,
|
|
527
|
+
event_type="message_chunk",
|
|
528
|
+
data={"content": f"Chunk {i}"}
|
|
529
|
+
)
|
|
530
|
+
assert result is True
|
|
531
|
+
|
|
532
|
+
# All events should have been published
|
|
533
|
+
assert mock_post.call_count == 100
|
|
534
|
+
|
|
535
|
+
@pytest.mark.asyncio
|
|
536
|
+
async def test_large_session_persistence(self, control_plane_client):
|
|
537
|
+
"""Test persisting large sessions with many messages"""
|
|
538
|
+
|
|
539
|
+
execution_id = "e2e_large_session_123"
|
|
540
|
+
|
|
541
|
+
with patch.object(control_plane_client._client, 'post') as mock_post:
|
|
542
|
+
mock_response = Mock()
|
|
543
|
+
mock_response.status_code = 201
|
|
544
|
+
mock_post.return_value = mock_response
|
|
545
|
+
|
|
546
|
+
# Create large session with 50 messages
|
|
547
|
+
messages = []
|
|
548
|
+
for i in range(50):
|
|
549
|
+
messages.append({
|
|
550
|
+
"role": "user" if i % 2 == 0 else "assistant",
|
|
551
|
+
"content": f"Message {i} content" * 10, # Longer content
|
|
552
|
+
"timestamp": f"2024-01-01T00:{i:02d}:00Z"
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
result = control_plane_client.persist_session(
|
|
556
|
+
execution_id=execution_id,
|
|
557
|
+
session_id=execution_id,
|
|
558
|
+
user_id="user_123",
|
|
559
|
+
messages=messages
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
assert result is True
|
|
563
|
+
|
|
564
|
+
# Verify payload structure
|
|
565
|
+
call_args = mock_post.call_args
|
|
566
|
+
payload = call_args[1]["json"]
|
|
567
|
+
assert len(payload["messages"]) == 50
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
if __name__ == "__main__":
|
|
571
|
+
pytest.main([__file__, "-v", "--tb=short"])
|
|
File without changes
|