kubiya-control-plane-api 0.9.15__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.
- control_plane_api/LICENSE +676 -0
- control_plane_api/README.md +350 -0
- control_plane_api/__init__.py +4 -0
- control_plane_api/__version__.py +8 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +121 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
- control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
- control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
- control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
- control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
- control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
- control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
- control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
- control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
- control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
- control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
- control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
- control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
- control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
- control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
- control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
- control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
- control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -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 +384 -0
- control_plane_api/app/activities/plan_generation_activities.py +499 -0
- control_plane_api/app/activities/team_activities.py +424 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +469 -0
- control_plane_api/app/config/config_loader.py +224 -0
- control_plane_api/app/config/model_pricing.py +323 -0
- control_plane_api/app/config/storage_config.py +159 -0
- control_plane_api/app/config.py +115 -0
- control_plane_api/app/controllers/__init__.py +0 -0
- control_plane_api/app/controllers/execution_environment_controller.py +1315 -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/environment.py +65 -0
- control_plane_api/app/lib/event_bus/__init__.py +17 -0
- control_plane_api/app/lib/event_bus/base.py +136 -0
- control_plane_api/app/lib/event_bus/manager.py +335 -0
- control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
- control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
- control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
- control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
- control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
- control_plane_api/app/lib/job_executor.py +330 -0
- control_plane_api/app/lib/kubiya_client.py +293 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/mcp_validation.py +163 -0
- control_plane_api/app/lib/nats/__init__.py +13 -0
- control_plane_api/app/lib/nats/credentials_manager.py +288 -0
- control_plane_api/app/lib/nats/listener.py +374 -0
- control_plane_api/app/lib/planning_prompt_builder.py +153 -0
- control_plane_api/app/lib/planning_tools/__init__.py +41 -0
- control_plane_api/app/lib/planning_tools/agents.py +409 -0
- control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
- control_plane_api/app/lib/planning_tools/base.py +119 -0
- control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
- control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
- control_plane_api/app/lib/planning_tools/environments.py +218 -0
- control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
- control_plane_api/app/lib/planning_tools/models.py +93 -0
- control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
- control_plane_api/app/lib/planning_tools/resources.py +242 -0
- control_plane_api/app/lib/planning_tools/teams.py +334 -0
- control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
- control_plane_api/app/lib/redis_client.py +803 -0
- control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
- control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
- control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
- control_plane_api/app/lib/storage/__init__.py +20 -0
- control_plane_api/app/lib/storage/base_provider.py +274 -0
- control_plane_api/app/lib/storage/provider_factory.py +157 -0
- control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/supabase_utils.py +138 -0
- control_plane_api/app/lib/task_planning/__init__.py +138 -0
- control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
- control_plane_api/app/lib/task_planning/agents.py +389 -0
- control_plane_api/app/lib/task_planning/cache.py +218 -0
- control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
- control_plane_api/app/lib/task_planning/helpers.py +293 -0
- control_plane_api/app/lib/task_planning/hooks.py +474 -0
- control_plane_api/app/lib/task_planning/models.py +503 -0
- control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
- control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
- control_plane_api/app/lib/task_planning/runner.py +656 -0
- control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
- control_plane_api/app/lib/task_planning/workflow.py +424 -0
- control_plane_api/app/lib/templating/__init__.py +88 -0
- control_plane_api/app/lib/templating/compiler.py +278 -0
- control_plane_api/app/lib/templating/engine.py +178 -0
- control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
- control_plane_api/app/lib/templating/parsers/base.py +96 -0
- control_plane_api/app/lib/templating/parsers/env.py +85 -0
- control_plane_api/app/lib/templating/parsers/graph.py +112 -0
- control_plane_api/app/lib/templating/parsers/secret.py +87 -0
- control_plane_api/app/lib/templating/parsers/simple.py +81 -0
- control_plane_api/app/lib/templating/resolver.py +366 -0
- control_plane_api/app/lib/templating/types.py +214 -0
- control_plane_api/app/lib/templating/validator.py +201 -0
- control_plane_api/app/lib/temporal_client.py +232 -0
- control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
- control_plane_api/app/lib/temporal_credentials_service.py +203 -0
- control_plane_api/app/lib/validation/__init__.py +24 -0
- control_plane_api/app/lib/validation/runtime_validation.py +388 -0
- control_plane_api/app/main.py +531 -0
- control_plane_api/app/middleware/__init__.py +10 -0
- control_plane_api/app/middleware/auth.py +645 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/prometheus_middleware.py +173 -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 +40 -0
- control_plane_api/app/models/agent.py +90 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +107 -0
- control_plane_api/app/models/auth_user.py +73 -0
- control_plane_api/app/models/context.py +161 -0
- control_plane_api/app/models/custom_integration.py +99 -0
- control_plane_api/app/models/environment.py +64 -0
- control_plane_api/app/models/execution.py +125 -0
- control_plane_api/app/models/execution_transition.py +50 -0
- control_plane_api/app/models/job.py +159 -0
- control_plane_api/app/models/llm_model.py +78 -0
- control_plane_api/app/models/orchestration.py +66 -0
- control_plane_api/app/models/plan_execution.py +102 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +61 -0
- control_plane_api/app/models/project_management.py +85 -0
- control_plane_api/app/models/session.py +29 -0
- control_plane_api/app/models/skill.py +155 -0
- control_plane_api/app/models/system_tables.py +43 -0
- control_plane_api/app/models/task_planning.py +372 -0
- control_plane_api/app/models/team.py +86 -0
- control_plane_api/app/models/trace.py +257 -0
- control_plane_api/app/models/user_profile.py +54 -0
- control_plane_api/app/models/worker.py +221 -0
- control_plane_api/app/models/workflow.py +161 -0
- control_plane_api/app/models/workspace.py +50 -0
- control_plane_api/app/observability/__init__.py +177 -0
- control_plane_api/app/observability/context_logging.py +475 -0
- control_plane_api/app/observability/decorators.py +337 -0
- control_plane_api/app/observability/local_span_processor.py +702 -0
- control_plane_api/app/observability/metrics.py +303 -0
- control_plane_api/app/observability/middleware.py +246 -0
- control_plane_api/app/observability/optional.py +115 -0
- control_plane_api/app/observability/tracing.py +382 -0
- control_plane_api/app/policies/README.md +149 -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_enforcement/README.md +336 -0
- control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
- control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
- control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
- control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
- control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -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 +382 -0
- control_plane_api/app/routers/agents_v2.py +1598 -0
- control_plane_api/app/routers/analytics.py +1310 -0
- control_plane_api/app/routers/auth.py +59 -0
- control_plane_api/app/routers/client_config.py +57 -0
- control_plane_api/app/routers/context_graph.py +561 -0
- control_plane_api/app/routers/context_manager.py +577 -0
- control_plane_api/app/routers/custom_integrations.py +490 -0
- control_plane_api/app/routers/enforcer.py +132 -0
- control_plane_api/app/routers/environment_context.py +252 -0
- control_plane_api/app/routers/environments.py +761 -0
- control_plane_api/app/routers/execution_environment.py +847 -0
- control_plane_api/app/routers/executions/__init__.py +28 -0
- control_plane_api/app/routers/executions/router.py +286 -0
- control_plane_api/app/routers/executions/services/__init__.py +22 -0
- control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
- control_plane_api/app/routers/executions/services/status_service.py +420 -0
- control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
- control_plane_api/app/routers/executions/services/worker_health.py +514 -0
- control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
- control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
- control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
- control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
- control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
- control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
- control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
- control_plane_api/app/routers/executions.py +4888 -0
- control_plane_api/app/routers/health.py +165 -0
- control_plane_api/app/routers/health_v2.py +394 -0
- control_plane_api/app/routers/integration_templates.py +496 -0
- control_plane_api/app/routers/integrations.py +287 -0
- control_plane_api/app/routers/jobs.py +1809 -0
- control_plane_api/app/routers/metrics.py +517 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +628 -0
- control_plane_api/app/routers/plan_executions.py +1481 -0
- control_plane_api/app/routers/plan_generation_async.py +304 -0
- control_plane_api/app/routers/policies.py +669 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +987 -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 +171 -0
- control_plane_api/app/routers/skills.py +1010 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/storage.py +456 -0
- control_plane_api/app/routers/task_planning.py +611 -0
- control_plane_api/app/routers/task_queues.py +650 -0
- control_plane_api/app/routers/team_context.py +274 -0
- control_plane_api/app/routers/teams.py +1747 -0
- control_plane_api/app/routers/templates.py +248 -0
- control_plane_api/app/routers/traces.py +571 -0
- control_plane_api/app/routers/websocket_client.py +479 -0
- control_plane_api/app/routers/websocket_executions_status.py +437 -0
- control_plane_api/app/routers/websocket_gateway.py +323 -0
- control_plane_api/app/routers/websocket_traces.py +576 -0
- control_plane_api/app/routers/worker_queues.py +2555 -0
- control_plane_api/app/routers/worker_websocket.py +419 -0
- control_plane_api/app/routers/workers.py +1004 -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/__init__.py +1 -0
- control_plane_api/app/schemas/job_schemas.py +302 -0
- control_plane_api/app/schemas/mcp_schemas.py +311 -0
- control_plane_api/app/schemas/template_schemas.py +133 -0
- control_plane_api/app/schemas/trace_schemas.py +168 -0
- control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_planning_strategy.py +233 -0
- control_plane_api/app/services/agno_service.py +838 -0
- control_plane_api/app/services/claude_code_planning_service.py +203 -0
- control_plane_api/app/services/context_graph_client.py +224 -0
- control_plane_api/app/services/custom_integration_service.py +415 -0
- control_plane_api/app/services/integration_resolution_service.py +345 -0
- control_plane_api/app/services/litellm_service.py +394 -0
- control_plane_api/app/services/plan_generator.py +79 -0
- control_plane_api/app/services/planning_strategy.py +66 -0
- control_plane_api/app/services/planning_strategy_factory.py +118 -0
- control_plane_api/app/services/policy_service.py +615 -0
- control_plane_api/app/services/state_transition_service.py +755 -0
- control_plane_api/app/services/storage_service.py +593 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
- control_plane_api/app/services/trace_retention.py +354 -0
- control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
- control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
- control_plane_api/app/services/workflow_operations_service.py +611 -0
- control_plane_api/app/skills/__init__.py +100 -0
- control_plane_api/app/skills/base.py +239 -0
- control_plane_api/app/skills/builtin/__init__.py +37 -0
- control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
- control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
- control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
- control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
- control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
- control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
- control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
- control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
- control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
- control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
- control_plane_api/app/skills/builtin/docker/skill.py +104 -0
- control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
- control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
- control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
- control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
- control_plane_api/app/skills/builtin/python/__init__.py +4 -0
- control_plane_api/app/skills/builtin/python/skill.py +92 -0
- control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
- control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
- control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
- control_plane_api/app/skills/builtin/shell/skill.py +161 -0
- control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/app/skills/builtin/slack/skill.py +302 -0
- control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
- control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/config.py +63 -0
- control_plane_api/app/skills/loaders/__init__.py +14 -0
- control_plane_api/app/skills/loaders/base.py +73 -0
- control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
- control_plane_api/app/skills/registry.py +125 -0
- control_plane_api/app/utils/helpers.py +12 -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 +520 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/plan_generation.py +254 -0
- control_plane_api/app/workflows/team_execution.py +442 -0
- control_plane_api/scripts/seed_models.py +240 -0
- control_plane_api/scripts/validate_existing_tool_names.py +492 -0
- control_plane_api/shared/__init__.py +8 -0
- control_plane_api/shared/version.py +17 -0
- control_plane_api/test_deduplication.py +274 -0
- control_plane_api/test_executor_deduplication_e2e.py +309 -0
- control_plane_api/test_job_execution_e2e.py +283 -0
- control_plane_api/test_real_integration.py +193 -0
- control_plane_api/version.py +38 -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 +1585 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/job_activities.py +199 -0
- control_plane_api/worker/activities/runtime_activities.py +1167 -0
- control_plane_api/worker/activities/skill_activities.py +282 -0
- control_plane_api/worker/activities/team_activities.py +479 -0
- control_plane_api/worker/agent_runtime_server.py +370 -0
- control_plane_api/worker/binary_manager.py +333 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +273 -0
- control_plane_api/worker/control_plane_client.py +1491 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/health_monitor.py +159 -0
- control_plane_api/worker/metrics.py +237 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/error_events.py +105 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +35 -0
- control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
- control_plane_api/worker/runtimes/agno/__init__.py +34 -0
- control_plane_api/worker/runtimes/agno/config.py +248 -0
- control_plane_api/worker/runtimes/agno/hooks.py +385 -0
- control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
- control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
- control_plane_api/worker/runtimes/agno/utils.py +163 -0
- control_plane_api/worker/runtimes/base.py +979 -0
- control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
- control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
- control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
- control_plane_api/worker/runtimes/claude_code/config.py +829 -0
- control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
- control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
- control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
- control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
- control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
- control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
- control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/model_utils.py +107 -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_communication_tools.py +908 -0
- control_plane_api/worker/services/agent_executor.py +485 -0
- control_plane_api/worker/services/agent_executor_v2.py +793 -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/code_ingestion_tools.py +465 -0
- control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
- control_plane_api/worker/services/data_visualization.py +834 -0
- control_plane_api/worker/services/event_publisher.py +531 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +365 -0
- control_plane_api/worker/services/skill_context_enhancement.py +181 -0
- control_plane_api/worker/services/skill_factory.py +471 -0
- control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
- control_plane_api/worker/services/team_executor.py +715 -0
- control_plane_api/worker/services/team_executor_v2.py +1866 -0
- control_plane_api/worker/services/tool_enforcement.py +254 -0
- control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
- control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
- control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
- control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
- control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
- control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/models.py +142 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
- control_plane_api/worker/skills/__init__.py +12 -0
- control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
- control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
- control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
- control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
- control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
- control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
- control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
- control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
- control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
- control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
- control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
- control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
- control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
- control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
- control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
- control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
- control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
- control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
- control_plane_api/worker/skills/loaders/__init__.py +5 -0
- control_plane_api/worker/skills/loaders/base.py +23 -0
- control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
- control_plane_api/worker/skills/registry.py +208 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/conftest.py +12 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
- control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
- control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
- control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
- control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
- control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
- control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
- control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
- control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
- control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
- control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
- control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +330 -0
- control_plane_api/worker/utils/environment.py +65 -0
- control_plane_api/worker/utils/error_publisher.py +260 -0
- control_plane_api/worker/utils/event_batcher.py +256 -0
- control_plane_api/worker/utils/logging_config.py +335 -0
- control_plane_api/worker/utils/logging_helper.py +326 -0
- control_plane_api/worker/utils/parameter_validator.py +120 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +665 -0
- control_plane_api/worker/utils/tool_validation.py +332 -0
- control_plane_api/worker/utils/workspace_manager.py +163 -0
- control_plane_api/worker/websocket_client.py +393 -0
- control_plane_api/worker/worker.py +1297 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +909 -0
- control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
- control_plane_api/worker/workflows/team_execution.py +611 -0
- kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
- kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
- kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
- kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
- scripts/__init__.py +1 -0
- scripts/migrations.py +39 -0
- scripts/seed_worker_queues.py +128 -0
- scripts/setup_agent_runtime.py +142 -0
- worker_internal/__init__.py +1 -0
- worker_internal/planner/__init__.py +1 -0
- worker_internal/planner/activities.py +1499 -0
- worker_internal/planner/agent_tools.py +197 -0
- worker_internal/planner/event_models.py +148 -0
- worker_internal/planner/event_publisher.py +67 -0
- worker_internal/planner/models.py +199 -0
- worker_internal/planner/retry_logic.py +134 -0
- worker_internal/planner/worker.py +300 -0
- worker_internal/planner/workflows.py +970 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
"""
|
|
2
|
+
End-to-End Tests for Single Execution Mode Fixes
|
|
3
|
+
|
|
4
|
+
This test suite validates the three critical fixes for single execution mode:
|
|
5
|
+
1. WebSocket switching is disabled in single execution mode
|
|
6
|
+
2. Execution monitor requires consecutive completion checks
|
|
7
|
+
3. Extended timeout for long-running executions
|
|
8
|
+
|
|
9
|
+
Tests simulate real execution scenarios that previously caused premature exits.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
import asyncio
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
from unittest.mock import Mock, patch, AsyncMock, MagicMock, call
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import sys
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
22
|
+
|
|
23
|
+
from control_plane_api.worker.services.event_publisher import (
|
|
24
|
+
EventPublisher,
|
|
25
|
+
EventPublisherConfig,
|
|
26
|
+
TransportMode,
|
|
27
|
+
)
|
|
28
|
+
from control_plane_api.worker.control_plane_client import ControlPlaneClient
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MockHTTPXClient:
|
|
32
|
+
"""Mock HTTPX client for testing"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, execution_statuses=None):
|
|
35
|
+
"""
|
|
36
|
+
Args:
|
|
37
|
+
execution_statuses: List of status dicts to return on sequential calls
|
|
38
|
+
e.g., [{"status": "running"}, {"status": "completed"}]
|
|
39
|
+
"""
|
|
40
|
+
self.execution_statuses = execution_statuses or []
|
|
41
|
+
self.call_count = 0
|
|
42
|
+
self.requests = []
|
|
43
|
+
|
|
44
|
+
async def get(self, url, headers=None, params=None):
|
|
45
|
+
"""Mock GET request"""
|
|
46
|
+
self.requests.append({"method": "GET", "url": url, "headers": headers, "params": params})
|
|
47
|
+
|
|
48
|
+
# Return execution status based on call count
|
|
49
|
+
if "/executions" in url and self.call_count < len(self.execution_statuses):
|
|
50
|
+
status_data = self.execution_statuses[self.call_count]
|
|
51
|
+
self.call_count += 1
|
|
52
|
+
|
|
53
|
+
response = Mock()
|
|
54
|
+
response.status_code = 200
|
|
55
|
+
response.json.return_value = [status_data]
|
|
56
|
+
return response
|
|
57
|
+
|
|
58
|
+
# Default response
|
|
59
|
+
response = Mock()
|
|
60
|
+
response.status_code = 200
|
|
61
|
+
response.json.return_value = []
|
|
62
|
+
return response
|
|
63
|
+
|
|
64
|
+
async def post(self, url, json=None, headers=None):
|
|
65
|
+
"""Mock POST request"""
|
|
66
|
+
self.requests.append({"method": "POST", "url": url, "json": json, "headers": headers})
|
|
67
|
+
|
|
68
|
+
response = Mock()
|
|
69
|
+
response.status_code = 202
|
|
70
|
+
response.text = "OK"
|
|
71
|
+
return response
|
|
72
|
+
|
|
73
|
+
async def __aenter__(self):
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
class TestWebSocketDisabledInSingleExecution:
|
|
82
|
+
"""Test that WebSocket switching is disabled in single execution mode"""
|
|
83
|
+
|
|
84
|
+
async def test_websocket_disabled_when_env_var_set(self):
|
|
85
|
+
"""Verify WebSocket is disabled when KUBIYA_SINGLE_EXECUTION_MODE=true"""
|
|
86
|
+
# Set environment variable
|
|
87
|
+
os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Create config from env
|
|
91
|
+
config = EventPublisherConfig.from_env()
|
|
92
|
+
|
|
93
|
+
# Assert WebSocket is disabled
|
|
94
|
+
assert config.websocket_enabled is False, "WebSocket should be disabled in single execution mode"
|
|
95
|
+
|
|
96
|
+
# Create event publisher
|
|
97
|
+
mock_control_plane = Mock()
|
|
98
|
+
mock_control_plane.publish_event_async = AsyncMock(return_value=True)
|
|
99
|
+
|
|
100
|
+
publisher = EventPublisher(
|
|
101
|
+
control_plane=mock_control_plane,
|
|
102
|
+
execution_id="test-exec-123",
|
|
103
|
+
config=config,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Verify no transport switch task was created
|
|
107
|
+
assert publisher._transport_switch_task is None, "Transport switch task should not be created"
|
|
108
|
+
|
|
109
|
+
# Verify current transport stays HTTP
|
|
110
|
+
assert publisher._current_transport == TransportMode.HTTP
|
|
111
|
+
|
|
112
|
+
finally:
|
|
113
|
+
# Clean up
|
|
114
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
115
|
+
|
|
116
|
+
async def test_websocket_enabled_when_env_var_not_set(self):
|
|
117
|
+
"""Verify WebSocket is enabled by default when not in single execution mode"""
|
|
118
|
+
# Ensure env var is not set
|
|
119
|
+
if "KUBIYA_SINGLE_EXECUTION_MODE" in os.environ:
|
|
120
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
121
|
+
|
|
122
|
+
# Create config from env
|
|
123
|
+
config = EventPublisherConfig.from_env()
|
|
124
|
+
|
|
125
|
+
# Assert WebSocket is enabled by default
|
|
126
|
+
assert config.websocket_enabled is True, "WebSocket should be enabled by default"
|
|
127
|
+
|
|
128
|
+
async def test_websocket_can_be_explicitly_enabled_in_single_mode(self):
|
|
129
|
+
"""Verify WebSocket can be explicitly enabled even in single execution mode"""
|
|
130
|
+
# Set both env vars
|
|
131
|
+
os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
|
|
132
|
+
os.environ["EVENT_WEBSOCKET_ENABLED"] = "true"
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
config = EventPublisherConfig.from_env()
|
|
136
|
+
|
|
137
|
+
# Explicit override should take precedence
|
|
138
|
+
assert config.websocket_enabled is True, "Explicit EVENT_WEBSOCKET_ENABLED should override default"
|
|
139
|
+
|
|
140
|
+
finally:
|
|
141
|
+
# Clean up
|
|
142
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
143
|
+
del os.environ["EVENT_WEBSOCKET_ENABLED"]
|
|
144
|
+
|
|
145
|
+
async def test_no_websocket_switch_during_execution(self):
|
|
146
|
+
"""Verify that execution doesn't switch to WebSocket after threshold in single mode"""
|
|
147
|
+
os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
config = EventPublisherConfig.from_env()
|
|
151
|
+
config.websocket_switch_threshold_seconds = 1 # 1 second for fast test
|
|
152
|
+
|
|
153
|
+
mock_control_plane = Mock()
|
|
154
|
+
mock_control_plane.publish_event_async = AsyncMock(return_value=True)
|
|
155
|
+
|
|
156
|
+
publisher = EventPublisher(
|
|
157
|
+
control_plane=mock_control_plane,
|
|
158
|
+
execution_id="test-exec-123",
|
|
159
|
+
config=config,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Publish some events
|
|
163
|
+
await publisher.publish("message_chunk", {"content": "test1"})
|
|
164
|
+
|
|
165
|
+
# Wait past the threshold
|
|
166
|
+
await asyncio.sleep(1.5)
|
|
167
|
+
|
|
168
|
+
# Publish more events
|
|
169
|
+
await publisher.publish("message_chunk", {"content": "test2"})
|
|
170
|
+
|
|
171
|
+
# Verify transport is still HTTP
|
|
172
|
+
assert publisher._current_transport == TransportMode.HTTP, "Transport should remain HTTP"
|
|
173
|
+
assert publisher._ws_connection is None, "WebSocket connection should not be created"
|
|
174
|
+
|
|
175
|
+
# Clean up
|
|
176
|
+
await publisher.close()
|
|
177
|
+
|
|
178
|
+
finally:
|
|
179
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.asyncio
|
|
183
|
+
class TestRobustExecutionMonitor:
|
|
184
|
+
"""Test that execution monitor requires consecutive checks before shutdown"""
|
|
185
|
+
|
|
186
|
+
async def test_consecutive_completion_checks_required(self):
|
|
187
|
+
"""Verify monitor requires 2 consecutive 'completed' checks before shutdown"""
|
|
188
|
+
|
|
189
|
+
# Simulate execution status progression:
|
|
190
|
+
# Call 1: running
|
|
191
|
+
# Call 2: completed (first check)
|
|
192
|
+
# Call 3: completed (second check - should trigger shutdown)
|
|
193
|
+
execution_statuses = [
|
|
194
|
+
{"id": "exec-123", "status": "running"},
|
|
195
|
+
{"id": "exec-123", "status": "completed"},
|
|
196
|
+
{"id": "exec-123", "status": "completed"},
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
mock_client = MockHTTPXClient(execution_statuses)
|
|
200
|
+
|
|
201
|
+
# Simulate the monitor logic
|
|
202
|
+
consecutive_completion_checks = 0
|
|
203
|
+
required_consecutive_checks = 2
|
|
204
|
+
execution_seen = False
|
|
205
|
+
execution_id = None
|
|
206
|
+
should_shutdown = False
|
|
207
|
+
|
|
208
|
+
# Simulate 3 polling cycles
|
|
209
|
+
for i in range(3):
|
|
210
|
+
async with mock_client as http_client:
|
|
211
|
+
response = await http_client.get(
|
|
212
|
+
"http://test/api/v1/worker-queues/queue-123/executions",
|
|
213
|
+
headers={"Authorization": "Bearer test-key"},
|
|
214
|
+
params={"limit": 5, "status": "all"}
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
executions = response.json()
|
|
218
|
+
|
|
219
|
+
for execution in executions:
|
|
220
|
+
exec_status = execution.get("status", "").lower()
|
|
221
|
+
exec_id = execution.get("id")
|
|
222
|
+
|
|
223
|
+
if not execution_seen:
|
|
224
|
+
if exec_status in ["running", "completed", "failed"]:
|
|
225
|
+
execution_seen = True
|
|
226
|
+
execution_id = exec_id
|
|
227
|
+
|
|
228
|
+
if execution_seen and exec_id == execution_id:
|
|
229
|
+
if exec_status in ["completed", "failed", "cancelled"]:
|
|
230
|
+
consecutive_completion_checks += 1
|
|
231
|
+
|
|
232
|
+
if consecutive_completion_checks >= required_consecutive_checks:
|
|
233
|
+
should_shutdown = True
|
|
234
|
+
break
|
|
235
|
+
else:
|
|
236
|
+
# Execution back to running - reset counter
|
|
237
|
+
consecutive_completion_checks = 0
|
|
238
|
+
|
|
239
|
+
# Assertions
|
|
240
|
+
assert consecutive_completion_checks == 2, "Should have 2 consecutive completion checks"
|
|
241
|
+
assert should_shutdown is True, "Should trigger shutdown after 2 consecutive checks"
|
|
242
|
+
assert mock_client.call_count == 3, "Should have made 3 API calls"
|
|
243
|
+
|
|
244
|
+
async def test_completion_counter_resets_on_running_status(self):
|
|
245
|
+
"""Verify completion counter resets if execution goes back to running"""
|
|
246
|
+
|
|
247
|
+
# Simulate: completed → running → completed → completed
|
|
248
|
+
execution_statuses = [
|
|
249
|
+
{"id": "exec-123", "status": "completed"}, # First check
|
|
250
|
+
{"id": "exec-123", "status": "running"}, # Back to running - should reset
|
|
251
|
+
{"id": "exec-123", "status": "completed"}, # First check again
|
|
252
|
+
{"id": "exec-123", "status": "completed"}, # Second check - triggers shutdown
|
|
253
|
+
]
|
|
254
|
+
|
|
255
|
+
mock_client = MockHTTPXClient(execution_statuses)
|
|
256
|
+
|
|
257
|
+
consecutive_completion_checks = 0
|
|
258
|
+
required_consecutive_checks = 2
|
|
259
|
+
execution_seen = False
|
|
260
|
+
execution_id = None
|
|
261
|
+
should_shutdown = False
|
|
262
|
+
|
|
263
|
+
for i in range(4):
|
|
264
|
+
async with mock_client as http_client:
|
|
265
|
+
response = await http_client.get(
|
|
266
|
+
"http://test/api/v1/worker-queues/queue-123/executions",
|
|
267
|
+
headers={"Authorization": "Bearer test-key"},
|
|
268
|
+
params={"limit": 5, "status": "all"}
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
executions = response.json()
|
|
272
|
+
|
|
273
|
+
for execution in executions:
|
|
274
|
+
exec_status = execution.get("status", "").lower()
|
|
275
|
+
exec_id = execution.get("id")
|
|
276
|
+
|
|
277
|
+
if not execution_seen:
|
|
278
|
+
execution_seen = True
|
|
279
|
+
execution_id = exec_id
|
|
280
|
+
|
|
281
|
+
if execution_seen and exec_id == execution_id:
|
|
282
|
+
if exec_status in ["completed", "failed", "cancelled"]:
|
|
283
|
+
consecutive_completion_checks += 1
|
|
284
|
+
|
|
285
|
+
if consecutive_completion_checks >= required_consecutive_checks:
|
|
286
|
+
should_shutdown = True
|
|
287
|
+
break
|
|
288
|
+
else:
|
|
289
|
+
# Reset counter
|
|
290
|
+
if consecutive_completion_checks > 0:
|
|
291
|
+
consecutive_completion_checks = 0
|
|
292
|
+
|
|
293
|
+
assert should_shutdown is True, "Should eventually trigger shutdown"
|
|
294
|
+
assert mock_client.call_count == 4, "Should have made 4 API calls due to reset"
|
|
295
|
+
|
|
296
|
+
async def test_completion_counter_resets_on_api_failure(self):
|
|
297
|
+
"""Verify completion counter resets on API failures for safety"""
|
|
298
|
+
|
|
299
|
+
# This test simulates the behavior where API failures should reset the counter
|
|
300
|
+
consecutive_completion_checks = 1 # Simulate we had one check
|
|
301
|
+
|
|
302
|
+
# Simulate API failure
|
|
303
|
+
try:
|
|
304
|
+
raise Exception("API connection failed")
|
|
305
|
+
except Exception:
|
|
306
|
+
# Counter should be reset on error
|
|
307
|
+
if consecutive_completion_checks > 0:
|
|
308
|
+
consecutive_completion_checks = 0
|
|
309
|
+
|
|
310
|
+
assert consecutive_completion_checks == 0, "Counter should reset on API failure"
|
|
311
|
+
|
|
312
|
+
async def test_prevents_premature_shutdown_at_240_seconds(self):
|
|
313
|
+
"""
|
|
314
|
+
Test that execution doesn't shutdown at 240 seconds (WebSocket switch time)
|
|
315
|
+
This is the actual bug we're fixing.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
# Simulate the scenario:
|
|
319
|
+
# - Execution running at 237 seconds
|
|
320
|
+
# - First check at 240 seconds returns "completed" (false positive)
|
|
321
|
+
# - Second check at 243 seconds returns "running" (execution still active)
|
|
322
|
+
# - Should NOT shutdown
|
|
323
|
+
|
|
324
|
+
execution_statuses = [
|
|
325
|
+
{"id": "exec-123", "status": "running"}, # Before 240s
|
|
326
|
+
{"id": "exec-123", "status": "completed"}, # At 240s (false positive from race condition)
|
|
327
|
+
{"id": "exec-123", "status": "running"}, # At 243s (still active!)
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
mock_client = MockHTTPXClient(execution_statuses)
|
|
331
|
+
|
|
332
|
+
consecutive_completion_checks = 0
|
|
333
|
+
required_consecutive_checks = 2
|
|
334
|
+
execution_seen = False
|
|
335
|
+
execution_id = None
|
|
336
|
+
should_shutdown = False
|
|
337
|
+
|
|
338
|
+
for i in range(3):
|
|
339
|
+
async with mock_client as http_client:
|
|
340
|
+
response = await http_client.get(
|
|
341
|
+
"http://test/api/v1/worker-queues/queue-123/executions",
|
|
342
|
+
headers={"Authorization": "Bearer test-key"},
|
|
343
|
+
params={"limit": 5, "status": "all"}
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
executions = response.json()
|
|
347
|
+
|
|
348
|
+
for execution in executions:
|
|
349
|
+
exec_status = execution.get("status", "").lower()
|
|
350
|
+
exec_id = execution.get("id")
|
|
351
|
+
|
|
352
|
+
if not execution_seen:
|
|
353
|
+
execution_seen = True
|
|
354
|
+
execution_id = exec_id
|
|
355
|
+
|
|
356
|
+
if execution_seen and exec_id == execution_id:
|
|
357
|
+
if exec_status in ["completed", "failed", "cancelled"]:
|
|
358
|
+
consecutive_completion_checks += 1
|
|
359
|
+
|
|
360
|
+
if consecutive_completion_checks >= required_consecutive_checks:
|
|
361
|
+
should_shutdown = True
|
|
362
|
+
break
|
|
363
|
+
else:
|
|
364
|
+
# Reset on running status
|
|
365
|
+
if consecutive_completion_checks > 0:
|
|
366
|
+
consecutive_completion_checks = 0
|
|
367
|
+
|
|
368
|
+
# Critical assertions
|
|
369
|
+
assert should_shutdown is False, "Should NOT shutdown due to false positive"
|
|
370
|
+
assert consecutive_completion_checks == 0, "Counter should be reset after false positive"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.mark.asyncio
|
|
374
|
+
class TestLongRunningExecution:
|
|
375
|
+
"""Test that long-running executions (>4 minutes) complete successfully"""
|
|
376
|
+
|
|
377
|
+
async def test_execution_continues_beyond_240_seconds(self):
|
|
378
|
+
"""Verify execution continues past 240 seconds without premature exit"""
|
|
379
|
+
|
|
380
|
+
os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
config = EventPublisherConfig.from_env()
|
|
384
|
+
|
|
385
|
+
# Verify WebSocket is disabled
|
|
386
|
+
assert config.websocket_enabled is False
|
|
387
|
+
|
|
388
|
+
mock_control_plane = Mock()
|
|
389
|
+
mock_control_plane.publish_event_async = AsyncMock(return_value=True)
|
|
390
|
+
|
|
391
|
+
publisher = EventPublisher(
|
|
392
|
+
control_plane=mock_control_plane,
|
|
393
|
+
execution_id="test-long-exec",
|
|
394
|
+
config=config,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Publish events over time
|
|
398
|
+
start_time = time.time()
|
|
399
|
+
|
|
400
|
+
# Simulate publishing events for 5+ minutes (compressed to seconds for test)
|
|
401
|
+
for i in range(6): # Simulate 6 minutes
|
|
402
|
+
await publisher.publish("message_chunk", {"content": f"chunk-{i}", "minute": i})
|
|
403
|
+
|
|
404
|
+
# Small delay to simulate time passing
|
|
405
|
+
await asyncio.sleep(0.1)
|
|
406
|
+
|
|
407
|
+
# Verify transport never switched to WebSocket
|
|
408
|
+
assert publisher._current_transport == TransportMode.HTTP
|
|
409
|
+
assert publisher._ws_connection is None
|
|
410
|
+
|
|
411
|
+
# Flush and close to ensure all batched events are sent
|
|
412
|
+
await publisher.flush()
|
|
413
|
+
await publisher.close()
|
|
414
|
+
|
|
415
|
+
# Verify all events were published via HTTP (may be batched, so at least 1 call)
|
|
416
|
+
assert mock_control_plane.publish_event_async.call_count >= 1, "Should publish events via HTTP"
|
|
417
|
+
|
|
418
|
+
finally:
|
|
419
|
+
if "KUBIYA_SINGLE_EXECUTION_MODE" in os.environ:
|
|
420
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
421
|
+
|
|
422
|
+
async def test_extended_timeout_allows_30_minute_execution(self):
|
|
423
|
+
"""Verify the extended timeout of 30 minutes (1800 seconds) is configured"""
|
|
424
|
+
|
|
425
|
+
# The actual monitor uses max_runtime = 1800 seconds
|
|
426
|
+
max_runtime = 1800 # 30 minutes
|
|
427
|
+
|
|
428
|
+
# Verify this is sufficient for long executions
|
|
429
|
+
assert max_runtime == 1800, "Timeout should be 30 minutes (1800 seconds)"
|
|
430
|
+
assert max_runtime > 240, "Timeout should be much longer than WebSocket switch threshold"
|
|
431
|
+
|
|
432
|
+
# Simulate time checks
|
|
433
|
+
simulated_execution_time = 600 # 10 minutes
|
|
434
|
+
assert simulated_execution_time < max_runtime, "10-minute execution should complete within timeout"
|
|
435
|
+
|
|
436
|
+
simulated_execution_time = 1200 # 20 minutes
|
|
437
|
+
assert simulated_execution_time < max_runtime, "20-minute execution should complete within timeout"
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@pytest.mark.asyncio
|
|
441
|
+
class TestEventPublishingInAgnoRuntime:
|
|
442
|
+
"""Test that event publishing works correctly in Agno runtime"""
|
|
443
|
+
|
|
444
|
+
async def test_event_publisher_awaits_async_functions(self):
|
|
445
|
+
"""Verify EventPublisher properly awaits async publish functions"""
|
|
446
|
+
|
|
447
|
+
mock_control_plane = Mock()
|
|
448
|
+
call_tracker = []
|
|
449
|
+
|
|
450
|
+
async def mock_publish_async(execution_id, event_type, data):
|
|
451
|
+
"""Mock async publish that tracks calls"""
|
|
452
|
+
call_tracker.append({"execution_id": execution_id, "event_type": event_type, "data": data})
|
|
453
|
+
await asyncio.sleep(0.01) # Simulate async work
|
|
454
|
+
return True
|
|
455
|
+
|
|
456
|
+
mock_control_plane.publish_event_async = mock_publish_async
|
|
457
|
+
|
|
458
|
+
config = EventPublisherConfig(websocket_enabled=False)
|
|
459
|
+
publisher = EventPublisher(
|
|
460
|
+
control_plane=mock_control_plane,
|
|
461
|
+
execution_id="test-exec-456",
|
|
462
|
+
config=config,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Publish multiple events
|
|
466
|
+
# Note: message_chunk is batched (EventPriority.NORMAL)
|
|
467
|
+
# tool_started is immediate (EventPriority.IMMEDIATE by default)
|
|
468
|
+
result1 = await publisher.publish("message_chunk", {"content": "chunk1"})
|
|
469
|
+
result2 = await publisher.publish("tool_started", {"tool": "test"})
|
|
470
|
+
|
|
471
|
+
# Verify both published successfully
|
|
472
|
+
assert result1 is True
|
|
473
|
+
assert result2 is True
|
|
474
|
+
|
|
475
|
+
# Flush to ensure batched events are sent
|
|
476
|
+
await publisher.flush()
|
|
477
|
+
await publisher.close()
|
|
478
|
+
|
|
479
|
+
# Verify events were awaited (not just coroutines created)
|
|
480
|
+
# Note: message_chunk is batched, tool_started is immediate
|
|
481
|
+
assert len(call_tracker) >= 1, "At least one event should be published"
|
|
482
|
+
|
|
483
|
+
# Check that tool_started was published immediately
|
|
484
|
+
tool_events = [e for e in call_tracker if e["event_type"] == "tool_started"]
|
|
485
|
+
assert len(tool_events) == 1, "tool_started should be published immediately"
|
|
486
|
+
|
|
487
|
+
async def test_no_unawaited_coroutine_warnings(self):
|
|
488
|
+
"""Verify no 'await wasn't used with future' errors occur"""
|
|
489
|
+
|
|
490
|
+
mock_control_plane = Mock()
|
|
491
|
+
|
|
492
|
+
async def mock_publish_that_raises(execution_id, event_type, data):
|
|
493
|
+
"""Simulate an error that might cause unawaited coroutines"""
|
|
494
|
+
if event_type == "error_event":
|
|
495
|
+
raise RuntimeError("await wasn't used with future")
|
|
496
|
+
return True
|
|
497
|
+
|
|
498
|
+
mock_control_plane.publish_event_async = mock_publish_that_raises
|
|
499
|
+
|
|
500
|
+
config = EventPublisherConfig(websocket_enabled=False)
|
|
501
|
+
publisher = EventPublisher(
|
|
502
|
+
control_plane=mock_control_plane,
|
|
503
|
+
execution_id="test-exec-789",
|
|
504
|
+
config=config,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Publish normal event - should work
|
|
508
|
+
result = await publisher.publish("message_chunk", {"content": "test"})
|
|
509
|
+
assert result is True
|
|
510
|
+
|
|
511
|
+
# Publish error event - should handle exception gracefully
|
|
512
|
+
result = await publisher.publish("error_event", {"error": "test"})
|
|
513
|
+
assert result is False # Returns False on error, doesn't raise
|
|
514
|
+
|
|
515
|
+
await publisher.close()
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
@pytest.mark.asyncio
|
|
519
|
+
class TestIntegrationScenarios:
|
|
520
|
+
"""Integration tests simulating real-world scenarios"""
|
|
521
|
+
|
|
522
|
+
async def test_complete_single_execution_flow(self):
|
|
523
|
+
"""
|
|
524
|
+
Test complete flow: worker starts → execution runs → completes → worker exits
|
|
525
|
+
This simulates the actual 'kubiya exec' flow.
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
# Setup: Single execution mode
|
|
529
|
+
os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
# Phase 1: Worker initialization
|
|
533
|
+
config = EventPublisherConfig.from_env()
|
|
534
|
+
assert config.websocket_enabled is False, "WebSocket should be disabled"
|
|
535
|
+
|
|
536
|
+
# Phase 2: Execution starts
|
|
537
|
+
mock_control_plane = Mock()
|
|
538
|
+
mock_control_plane.publish_event_async = AsyncMock(return_value=True)
|
|
539
|
+
|
|
540
|
+
publisher = EventPublisher(
|
|
541
|
+
control_plane=mock_control_plane,
|
|
542
|
+
execution_id="integration-test-exec",
|
|
543
|
+
config=config,
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Phase 3: Simulate execution for 5 minutes with events
|
|
547
|
+
execution_statuses = [
|
|
548
|
+
{"id": "integration-test-exec", "status": "running"},
|
|
549
|
+
{"id": "integration-test-exec", "status": "running"},
|
|
550
|
+
{"id": "integration-test-exec", "status": "running"},
|
|
551
|
+
{"id": "integration-test-exec", "status": "completed"},
|
|
552
|
+
{"id": "integration-test-exec", "status": "completed"}, # Second check triggers shutdown
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
mock_http_client = MockHTTPXClient(execution_statuses)
|
|
556
|
+
|
|
557
|
+
# Simulate monitoring
|
|
558
|
+
consecutive_completion_checks = 0
|
|
559
|
+
should_shutdown = False
|
|
560
|
+
|
|
561
|
+
for i in range(5):
|
|
562
|
+
# Publish event during execution
|
|
563
|
+
await publisher.publish("message_chunk", {"content": f"Working on task {i}..."})
|
|
564
|
+
|
|
565
|
+
# Monitor checks status
|
|
566
|
+
async with mock_http_client as client:
|
|
567
|
+
response = await client.get(
|
|
568
|
+
"http://test/api/v1/worker-queues/queue-123/executions",
|
|
569
|
+
headers={"Authorization": "Bearer test-key"},
|
|
570
|
+
params={"limit": 5, "status": "all"}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
executions = response.json()
|
|
574
|
+
for execution in executions:
|
|
575
|
+
exec_status = execution.get("status")
|
|
576
|
+
|
|
577
|
+
if exec_status == "completed":
|
|
578
|
+
consecutive_completion_checks += 1
|
|
579
|
+
if consecutive_completion_checks >= 2:
|
|
580
|
+
should_shutdown = True
|
|
581
|
+
break
|
|
582
|
+
|
|
583
|
+
# Phase 4: Cleanup
|
|
584
|
+
await publisher.flush()
|
|
585
|
+
await publisher.close()
|
|
586
|
+
|
|
587
|
+
# Assertions
|
|
588
|
+
assert publisher._current_transport == TransportMode.HTTP, "Should stay HTTP throughout"
|
|
589
|
+
assert consecutive_completion_checks == 2, "Should have 2 completion checks"
|
|
590
|
+
assert should_shutdown is True, "Should shutdown after verification"
|
|
591
|
+
assert mock_control_plane.publish_event_async.call_count >= 1, "Should publish events (may be batched)"
|
|
592
|
+
|
|
593
|
+
finally:
|
|
594
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
595
|
+
|
|
596
|
+
async def test_execution_with_kubectl_commands_beyond_4_minutes(self):
|
|
597
|
+
"""
|
|
598
|
+
Simulate the actual user scenario: kubectl commands running beyond 4 minutes
|
|
599
|
+
This is the exact bug scenario from the issue.
|
|
600
|
+
"""
|
|
601
|
+
|
|
602
|
+
os.environ["KUBIYA_SINGLE_EXECUTION_MODE"] = "true"
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
config = EventPublisherConfig.from_env()
|
|
606
|
+
|
|
607
|
+
mock_control_plane = Mock()
|
|
608
|
+
mock_control_plane.publish_event_async = AsyncMock(return_value=True)
|
|
609
|
+
|
|
610
|
+
publisher = EventPublisher(
|
|
611
|
+
control_plane=mock_control_plane,
|
|
612
|
+
execution_id="kubectl-exec",
|
|
613
|
+
config=config,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Simulate the timeline from the user's logs:
|
|
617
|
+
# 07:02:40 - Execution starts
|
|
618
|
+
# 07:04:09 - kubectl get nodes command (129 seconds)
|
|
619
|
+
# 07:06:41 - kubectl config view command (240 seconds - WebSocket switch time!)
|
|
620
|
+
# Should continue without exiting
|
|
621
|
+
|
|
622
|
+
events = [
|
|
623
|
+
(0, "Execution started"),
|
|
624
|
+
(129, "Running kubectl get nodes -o wide"),
|
|
625
|
+
(240, "Running kubectl config view --minify"), # Critical moment!
|
|
626
|
+
(245, "Running minikube status -p kubiya-k8s"),
|
|
627
|
+
(300, "Task completed successfully"),
|
|
628
|
+
]
|
|
629
|
+
|
|
630
|
+
for elapsed_time, description in events:
|
|
631
|
+
await publisher.publish("message_chunk", {
|
|
632
|
+
"content": description,
|
|
633
|
+
"elapsed": elapsed_time
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
# At 240 seconds, verify no WebSocket switch occurred
|
|
637
|
+
if elapsed_time == 240:
|
|
638
|
+
assert publisher._current_transport == TransportMode.HTTP, \
|
|
639
|
+
"Should NOT switch to WebSocket at 240 seconds"
|
|
640
|
+
assert publisher._ws_connection is None, \
|
|
641
|
+
"WebSocket connection should NOT be created"
|
|
642
|
+
|
|
643
|
+
await publisher.flush()
|
|
644
|
+
await publisher.close()
|
|
645
|
+
|
|
646
|
+
# Verify all events were published successfully via HTTP (may be batched)
|
|
647
|
+
assert mock_control_plane.publish_event_async.call_count >= 1, \
|
|
648
|
+
"Should publish events via HTTP"
|
|
649
|
+
|
|
650
|
+
finally:
|
|
651
|
+
del os.environ["KUBIYA_SINGLE_EXECUTION_MODE"]
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
if __name__ == "__main__":
|
|
655
|
+
"""Run tests with pytest"""
|
|
656
|
+
pytest.main([__file__, "-v", "-s"])
|
|
File without changes
|