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.

Files changed (185) hide show
  1. control_plane_api/README.md +266 -0
  2. control_plane_api/__init__.py +0 -0
  3. control_plane_api/__version__.py +1 -0
  4. control_plane_api/alembic/README +1 -0
  5. control_plane_api/alembic/env.py +98 -0
  6. control_plane_api/alembic/script.py.mako +28 -0
  7. control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
  8. control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
  9. control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
  10. control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
  11. control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
  12. control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
  13. control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
  14. control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
  15. control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
  16. control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
  17. control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -0
  18. control_plane_api/alembic.ini +148 -0
  19. control_plane_api/api/index.py +12 -0
  20. control_plane_api/app/__init__.py +11 -0
  21. control_plane_api/app/activities/__init__.py +20 -0
  22. control_plane_api/app/activities/agent_activities.py +379 -0
  23. control_plane_api/app/activities/team_activities.py +410 -0
  24. control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
  25. control_plane_api/app/config/__init__.py +35 -0
  26. control_plane_api/app/config/api_config.py +354 -0
  27. control_plane_api/app/config/model_pricing.py +318 -0
  28. control_plane_api/app/config.py +95 -0
  29. control_plane_api/app/database.py +135 -0
  30. control_plane_api/app/exceptions.py +408 -0
  31. control_plane_api/app/lib/__init__.py +11 -0
  32. control_plane_api/app/lib/job_executor.py +312 -0
  33. control_plane_api/app/lib/kubiya_client.py +235 -0
  34. control_plane_api/app/lib/litellm_pricing.py +166 -0
  35. control_plane_api/app/lib/planning_tools/__init__.py +22 -0
  36. control_plane_api/app/lib/planning_tools/agents.py +155 -0
  37. control_plane_api/app/lib/planning_tools/base.py +189 -0
  38. control_plane_api/app/lib/planning_tools/environments.py +214 -0
  39. control_plane_api/app/lib/planning_tools/resources.py +240 -0
  40. control_plane_api/app/lib/planning_tools/teams.py +198 -0
  41. control_plane_api/app/lib/policy_enforcer_client.py +939 -0
  42. control_plane_api/app/lib/redis_client.py +436 -0
  43. control_plane_api/app/lib/supabase.py +71 -0
  44. control_plane_api/app/lib/temporal_client.py +138 -0
  45. control_plane_api/app/lib/validation/__init__.py +20 -0
  46. control_plane_api/app/lib/validation/runtime_validation.py +287 -0
  47. control_plane_api/app/main.py +128 -0
  48. control_plane_api/app/middleware/__init__.py +8 -0
  49. control_plane_api/app/middleware/auth.py +513 -0
  50. control_plane_api/app/middleware/exception_handler.py +267 -0
  51. control_plane_api/app/middleware/rate_limiting.py +384 -0
  52. control_plane_api/app/middleware/request_id.py +202 -0
  53. control_plane_api/app/models/__init__.py +27 -0
  54. control_plane_api/app/models/agent.py +79 -0
  55. control_plane_api/app/models/analytics.py +206 -0
  56. control_plane_api/app/models/associations.py +81 -0
  57. control_plane_api/app/models/environment.py +63 -0
  58. control_plane_api/app/models/execution.py +93 -0
  59. control_plane_api/app/models/job.py +179 -0
  60. control_plane_api/app/models/llm_model.py +75 -0
  61. control_plane_api/app/models/presence.py +49 -0
  62. control_plane_api/app/models/project.py +47 -0
  63. control_plane_api/app/models/session.py +38 -0
  64. control_plane_api/app/models/team.py +66 -0
  65. control_plane_api/app/models/workflow.py +55 -0
  66. control_plane_api/app/policies/README.md +121 -0
  67. control_plane_api/app/policies/approved_users.rego +62 -0
  68. control_plane_api/app/policies/business_hours.rego +51 -0
  69. control_plane_api/app/policies/rate_limiting.rego +100 -0
  70. control_plane_api/app/policies/tool_restrictions.rego +86 -0
  71. control_plane_api/app/routers/__init__.py +4 -0
  72. control_plane_api/app/routers/agents.py +364 -0
  73. control_plane_api/app/routers/agents_v2.py +1260 -0
  74. control_plane_api/app/routers/analytics.py +1014 -0
  75. control_plane_api/app/routers/context_manager.py +562 -0
  76. control_plane_api/app/routers/environment_context.py +270 -0
  77. control_plane_api/app/routers/environments.py +715 -0
  78. control_plane_api/app/routers/execution_environment.py +517 -0
  79. control_plane_api/app/routers/executions.py +1911 -0
  80. control_plane_api/app/routers/health.py +92 -0
  81. control_plane_api/app/routers/health_v2.py +326 -0
  82. control_plane_api/app/routers/integrations.py +274 -0
  83. control_plane_api/app/routers/jobs.py +1344 -0
  84. control_plane_api/app/routers/models.py +82 -0
  85. control_plane_api/app/routers/models_v2.py +361 -0
  86. control_plane_api/app/routers/policies.py +639 -0
  87. control_plane_api/app/routers/presence.py +234 -0
  88. control_plane_api/app/routers/projects.py +902 -0
  89. control_plane_api/app/routers/runners.py +379 -0
  90. control_plane_api/app/routers/runtimes.py +172 -0
  91. control_plane_api/app/routers/secrets.py +155 -0
  92. control_plane_api/app/routers/skills.py +1001 -0
  93. control_plane_api/app/routers/skills_definitions.py +140 -0
  94. control_plane_api/app/routers/task_planning.py +1256 -0
  95. control_plane_api/app/routers/task_queues.py +654 -0
  96. control_plane_api/app/routers/team_context.py +270 -0
  97. control_plane_api/app/routers/teams.py +1400 -0
  98. control_plane_api/app/routers/worker_queues.py +1545 -0
  99. control_plane_api/app/routers/workers.py +935 -0
  100. control_plane_api/app/routers/workflows.py +204 -0
  101. control_plane_api/app/runtimes/__init__.py +6 -0
  102. control_plane_api/app/runtimes/validation.py +344 -0
  103. control_plane_api/app/schemas/job_schemas.py +295 -0
  104. control_plane_api/app/services/__init__.py +1 -0
  105. control_plane_api/app/services/agno_service.py +619 -0
  106. control_plane_api/app/services/litellm_service.py +190 -0
  107. control_plane_api/app/services/policy_service.py +525 -0
  108. control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
  109. control_plane_api/app/skills/__init__.py +44 -0
  110. control_plane_api/app/skills/base.py +229 -0
  111. control_plane_api/app/skills/business_intelligence.py +189 -0
  112. control_plane_api/app/skills/data_visualization.py +154 -0
  113. control_plane_api/app/skills/docker.py +104 -0
  114. control_plane_api/app/skills/file_generation.py +94 -0
  115. control_plane_api/app/skills/file_system.py +110 -0
  116. control_plane_api/app/skills/python.py +92 -0
  117. control_plane_api/app/skills/registry.py +65 -0
  118. control_plane_api/app/skills/shell.py +102 -0
  119. control_plane_api/app/skills/workflow_executor.py +469 -0
  120. control_plane_api/app/utils/workflow_executor.py +354 -0
  121. control_plane_api/app/workflows/__init__.py +11 -0
  122. control_plane_api/app/workflows/agent_execution.py +507 -0
  123. control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
  124. control_plane_api/app/workflows/namespace_provisioning.py +326 -0
  125. control_plane_api/app/workflows/team_execution.py +399 -0
  126. control_plane_api/scripts/seed_models.py +239 -0
  127. control_plane_api/worker/__init__.py +0 -0
  128. control_plane_api/worker/activities/__init__.py +0 -0
  129. control_plane_api/worker/activities/agent_activities.py +1241 -0
  130. control_plane_api/worker/activities/approval_activities.py +234 -0
  131. control_plane_api/worker/activities/runtime_activities.py +388 -0
  132. control_plane_api/worker/activities/skill_activities.py +267 -0
  133. control_plane_api/worker/activities/team_activities.py +1217 -0
  134. control_plane_api/worker/config/__init__.py +31 -0
  135. control_plane_api/worker/config/worker_config.py +275 -0
  136. control_plane_api/worker/control_plane_client.py +529 -0
  137. control_plane_api/worker/examples/analytics_integration_example.py +362 -0
  138. control_plane_api/worker/models/__init__.py +1 -0
  139. control_plane_api/worker/models/inputs.py +89 -0
  140. control_plane_api/worker/runtimes/__init__.py +31 -0
  141. control_plane_api/worker/runtimes/base.py +789 -0
  142. control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
  143. control_plane_api/worker/runtimes/default_runtime.py +617 -0
  144. control_plane_api/worker/runtimes/factory.py +173 -0
  145. control_plane_api/worker/runtimes/validation.py +93 -0
  146. control_plane_api/worker/services/__init__.py +1 -0
  147. control_plane_api/worker/services/agent_executor.py +422 -0
  148. control_plane_api/worker/services/agent_executor_v2.py +383 -0
  149. control_plane_api/worker/services/analytics_collector.py +457 -0
  150. control_plane_api/worker/services/analytics_service.py +464 -0
  151. control_plane_api/worker/services/approval_tools.py +310 -0
  152. control_plane_api/worker/services/approval_tools_agno.py +207 -0
  153. control_plane_api/worker/services/cancellation_manager.py +177 -0
  154. control_plane_api/worker/services/data_visualization.py +827 -0
  155. control_plane_api/worker/services/jira_tools.py +257 -0
  156. control_plane_api/worker/services/runtime_analytics.py +328 -0
  157. control_plane_api/worker/services/session_service.py +194 -0
  158. control_plane_api/worker/services/skill_factory.py +175 -0
  159. control_plane_api/worker/services/team_executor.py +574 -0
  160. control_plane_api/worker/services/team_executor_v2.py +465 -0
  161. control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
  162. control_plane_api/worker/tests/__init__.py +1 -0
  163. control_plane_api/worker/tests/e2e/__init__.py +0 -0
  164. control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
  165. control_plane_api/worker/tests/integration/__init__.py +0 -0
  166. control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
  167. control_plane_api/worker/tests/unit/__init__.py +0 -0
  168. control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
  169. control_plane_api/worker/utils/__init__.py +1 -0
  170. control_plane_api/worker/utils/chunk_batcher.py +305 -0
  171. control_plane_api/worker/utils/retry_utils.py +60 -0
  172. control_plane_api/worker/utils/streaming_utils.py +373 -0
  173. control_plane_api/worker/worker.py +753 -0
  174. control_plane_api/worker/workflows/__init__.py +0 -0
  175. control_plane_api/worker/workflows/agent_execution.py +589 -0
  176. control_plane_api/worker/workflows/team_execution.py +429 -0
  177. kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
  178. kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
  179. kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
  180. kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
  181. kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
  182. kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
  183. kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
  184. {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
  185. {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