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.
- aegis/__init__.py +5 -0
- aegis/__main__.py +374 -0
- aegis/core/CLAUDE.md +365 -0
- aegis/core/__init__.py +6 -0
- aegis/core/components.py +115 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/template_generator.py +163 -0
- aegis/templates/CLAUDE.md +306 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +27 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +127 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py +321 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py +638 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +134 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +247 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/tasks.py.j2 +596 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +133 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +190 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/core/theme.py +46 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py +687 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py +138 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +48 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +36 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +526 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +279 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +60 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py +67 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +85 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +61 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +661 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +269 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1105 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +169 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +195 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +414 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +104 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +239 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +76 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +81 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +376 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +633 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +665 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +602 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +224 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +50 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis_stack-0.1.0.dist-info/METADATA +114 -0
- aegis_stack-0.1.0.dist-info/RECORD +103 -0
- aegis_stack-0.1.0.dist-info/WHEEL +4 -0
- aegis_stack-0.1.0.dist-info/entry_points.txt +2 -0
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2
ADDED
|
@@ -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")
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# API tests package
|
|
@@ -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 %}
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Service tests
|