aegis-stack 0.1.0__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 aegis-stack might be problematic. Click here for more details.

Files changed (103) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +374 -0
  3. aegis/core/CLAUDE.md +365 -0
  4. aegis/core/__init__.py +6 -0
  5. aegis/core/components.py +115 -0
  6. aegis/core/dependency_resolver.py +119 -0
  7. aegis/core/template_generator.py +163 -0
  8. aegis/templates/CLAUDE.md +306 -0
  9. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
  10. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
  11. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  12. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
  13. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
  14. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  15. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
  16. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  17. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  18. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  19. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
  20. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
  21. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
  22. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  23. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  24. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
  25. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
  26. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
  27. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
  28. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
  29. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
  30. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  31. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  32. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  33. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  34. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  35. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  100. aegis_stack-0.1.0.dist-info/METADATA +114 -0
  101. aegis_stack-0.1.0.dist-info/RECORD +103 -0
  102. aegis_stack-0.1.0.dist-info/WHEEL +4 -0
  103. aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ # More comprehensive venv cleanup to prevent Docker container conflicts
6
+ if [ -d ".venv" ]; then
7
+ echo "🧹 Found existing .venv directory, checking compatibility..."
8
+
9
+ # Check if .venv has issues (broken symlinks, wrong Python version, etc.)
10
+ if [ -L ".venv/bin/python3" ] && [ ! -e ".venv/bin/python3" ]; then
11
+ echo "🧹 Cleaning up broken venv symlinks..."
12
+ rm -rf .venv
13
+ elif [ -f ".venv/bin/python3" ]; then
14
+ # Check if the Python executable is compatible and accessible
15
+ if ! .venv/bin/python3 --version > /dev/null 2>&1; then
16
+ echo "🧹 Cleaning up incompatible venv..."
17
+ rm -rf .venv
18
+ fi
19
+ elif [ ! -w ".venv" ] || [ ! -x ".venv" ]; then
20
+ # Check for permission issues in Docker containers
21
+ echo "🧹 Cleaning up venv with permission issues..."
22
+ rm -rf .venv
23
+ else
24
+ # If directory exists but has no python executable, clean it up
25
+ if [ ! -f ".venv/bin/python3" ] && [ ! -f ".venv/bin/python" ]; then
26
+ echo "🧹 Cleaning up incomplete venv..."
27
+ rm -rf .venv
28
+ fi
29
+ fi
30
+ fi
31
+
32
+ # Configure UV environment based on execution context
33
+ if [ -n "$DOCKER_CONTAINER" ] || [ "$USER" = "root" ]; then
34
+ echo "🐳 Running in Docker container, configuring UV for containerized environment..."
35
+
36
+ # Set Docker-specific UV configuration
37
+ export UV_PROJECT_ENVIRONMENT=/code/.venv
38
+ export UV_LINK_MODE=copy
39
+ export VIRTUAL_ENV=/code/.venv
40
+
41
+ # Ensure .venv path is in PATH for CLI commands
42
+ export PATH="/code/.venv/bin:$PATH"
43
+
44
+ echo "✅ UV configured for Docker: UV_PROJECT_ENVIRONMENT=/code/.venv"
45
+ else
46
+ echo "🖥️ Running in local environment, UV will use project defaults"
47
+
48
+ # Ensure we don't inherit Docker environment variables
49
+ unset UV_PROJECT_ENVIRONMENT
50
+ unset UV_SYSTEM_PYTHON
51
+
52
+ # Let UV auto-detect local .venv
53
+ echo "✅ UV configured for local development"
54
+ fi
55
+
56
+ # Pop run_command from arguments
57
+ run_command="$1"
58
+ shift
59
+
60
+ if [ "$run_command" = "webserver" ]; then
61
+ # Web server (FastAPI + Flet)
62
+ uv run python -m app.entrypoints.webserver
63
+ elif [ "$run_command" = "scheduler" ]; then
64
+ # Scheduler component
65
+ uv run python -m app.entrypoints.scheduler
66
+ elif [ "$run_command" = "worker" ]; then
67
+ # Worker component using STANDARD arq CLI
68
+ queue_type="${1:-system}" # Default to system queue if not specified
69
+ shift
70
+
71
+ # Build the module path for the queue
72
+ worker_module="app.components.worker.queues.${queue_type}.WorkerSettings"
73
+
74
+ # Development mode auto-reload (APP_ENV from .env or WORKER_WATCH override)
75
+ if [ "$APP_ENV" = "dev" ] || [ "$WORKER_WATCH" = "true" ]; then
76
+ echo "🔄 Starting ${queue_type} worker with auto-reload..."
77
+ exec uv run python -m arq "${worker_module}" --watch /code/app "$@"
78
+ else
79
+ echo "🚀 Starting ${queue_type} worker..."
80
+ exec uv run python -m arq "${worker_module}" "$@"
81
+ fi
82
+ elif [ "$run_command" = "lint" ]; then
83
+ uv run ruff check .
84
+ elif [ "$run_command" = "typecheck" ]; then
85
+ uv run mypy .
86
+ elif [ "$run_command" = "test" ]; then
87
+ uv run pytest "$@"
88
+ elif [ "$run_command" = "health" ]; then
89
+ uv run python -m app.cli.health check "$@"
90
+ elif [ "$run_command" = "help" ]; then
91
+ echo "Available commands:"
92
+ echo " webserver - Run FastAPI + Flet web server"
93
+ echo " scheduler - Run scheduler component"
94
+ echo " worker - Run arq worker (standard arq CLI patterns)"
95
+ echo " health - Check system health status"
96
+ echo " lint - Run ruff linting"
97
+ echo " typecheck - Run mypy type checking"
98
+ echo " test - Run pytest test suite"
99
+ echo " help - Show this help message"
100
+ else
101
+ echo "Unknown command: $run_command"
102
+ echo "Available commands: webserver, scheduler, worker, health, lint, typecheck, test, help"
103
+ exit 1
104
+ fi
@@ -0,0 +1,16 @@
1
+ # scripts/gen_docs.py
2
+ """
3
+ A script to dynamically generate documentation files for MkDocs.
4
+ This is run automatically by the mkdocs-gen-files plugin.
5
+ """
6
+
7
+ import mkdocs_gen_files # noqa: F401
8
+
9
+ print("--- Running gen_docs.py ---")
10
+
11
+ # Copy the root README.md to be the documentation's index page.
12
+ # This allows us to maintain a single source of truth for the project's
13
+ # main landing page, which is visible on both GitHub and the docs site.
14
+ with open("README.md") as readme, open("docs/index.md", "w") as index:
15
+ index.write(readme.read())
16
+ print("✓ Copied README.md to docs/index.md")
@@ -0,0 +1,239 @@
1
+ """
2
+ Tests for health API endpoints.
3
+
4
+ These tests focus on the HTTP endpoints that CLI health commands call,
5
+ ensuring API responses match expected CLI input format and handle errors correctly.
6
+ """
7
+
8
+ import pytest
9
+ from httpx import AsyncClient, ASGITransport
10
+
11
+ from app.integrations.main import create_integrated_app
12
+
13
+
14
+ class TestHealthEndpoints:
15
+ """Test health API endpoints with various component states."""
16
+
17
+ @pytest.fixture
18
+ async def async_client(self) -> AsyncClient:
19
+ """Async HTTP client for testing."""
20
+ app = create_integrated_app()
21
+
22
+ # Manually trigger startup for health check registration
23
+ from app.components.backend.hooks import backend_hooks
24
+ await backend_hooks.discover_lifespan_hooks()
25
+ await backend_hooks.execute_startup_hooks()
26
+
27
+ transport = ASGITransport(app=app)
28
+ async with AsyncClient(
29
+ transport=transport, base_url="http://test"
30
+ ) as client:
31
+ yield client
32
+
33
+ # Clean up after test
34
+ await backend_hooks.execute_shutdown_hooks()
35
+
36
+ @pytest.mark.asyncio
37
+ async def test_basic_health_endpoint_accessible(
38
+ self, async_client: AsyncClient
39
+ ) -> None:
40
+ """Test /health/ endpoint is accessible."""
41
+ response = await async_client.get("/health/")
42
+
43
+ # Should return a valid response (200 for healthy, 503 for unhealthy)
44
+ assert response.status_code in [200, 503]
45
+ data = response.json()
46
+
47
+ # Verify response structure matches HealthResponse model
48
+ assert "healthy" in data
49
+ assert "status" in data
50
+ assert "components" in data
51
+ assert "timestamp" in data
52
+
53
+ # Verify components are included
54
+ assert isinstance(data["components"], dict)
55
+
56
+ @pytest.mark.asyncio
57
+ async def test_detailed_health_endpoint_accessible(
58
+ self, async_client: AsyncClient
59
+ ) -> None:
60
+ """Test /health/detailed endpoint is accessible."""
61
+ response = await async_client.get("/health/detailed")
62
+
63
+ # Should return a valid response
64
+ assert response.status_code in [200, 503]
65
+ data = response.json()
66
+
67
+ if response.status_code == 200:
68
+ # Verify response structure matches DetailedHealthResponse model
69
+ assert "healthy" in data
70
+ assert "status" in data
71
+ assert "service" in data
72
+ assert "version" in data
73
+ assert "components" in data
74
+ assert "system_info" in data
75
+ assert "timestamp" in data
76
+ assert "healthy_components" in data
77
+ assert "unhealthy_components" in data
78
+ assert "health_percentage" in data
79
+ else:
80
+ # For 503 responses, check error structure
81
+ assert "detail" in data
82
+
83
+ @pytest.mark.asyncio
84
+ async def test_health_endpoints_json_format(
85
+ self, async_client: AsyncClient
86
+ ) -> None:
87
+ """Test that health endpoints return valid JSON for CLI consumption."""
88
+
89
+ # Test both endpoints
90
+ endpoints = ["/health/", "/health/detailed"]
91
+
92
+ for endpoint in endpoints:
93
+ response = await async_client.get(endpoint)
94
+ assert response.status_code in [200, 503]
95
+
96
+ # Should be valid JSON
97
+ data = response.json()
98
+ assert isinstance(data, dict)
99
+
100
+ # Should have basic health information
101
+ if response.status_code == 200:
102
+ assert isinstance(data.get("healthy"), bool)
103
+ assert isinstance(data.get("components"), dict)
104
+ elif response.status_code == 503:
105
+ # Error response should have detail
106
+ assert "detail" in data
107
+
108
+ @pytest.mark.asyncio
109
+ async def test_health_endpoint_component_structure(
110
+ self, async_client: AsyncClient
111
+ ) -> None:
112
+ """Test that component structure is suitable for CLI tree display."""
113
+
114
+ response = await async_client.get("/health/detailed")
115
+ assert response.status_code in [200, 503]
116
+
117
+ data = response.json()
118
+
119
+ if response.status_code == 200:
120
+ components = data["components"]
121
+
122
+ # Should have aegis root component
123
+ assert "aegis" in components
124
+ aegis_component = components["aegis"]
125
+
126
+ # Aegis component should have required fields for CLI display
127
+ assert "name" in aegis_component
128
+ assert "healthy" in aegis_component
129
+ assert "message" in aegis_component
130
+
131
+ # Should have sub-components for tree structure
132
+ if "sub_components" in aegis_component:
133
+ sub_components = aegis_component["sub_components"]
134
+ assert isinstance(sub_components, dict)
135
+
136
+ # Each sub-component should have required fields
137
+ for comp_name, comp_data in sub_components.items():
138
+ assert "name" in comp_data
139
+ assert "healthy" in comp_data
140
+ assert "message" in comp_data
141
+
142
+ {%- if cookiecutter.include_worker == "yes" %}
143
+
144
+ @pytest.mark.asyncio
145
+ async def test_worker_component_appears_in_health_response(
146
+ self, async_client: AsyncClient
147
+ ) -> None:
148
+ """Test that worker component appears in health responses when included."""
149
+
150
+ response = await async_client.get("/health/detailed")
151
+ assert response.status_code in [200, 503]
152
+
153
+ data = response.json()
154
+
155
+ if response.status_code == 200:
156
+ components = data["components"]
157
+
158
+ # Should have aegis root component
159
+ assert "aegis" in components
160
+ aegis_component = components["aegis"]
161
+
162
+ if "sub_components" in aegis_component:
163
+ sub_components = aegis_component["sub_components"]
164
+
165
+ # Worker component should be present
166
+ assert "worker" in sub_components, (
167
+ f"Worker component missing from health response. "
168
+ f"Available components: {list(sub_components.keys())}"
169
+ )
170
+
171
+ worker_component = sub_components["worker"]
172
+
173
+ # Worker component should have required structure
174
+ assert "name" in worker_component
175
+ assert worker_component["name"] == "worker"
176
+ assert "healthy" in worker_component
177
+ assert "message" in worker_component
178
+ assert isinstance(worker_component["healthy"], bool)
179
+
180
+ # Worker should have queue sub-components
181
+ if "sub_components" in worker_component:
182
+ worker_sub_components = worker_component["sub_components"]
183
+
184
+ # Should have queues component
185
+ if "queues" in worker_sub_components:
186
+ queues_component = worker_sub_components["queues"]
187
+ assert "name" in queues_component
188
+ assert "healthy" in queues_component
189
+ assert "message" in queues_component
190
+
191
+ # Check for individual queue health if available
192
+ if "sub_components" in queues_component:
193
+ queue_sub_components = queues_component["sub_components"]
194
+
195
+ # Should have system and load_test queues
196
+ for queue_name in ["system", "load_test"]:
197
+ if queue_name in queue_sub_components:
198
+ queue_comp = queue_sub_components[queue_name]
199
+ assert "name" in queue_comp
200
+ assert "healthy" in queue_comp
201
+ assert "message" in queue_comp
202
+
203
+ # Verify queue metadata exists
204
+ if "metadata" in queue_comp:
205
+ metadata = queue_comp["metadata"]
206
+ assert "queue_type" in metadata
207
+ assert metadata["queue_type"] == queue_name
208
+
209
+ @pytest.mark.asyncio
210
+ async def test_basic_health_includes_worker_in_components(
211
+ self, async_client: AsyncClient
212
+ ) -> None:
213
+ """Test that basic health endpoint includes worker in components dict."""
214
+
215
+ response = await async_client.get("/health/")
216
+ assert response.status_code in [200, 503]
217
+
218
+ data = response.json()
219
+
220
+ if response.status_code == 200:
221
+ components = data["components"]
222
+
223
+ # Should have aegis root component with worker
224
+ assert "aegis" in components
225
+ aegis_component = components["aegis"]
226
+
227
+ if "sub_components" in aegis_component:
228
+ sub_components = aegis_component["sub_components"]
229
+
230
+ # Worker should be present in basic health too
231
+ assert "worker" in sub_components, (
232
+ "Worker component should appear in basic health response"
233
+ )
234
+
235
+ worker_component = sub_components["worker"]
236
+ assert worker_component["name"] == "worker"
237
+ assert isinstance(worker_component["healthy"], bool)
238
+
239
+ {%- endif %}
@@ -0,0 +1,76 @@
1
+ """
2
+ Tests for scheduler functionality.
3
+
4
+ Note: The scheduler focuses entirely on system service monitoring.
5
+ We test the service functions directly rather than complex scheduler components.
6
+
7
+ For integration tests of the actual scheduler, see the CLI tests that generate
8
+ complete projects and validate they work correctly.
9
+ """
10
+
11
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
12
+ import pytest
13
+
14
+ from app.components.scheduler.main import _check_scheduler_health, create_scheduler
15
+ from app.services.system.health import check_system_status
16
+
17
+
18
+ @pytest.mark.asyncio
19
+ async def test_scheduler_basic_setup() -> None:
20
+ """Test that the scheduler can be set up and jobs can be added."""
21
+ scheduler = AsyncIOScheduler()
22
+
23
+ # Add a simple job
24
+ scheduler.add_job(check_system_status, trigger="interval", minutes=5, id="test_job")
25
+
26
+ # Check job was added
27
+ jobs = scheduler.get_jobs()
28
+ assert len(jobs) == 1
29
+ assert jobs[0].id == "test_job"
30
+
31
+
32
+ @pytest.mark.asyncio
33
+ async def test_system_service_can_be_scheduled() -> None:
34
+ """Test that our system service functions work with APScheduler."""
35
+ scheduler = AsyncIOScheduler()
36
+
37
+ # Test that our system service function can be scheduled without errors
38
+ scheduler.add_job(check_system_status, trigger="interval", seconds=1, id="system")
39
+
40
+ assert len(scheduler.get_jobs()) == 1
41
+
42
+ # Get job function
43
+ system_job = scheduler.get_job("system")
44
+
45
+ assert system_job.func == check_system_status
46
+
47
+
48
+ @pytest.mark.asyncio
49
+ async def test_scheduler_health_check() -> None:
50
+ """Test the scheduler health check functionality."""
51
+ # Test health check when scheduler is not initialized
52
+ health_status = await _check_scheduler_health()
53
+ assert not health_status.healthy
54
+ assert "not initialized" in health_status.message
55
+
56
+ # Test with created but not started scheduler
57
+ scheduler = create_scheduler()
58
+ import app.components.scheduler.main as scheduler_module
59
+
60
+ scheduler_module._scheduler = scheduler
61
+
62
+ health_status = await _check_scheduler_health()
63
+ assert not health_status.healthy
64
+ assert "not running" in health_status.message
65
+
66
+ # Test with running scheduler
67
+ scheduler.start()
68
+ try:
69
+ health_status = await _check_scheduler_health()
70
+ assert health_status.healthy
71
+ assert "running with" in health_status.message
72
+ assert health_status.metadata is not None
73
+ assert "job_count" in health_status.metadata
74
+ finally:
75
+ scheduler.shutdown()
76
+ scheduler_module._scheduler = None
@@ -0,0 +1,81 @@
1
+ """
2
+ Pytest configuration and fixtures for test suite.
3
+
4
+ Provides common fixtures and configuration for all tests.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Generator
11
+
12
+ import pytest
13
+
14
+ # Add project root to Python path for imports
15
+ sys.path.insert(0, str(Path(__file__).parent.parent))
16
+
17
+ {% if cookiecutter.include_database == "yes" %}
18
+ from sqlalchemy import create_engine, event
19
+ from sqlalchemy.engine.base import Engine
20
+ from sqlalchemy.orm import sessionmaker
21
+ from sqlmodel import Session, SQLModel
22
+
23
+
24
+ @pytest.fixture(scope="session")
25
+ def engine() -> Engine:
26
+ """
27
+ Create in-memory SQLite database engine for tests.
28
+
29
+ Uses :memory: database that exists only in RAM for maximum speed
30
+ and perfect test isolation. Each test session gets a fresh database.
31
+
32
+ Returns:
33
+ SQLAlchemy Engine connected to in-memory SQLite database
34
+ """
35
+ engine = create_engine(
36
+ "sqlite:///:memory:",
37
+ echo=False, # Set to True for SQL debugging
38
+ connect_args={"check_same_thread": False} # Allow multi-threaded access
39
+ )
40
+
41
+ # Critical: Enable foreign key constraints in SQLite
42
+ # SQLite has foreign keys disabled by default for backwards compatibility
43
+ @event.listens_for(engine, "connect")
44
+ def set_sqlite_pragma(dbapi_connection, connection_record):
45
+ cursor = dbapi_connection.cursor()
46
+ cursor.execute("PRAGMA foreign_keys=ON")
47
+ cursor.close()
48
+
49
+ # Create all tables once per test session
50
+ SQLModel.metadata.create_all(engine)
51
+
52
+ return engine
53
+
54
+
55
+ @pytest.fixture(scope="function")
56
+ def db_session(engine: Engine) -> Generator[Session, None, None]:
57
+ """
58
+ Provide transactional database session with automatic rollback.
59
+
60
+ Each test gets a fresh transaction that's rolled back after the test,
61
+ ensuring perfect isolation between tests. Uses the same transaction
62
+ pattern as PostgreSQL for consistency.
63
+
64
+ Args:
65
+ engine: Database engine from session-scoped fixture
66
+
67
+ Yields:
68
+ SQLModel Session for database operations
69
+ """
70
+ connection = engine.connect()
71
+ transaction = connection.begin()
72
+ session_local = sessionmaker(bind=connection)
73
+ session = session_local()
74
+
75
+ yield session
76
+
77
+ # Clean up: rollback transaction and close connection
78
+ session.close()
79
+ transaction.rollback()
80
+ connection.close()
81
+ {% endif %}