aegis-stack 0.2.0rc2__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.
- aegis/__init__.py +5 -0
- aegis/__main__.py +51 -0
- aegis/cli/__init__.py +6 -0
- aegis/cli/callbacks.py +114 -0
- aegis/cli/interactive.py +611 -0
- aegis/cli/utils.py +70 -0
- aegis/cli/validators.py +34 -0
- aegis/commands/__init__.py +6 -0
- aegis/commands/add.py +353 -0
- aegis/commands/add_service.py +332 -0
- aegis/commands/components.py +35 -0
- aegis/commands/init.py +370 -0
- aegis/commands/remove.py +227 -0
- aegis/commands/services.py +52 -0
- aegis/commands/update.py +252 -0
- aegis/commands/version.py +12 -0
- aegis/config/__init__.py +1 -0
- aegis/config/shared_files.py +136 -0
- aegis/core/CLAUDE.md +377 -0
- aegis/core/__init__.py +6 -0
- aegis/core/component_files.py +228 -0
- aegis/core/component_utils.py +220 -0
- aegis/core/components.py +127 -0
- aegis/core/copier_manager.py +315 -0
- aegis/core/copier_updater.py +475 -0
- aegis/core/dependency_resolver.py +119 -0
- aegis/core/manual_updater.py +554 -0
- aegis/core/post_gen_tasks.py +547 -0
- aegis/core/service_resolver.py +261 -0
- aegis/core/services.py +157 -0
- aegis/core/template_generator.py +266 -0
- aegis/core/version_compatibility.py +259 -0
- aegis/templates/CLAUDE.md +591 -0
- aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
- aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -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/ai.py.j2 +700 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -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/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -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 +418 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -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.j2 +156 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +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 +97 -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 +55 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -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 +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -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 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -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/ai/__init__.py +8 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -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/backup.py.j2 +119 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -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 +0 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -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 +131 -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 +93 -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_auth_endpoints.py.j2 +307 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -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/ai/__init__.py +1 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
- aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
- aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
- aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
- aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
- aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
- aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
- aegis_stack-0.2.0rc2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Test conversation memory and persistence across chat modes.
|
|
3
|
+
|
|
4
|
+
Tests ensure that both streaming and non-streaming modes maintain
|
|
5
|
+
conversation context properly for multi-turn conversations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from io import StringIO
|
|
10
|
+
from unittest.mock import AsyncMock, MagicMock, patch
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from app.cli.ai import _stream_chat_response
|
|
14
|
+
from app.services.ai.models import (
|
|
15
|
+
AIProvider,
|
|
16
|
+
Conversation,
|
|
17
|
+
ConversationMessage,
|
|
18
|
+
MessageRole,
|
|
19
|
+
StreamingMessage,
|
|
20
|
+
)
|
|
21
|
+
from app.services.ai.service import AIService
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def mock_ai_service():
|
|
26
|
+
"""Create a mock AI service for testing."""
|
|
27
|
+
service = MagicMock(spec=AIService)
|
|
28
|
+
service.config = MagicMock()
|
|
29
|
+
service.config.provider = AIProvider.OPENAI
|
|
30
|
+
service.config.enabled = True
|
|
31
|
+
|
|
32
|
+
# Mock conversation creation
|
|
33
|
+
conversation = Conversation(
|
|
34
|
+
id="test-conversation-123",
|
|
35
|
+
provider=AIProvider.OPENAI,
|
|
36
|
+
model="gpt-4",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
service.conversation_manager = MagicMock()
|
|
40
|
+
service.conversation_manager.create_conversation.return_value = conversation
|
|
41
|
+
service.conversation_manager.get_conversation.return_value = conversation
|
|
42
|
+
|
|
43
|
+
return service
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def mock_console():
|
|
48
|
+
"""Create a mock console for testing."""
|
|
49
|
+
from rich.console import Console
|
|
50
|
+
|
|
51
|
+
output = StringIO()
|
|
52
|
+
console = Console(file=output, force_terminal=False, width=80)
|
|
53
|
+
return console
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TestConversationMemory:
|
|
57
|
+
"""Test conversation memory across different chat modes."""
|
|
58
|
+
|
|
59
|
+
@pytest.mark.asyncio
|
|
60
|
+
async def test_streaming_returns_conversation_id(
|
|
61
|
+
self, mock_ai_service, mock_console
|
|
62
|
+
):
|
|
63
|
+
"""Test that streaming mode returns conversation_id for memory continuity."""
|
|
64
|
+
|
|
65
|
+
# Mock streaming chunks
|
|
66
|
+
chunks = [
|
|
67
|
+
StreamingMessage(
|
|
68
|
+
content="Hello",
|
|
69
|
+
is_final=False,
|
|
70
|
+
is_delta=True,
|
|
71
|
+
message_id="msg-1",
|
|
72
|
+
conversation_id="test-conversation-123",
|
|
73
|
+
metadata={"provider": "openai", "model": "gpt-4", "stream_delta": True},
|
|
74
|
+
),
|
|
75
|
+
StreamingMessage(
|
|
76
|
+
content=" world!",
|
|
77
|
+
is_final=True,
|
|
78
|
+
is_delta=True,
|
|
79
|
+
message_id="msg-1",
|
|
80
|
+
conversation_id="test-conversation-123",
|
|
81
|
+
metadata={
|
|
82
|
+
"provider": "openai",
|
|
83
|
+
"model": "gpt-4",
|
|
84
|
+
"response_time_ms": 1500.0,
|
|
85
|
+
"stream_complete": True,
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
async def mock_stream_chat(*args, **kwargs):
|
|
91
|
+
for chunk in chunks:
|
|
92
|
+
yield chunk
|
|
93
|
+
|
|
94
|
+
mock_ai_service.stream_chat = mock_stream_chat
|
|
95
|
+
|
|
96
|
+
# Patch console to use our mock
|
|
97
|
+
with patch("app.cli.ai.console", mock_console):
|
|
98
|
+
conversation_id = await _stream_chat_response(
|
|
99
|
+
ai_service=mock_ai_service,
|
|
100
|
+
message="Hello AI",
|
|
101
|
+
conversation_id=None,
|
|
102
|
+
user_id="test-user",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Verify conversation_id is returned for memory continuity
|
|
106
|
+
assert conversation_id == "test-conversation-123"
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_streaming_with_existing_conversation(
|
|
110
|
+
self, mock_ai_service, mock_console
|
|
111
|
+
):
|
|
112
|
+
"""Test streaming mode continues existing conversation."""
|
|
113
|
+
|
|
114
|
+
existing_conversation_id = "existing-conversation-456"
|
|
115
|
+
|
|
116
|
+
# Mock streaming chunks with existing conversation
|
|
117
|
+
chunks = [
|
|
118
|
+
StreamingMessage(
|
|
119
|
+
content="Response text",
|
|
120
|
+
is_final=True,
|
|
121
|
+
is_delta=True,
|
|
122
|
+
message_id="msg-2",
|
|
123
|
+
conversation_id=existing_conversation_id,
|
|
124
|
+
metadata={"response_time_ms": 1200.0, "stream_complete": True},
|
|
125
|
+
),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
async def mock_stream_chat(*args, **kwargs):
|
|
129
|
+
# Verify that existing conversation_id was passed
|
|
130
|
+
assert kwargs.get("conversation_id") == existing_conversation_id
|
|
131
|
+
for chunk in chunks:
|
|
132
|
+
yield chunk
|
|
133
|
+
|
|
134
|
+
mock_ai_service.stream_chat = mock_stream_chat
|
|
135
|
+
|
|
136
|
+
with patch("app.cli.ai.console", mock_console):
|
|
137
|
+
returned_id = await _stream_chat_response(
|
|
138
|
+
ai_service=mock_ai_service,
|
|
139
|
+
message="Continue conversation",
|
|
140
|
+
conversation_id=existing_conversation_id,
|
|
141
|
+
user_id="test-user",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
assert returned_id == existing_conversation_id
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
async def test_streaming_interrupted_returns_none(
|
|
148
|
+
self, mock_ai_service, mock_console
|
|
149
|
+
):
|
|
150
|
+
"""Test that interrupted streaming returns None."""
|
|
151
|
+
|
|
152
|
+
async def mock_stream_chat_interrupted(*args, **kwargs):
|
|
153
|
+
# Simulate a few chunks then interruption
|
|
154
|
+
yield StreamingMessage(
|
|
155
|
+
content="Start",
|
|
156
|
+
is_final=False,
|
|
157
|
+
is_delta=True,
|
|
158
|
+
message_id="msg-3",
|
|
159
|
+
conversation_id="test-conversation-789",
|
|
160
|
+
metadata={"provider": "openai"},
|
|
161
|
+
)
|
|
162
|
+
# Simulate interruption by not yielding final chunk
|
|
163
|
+
|
|
164
|
+
mock_ai_service.stream_chat = mock_stream_chat_interrupted
|
|
165
|
+
|
|
166
|
+
# Mock signal handling to simulate interruption
|
|
167
|
+
with (
|
|
168
|
+
patch("app.cli.ai.console", mock_console),
|
|
169
|
+
patch("signal.signal"),
|
|
170
|
+
patch("signal.SIGINT"),
|
|
171
|
+
):
|
|
172
|
+
# Create a mock that simulates interruption
|
|
173
|
+
interrupted = False
|
|
174
|
+
|
|
175
|
+
async def interrupted_stream(*args, **kwargs):
|
|
176
|
+
nonlocal interrupted
|
|
177
|
+
async for chunk in mock_stream_chat_interrupted(*args, **kwargs):
|
|
178
|
+
if not interrupted:
|
|
179
|
+
yield chunk
|
|
180
|
+
interrupted = True # Simulate interruption after first chunk
|
|
181
|
+
break
|
|
182
|
+
|
|
183
|
+
mock_ai_service.stream_chat = interrupted_stream
|
|
184
|
+
|
|
185
|
+
conversation_id = await _stream_chat_response(
|
|
186
|
+
ai_service=mock_ai_service,
|
|
187
|
+
message="Test message",
|
|
188
|
+
conversation_id=None,
|
|
189
|
+
user_id="test-user",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Interrupted streaming should return None to avoid corrupted state
|
|
193
|
+
assert conversation_id is None
|
|
194
|
+
|
|
195
|
+
@pytest.mark.asyncio
|
|
196
|
+
async def test_streaming_timeout_returns_none(self, mock_ai_service, mock_console):
|
|
197
|
+
"""Test that streaming timeout returns None."""
|
|
198
|
+
|
|
199
|
+
async def mock_stream_chat_slow(*args, **kwargs):
|
|
200
|
+
# Simulate slow response that times out (2s delay, 1s timeout)
|
|
201
|
+
await asyncio.sleep(2.0) # Longer than timeout
|
|
202
|
+
yield StreamingMessage(
|
|
203
|
+
content="Too late",
|
|
204
|
+
is_final=True,
|
|
205
|
+
is_delta=True,
|
|
206
|
+
message_id="msg-4",
|
|
207
|
+
conversation_id="test-conversation-timeout",
|
|
208
|
+
metadata={},
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
mock_ai_service.stream_chat = mock_stream_chat_slow
|
|
212
|
+
|
|
213
|
+
# Mock the timeout duration to be much shorter for testing
|
|
214
|
+
with (
|
|
215
|
+
patch("app.cli.ai.console", mock_console),
|
|
216
|
+
patch("asyncio.timeout", return_value=asyncio.timeout(1.0)),
|
|
217
|
+
):
|
|
218
|
+
# Should timeout and return None
|
|
219
|
+
conversation_id = await _stream_chat_response(
|
|
220
|
+
ai_service=mock_ai_service,
|
|
221
|
+
message="Slow message",
|
|
222
|
+
conversation_id=None,
|
|
223
|
+
user_id="test-user",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert conversation_id is None
|
|
227
|
+
|
|
228
|
+
def test_non_streaming_preserves_conversation_id(self, mock_ai_service):
|
|
229
|
+
"""Test that non-streaming mode preserves conversation_id in metadata."""
|
|
230
|
+
|
|
231
|
+
# Create mock response with conversation_id in metadata
|
|
232
|
+
mock_response = ConversationMessage(
|
|
233
|
+
id="msg-5",
|
|
234
|
+
role=MessageRole.ASSISTANT,
|
|
235
|
+
content="Non-streaming response",
|
|
236
|
+
metadata={"conversation_id": "non-stream-conversation-123"},
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
mock_ai_service.chat = AsyncMock(return_value=mock_response)
|
|
240
|
+
|
|
241
|
+
# The conversation_id should be available in response.metadata
|
|
242
|
+
conversation_id = mock_response.metadata.get("conversation_id")
|
|
243
|
+
assert conversation_id == "non-stream-conversation-123"
|
|
244
|
+
|
|
245
|
+
@pytest.mark.asyncio
|
|
246
|
+
async def test_conversation_context_building(self, mock_ai_service):
|
|
247
|
+
"""Test that conversation context is built correctly from message history."""
|
|
248
|
+
|
|
249
|
+
# Create conversation with message history
|
|
250
|
+
conversation = Conversation(
|
|
251
|
+
id="context-test-conversation", provider=AIProvider.OPENAI, model="gpt-4"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Add some message history
|
|
255
|
+
conversation.add_message(MessageRole.USER, "First question")
|
|
256
|
+
conversation.add_message(MessageRole.ASSISTANT, "First answer")
|
|
257
|
+
conversation.add_message(MessageRole.USER, "Second question")
|
|
258
|
+
|
|
259
|
+
mock_ai_service.get_conversation.return_value = conversation
|
|
260
|
+
|
|
261
|
+
# Mock the _build_conversation_context method to verify it's called
|
|
262
|
+
mock_ai_service._build_conversation_context = MagicMock(
|
|
263
|
+
return_value=(
|
|
264
|
+
"User: First question\nAssistant: First answer\n\nUser: Second question"
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Test that context building includes message history
|
|
269
|
+
context = mock_ai_service._build_conversation_context(conversation)
|
|
270
|
+
|
|
271
|
+
assert "First question" in context
|
|
272
|
+
assert "First answer" in context
|
|
273
|
+
assert "Second question" in context
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class TestConversationPersistence:
|
|
277
|
+
"""Test conversation persistence across multiple interactions."""
|
|
278
|
+
|
|
279
|
+
@pytest.mark.asyncio
|
|
280
|
+
async def test_multi_turn_streaming_conversation(
|
|
281
|
+
self, mock_ai_service, mock_console
|
|
282
|
+
):
|
|
283
|
+
"""Test multi-turn conversation maintains context in streaming mode."""
|
|
284
|
+
|
|
285
|
+
conversation_id = "multi-turn-test-123"
|
|
286
|
+
|
|
287
|
+
# First turn
|
|
288
|
+
first_chunks = [
|
|
289
|
+
StreamingMessage(
|
|
290
|
+
content="Hello! How can I help?",
|
|
291
|
+
is_final=True,
|
|
292
|
+
is_delta=True,
|
|
293
|
+
message_id="msg-1",
|
|
294
|
+
conversation_id=conversation_id,
|
|
295
|
+
metadata={"stream_complete": True},
|
|
296
|
+
)
|
|
297
|
+
]
|
|
298
|
+
|
|
299
|
+
# Second turn
|
|
300
|
+
second_chunks = [
|
|
301
|
+
StreamingMessage(
|
|
302
|
+
content="I can help with that!",
|
|
303
|
+
is_final=True,
|
|
304
|
+
is_delta=True,
|
|
305
|
+
message_id="msg-2",
|
|
306
|
+
conversation_id=conversation_id,
|
|
307
|
+
metadata={"stream_complete": True},
|
|
308
|
+
)
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
# Mock streaming to return different chunks on each call
|
|
312
|
+
call_count = 0
|
|
313
|
+
|
|
314
|
+
async def mock_multi_turn_stream(*args, **kwargs):
|
|
315
|
+
nonlocal call_count
|
|
316
|
+
call_count += 1
|
|
317
|
+
if call_count == 1:
|
|
318
|
+
for chunk in first_chunks:
|
|
319
|
+
yield chunk
|
|
320
|
+
else:
|
|
321
|
+
for chunk in second_chunks:
|
|
322
|
+
yield chunk
|
|
323
|
+
|
|
324
|
+
mock_ai_service.stream_chat = mock_multi_turn_stream
|
|
325
|
+
|
|
326
|
+
with patch("app.cli.ai.console", mock_console):
|
|
327
|
+
# First interaction
|
|
328
|
+
first_conversation_id = await _stream_chat_response(
|
|
329
|
+
ai_service=mock_ai_service,
|
|
330
|
+
message="Hello",
|
|
331
|
+
conversation_id=None, # New conversation
|
|
332
|
+
user_id="test-user",
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Second interaction should continue same conversation
|
|
336
|
+
second_conversation_id = await _stream_chat_response(
|
|
337
|
+
ai_service=mock_ai_service,
|
|
338
|
+
message="Can you help me?",
|
|
339
|
+
conversation_id=first_conversation_id, # Continue conversation
|
|
340
|
+
user_id="test-user",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Both interactions should return same conversation_id
|
|
344
|
+
assert first_conversation_id == conversation_id
|
|
345
|
+
assert second_conversation_id == conversation_id
|
|
346
|
+
assert first_conversation_id == second_conversation_id
|
|
347
|
+
|
|
348
|
+
@pytest.mark.asyncio
|
|
349
|
+
async def test_mixed_streaming_non_streaming_persistence(self, mock_ai_service):
|
|
350
|
+
"""Test conversation persists when mixing streaming and non-streaming modes."""
|
|
351
|
+
|
|
352
|
+
conversation_id = "mixed-mode-test-456"
|
|
353
|
+
|
|
354
|
+
# Mock streaming response
|
|
355
|
+
streaming_chunks = [
|
|
356
|
+
StreamingMessage(
|
|
357
|
+
content="Streaming response",
|
|
358
|
+
is_final=True,
|
|
359
|
+
is_delta=True,
|
|
360
|
+
message_id="stream-msg",
|
|
361
|
+
conversation_id=conversation_id,
|
|
362
|
+
metadata={"stream_complete": True},
|
|
363
|
+
)
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
async def mock_stream(*args, **kwargs):
|
|
367
|
+
for chunk in streaming_chunks:
|
|
368
|
+
yield chunk
|
|
369
|
+
|
|
370
|
+
# Mock non-streaming response
|
|
371
|
+
non_streaming_response = ConversationMessage(
|
|
372
|
+
id="non-stream-msg",
|
|
373
|
+
role=MessageRole.ASSISTANT,
|
|
374
|
+
content="Non-streaming response",
|
|
375
|
+
metadata={"conversation_id": conversation_id},
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
mock_ai_service.stream_chat = mock_stream
|
|
379
|
+
mock_ai_service.chat = AsyncMock(return_value=non_streaming_response)
|
|
380
|
+
|
|
381
|
+
# Both modes should maintain same conversation_id
|
|
382
|
+
streaming_conv_id = conversation_id # Would come from streaming response
|
|
383
|
+
non_streaming_conv_id = non_streaming_response.metadata.get("conversation_id")
|
|
384
|
+
|
|
385
|
+
assert streaming_conv_id == conversation_id
|
|
386
|
+
assert non_streaming_conv_id == conversation_id
|
|
387
|
+
assert streaming_conv_id == non_streaming_conv_id
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class TestConversationMemoryEdgeCases:
|
|
391
|
+
"""Test edge cases for conversation memory."""
|
|
392
|
+
|
|
393
|
+
@pytest.mark.asyncio
|
|
394
|
+
async def test_duplicate_content_handling(self, mock_ai_service, mock_console):
|
|
395
|
+
"""Test that duplicate content from fake streaming providers is handled."""
|
|
396
|
+
|
|
397
|
+
# Mock duplicate chunks (fake streaming providers send full content repeatedly)
|
|
398
|
+
duplicate_chunks = [
|
|
399
|
+
StreamingMessage(
|
|
400
|
+
content="Full response text",
|
|
401
|
+
is_final=False,
|
|
402
|
+
is_delta=False, # Fake streaming sends full content
|
|
403
|
+
message_id="dup-msg",
|
|
404
|
+
conversation_id="dup-conversation-123",
|
|
405
|
+
metadata={"provider": "public"},
|
|
406
|
+
),
|
|
407
|
+
StreamingMessage(
|
|
408
|
+
content="Full response text", # Duplicate content
|
|
409
|
+
is_final=True,
|
|
410
|
+
is_delta=False,
|
|
411
|
+
message_id="dup-msg",
|
|
412
|
+
conversation_id="dup-conversation-123",
|
|
413
|
+
metadata={"response_time_ms": 1000.0, "stream_complete": True},
|
|
414
|
+
),
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
async def mock_duplicate_stream(*args, **kwargs):
|
|
418
|
+
for chunk in duplicate_chunks:
|
|
419
|
+
yield chunk
|
|
420
|
+
|
|
421
|
+
mock_ai_service.stream_chat = mock_duplicate_stream
|
|
422
|
+
|
|
423
|
+
with patch("app.cli.ai.console", mock_console):
|
|
424
|
+
conversation_id = await _stream_chat_response(
|
|
425
|
+
ai_service=mock_ai_service,
|
|
426
|
+
message="Test duplicates",
|
|
427
|
+
conversation_id=None,
|
|
428
|
+
user_id="test-user",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Should still return conversation_id despite duplicate content
|
|
432
|
+
assert conversation_id == "dup-conversation-123"
|
|
433
|
+
|
|
434
|
+
@pytest.mark.asyncio
|
|
435
|
+
async def test_empty_conversation_id_handling(self, mock_ai_service, mock_console):
|
|
436
|
+
"""Test handling when conversation_id is missing from response."""
|
|
437
|
+
|
|
438
|
+
# Mock chunk without conversation_id
|
|
439
|
+
empty_chunks = [
|
|
440
|
+
StreamingMessage(
|
|
441
|
+
content="Response without conversation_id",
|
|
442
|
+
is_final=True,
|
|
443
|
+
is_delta=True,
|
|
444
|
+
message_id="empty-msg",
|
|
445
|
+
conversation_id=None, # Missing conversation_id
|
|
446
|
+
metadata={"stream_complete": True},
|
|
447
|
+
)
|
|
448
|
+
]
|
|
449
|
+
|
|
450
|
+
async def mock_empty_stream(*args, **kwargs):
|
|
451
|
+
for chunk in empty_chunks:
|
|
452
|
+
yield chunk
|
|
453
|
+
|
|
454
|
+
mock_ai_service.stream_chat = mock_empty_stream
|
|
455
|
+
|
|
456
|
+
with patch("app.cli.ai.console", mock_console):
|
|
457
|
+
conversation_id = await _stream_chat_response(
|
|
458
|
+
ai_service=mock_ai_service,
|
|
459
|
+
message="Test empty conversation_id",
|
|
460
|
+
conversation_id=None,
|
|
461
|
+
user_id="test-user",
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
# Should return None when conversation_id is missing
|
|
465
|
+
assert conversation_id is None
|
|
@@ -0,0 +1,43 @@
|
|
|
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
|
+
import pytest
|
|
12
|
+
from app.services.system.health import check_system_status
|
|
13
|
+
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.mark.asyncio
|
|
17
|
+
async def test_scheduler_basic_setup() -> None:
|
|
18
|
+
"""Test that the scheduler can be set up and jobs can be added."""
|
|
19
|
+
scheduler = AsyncIOScheduler()
|
|
20
|
+
|
|
21
|
+
# Add a simple job
|
|
22
|
+
scheduler.add_job(check_system_status, trigger="interval", minutes=5, id="test_job")
|
|
23
|
+
|
|
24
|
+
# Check job was added
|
|
25
|
+
jobs = scheduler.get_jobs()
|
|
26
|
+
assert len(jobs) == 1
|
|
27
|
+
assert jobs[0].id == "test_job"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.mark.asyncio
|
|
31
|
+
async def test_system_service_can_be_scheduled() -> None:
|
|
32
|
+
"""Test that our system service functions work with APScheduler."""
|
|
33
|
+
scheduler = AsyncIOScheduler()
|
|
34
|
+
|
|
35
|
+
# Test that our system service function can be scheduled without errors
|
|
36
|
+
scheduler.add_job(check_system_status, trigger="interval", seconds=1, id="system")
|
|
37
|
+
|
|
38
|
+
assert len(scheduler.get_jobs()) == 1
|
|
39
|
+
|
|
40
|
+
# Get job function
|
|
41
|
+
system_job = scheduler.get_job("system")
|
|
42
|
+
|
|
43
|
+
assert system_job.func == check_system_status
|
|
@@ -0,0 +1,195 @@
|
|
|
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 Any, AsyncGenerator, Generator
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from fastapi import FastAPI
|
|
14
|
+
from fastapi.testclient import TestClient
|
|
15
|
+
{% if cookiecutter.include_database == "yes" %}
|
|
16
|
+
from sqlalchemy import create_engine, event
|
|
17
|
+
from sqlalchemy.engine.base import Engine
|
|
18
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
|
19
|
+
from sqlmodel import Session, SQLModel
|
|
20
|
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% if cookiecutter.include_auth == "yes" %}
|
|
23
|
+
# Import models to register them with SQLModel metadata
|
|
24
|
+
from app.models.user import User # noqa: F401
|
|
25
|
+
{% endif %}
|
|
26
|
+
|
|
27
|
+
# Add project root to Python path for imports
|
|
28
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
29
|
+
|
|
30
|
+
from app.integrations.main import create_integrated_app
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def app() -> FastAPI:
|
|
35
|
+
"""Create a configured FastAPI app instance for testing."""
|
|
36
|
+
return create_integrated_app()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def client(app: FastAPI) -> Generator[TestClient, None, None]:
|
|
41
|
+
"""Create a test client for the FastAPI app."""
|
|
42
|
+
with TestClient(app) as test_client:
|
|
43
|
+
yield test_client
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
{% if cookiecutter.include_database == "yes" %}
|
|
47
|
+
@pytest.fixture
|
|
48
|
+
def client_with_db(
|
|
49
|
+
app: FastAPI, db_session: Session
|
|
50
|
+
) -> Generator[TestClient, None, None]:
|
|
51
|
+
"""Create a test client with database dependency override."""
|
|
52
|
+
from app.components.backend.api.deps import get_db
|
|
53
|
+
|
|
54
|
+
def get_test_db() -> Generator[Session, None, None]:
|
|
55
|
+
yield db_session
|
|
56
|
+
|
|
57
|
+
app.dependency_overrides[get_db] = get_test_db
|
|
58
|
+
|
|
59
|
+
with TestClient(app) as test_client:
|
|
60
|
+
yield test_client
|
|
61
|
+
|
|
62
|
+
# Clean up dependency override
|
|
63
|
+
app.dependency_overrides.clear()
|
|
64
|
+
{% endif %}
|
|
65
|
+
|
|
66
|
+
{% if cookiecutter.include_database == "yes" %}
|
|
67
|
+
@pytest.fixture(scope="session")
|
|
68
|
+
def engine() -> Engine:
|
|
69
|
+
"""
|
|
70
|
+
Create in-memory SQLite database engine for tests.
|
|
71
|
+
|
|
72
|
+
Uses :memory: database that exists only in RAM for maximum speed
|
|
73
|
+
and perfect test isolation. Each test session gets a fresh database.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
SQLAlchemy Engine connected to in-memory SQLite database
|
|
77
|
+
"""
|
|
78
|
+
engine = create_engine(
|
|
79
|
+
"sqlite:///:memory:",
|
|
80
|
+
echo=False, # Set to True for SQL debugging
|
|
81
|
+
connect_args={"check_same_thread": False} # Allow multi-threaded access
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Critical: Enable foreign key constraints in SQLite
|
|
85
|
+
# SQLite has foreign keys disabled by default for backwards compatibility
|
|
86
|
+
@event.listens_for(engine, "connect")
|
|
87
|
+
def set_sqlite_pragma(dbapi_connection: Any, connection_record: Any) -> None:
|
|
88
|
+
cursor = dbapi_connection.cursor()
|
|
89
|
+
cursor.execute("PRAGMA foreign_keys=ON")
|
|
90
|
+
cursor.close()
|
|
91
|
+
|
|
92
|
+
# Create all tables once per test session
|
|
93
|
+
SQLModel.metadata.create_all(engine)
|
|
94
|
+
|
|
95
|
+
return engine
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.fixture(scope="function")
|
|
99
|
+
def db_session(engine: Engine) -> Generator[Session, None, None]:
|
|
100
|
+
"""
|
|
101
|
+
Provide transactional database session with automatic rollback.
|
|
102
|
+
|
|
103
|
+
Each test gets a fresh transaction that's rolled back after the test,
|
|
104
|
+
ensuring perfect isolation between tests. Uses the same transaction
|
|
105
|
+
pattern as PostgreSQL for consistency.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
engine: Database engine from session-scoped fixture
|
|
109
|
+
|
|
110
|
+
Yields:
|
|
111
|
+
SQLModel Session for database operations
|
|
112
|
+
"""
|
|
113
|
+
connection = engine.connect()
|
|
114
|
+
transaction = connection.begin()
|
|
115
|
+
session = Session(connection)
|
|
116
|
+
|
|
117
|
+
yield session
|
|
118
|
+
|
|
119
|
+
# Clean up: rollback transaction and close connection
|
|
120
|
+
session.close()
|
|
121
|
+
transaction.rollback()
|
|
122
|
+
connection.close()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.fixture(scope="session")
|
|
126
|
+
async def async_engine():
|
|
127
|
+
"""
|
|
128
|
+
Create async in-memory SQLite database engine for async tests.
|
|
129
|
+
|
|
130
|
+
Uses :memory: database that exists only in RAM for maximum speed
|
|
131
|
+
and perfect test isolation. Each test session gets a fresh database.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Async SQLAlchemy Engine connected to in-memory SQLite database
|
|
135
|
+
"""
|
|
136
|
+
engine = create_async_engine(
|
|
137
|
+
"sqlite+aiosqlite:///:memory:",
|
|
138
|
+
echo=False, # Set to True for SQL debugging
|
|
139
|
+
connect_args={"check_same_thread": False} # Allow multi-threaded access
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Create all tables once per test session
|
|
143
|
+
async with engine.begin() as conn:
|
|
144
|
+
await conn.run_sync(SQLModel.metadata.create_all)
|
|
145
|
+
|
|
146
|
+
yield engine
|
|
147
|
+
|
|
148
|
+
await engine.dispose()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.fixture(scope="function")
|
|
152
|
+
async def async_db_session(async_engine) -> AsyncGenerator[AsyncSession, None]:
|
|
153
|
+
"""
|
|
154
|
+
Provide async transactional database session with automatic rollback.
|
|
155
|
+
|
|
156
|
+
Each test gets a fresh transaction that's rolled back after the test,
|
|
157
|
+
ensuring perfect isolation between tests. Uses async SQLite with aiosqlite.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
async_engine: Async database engine from session-scoped fixture
|
|
161
|
+
|
|
162
|
+
Yields:
|
|
163
|
+
AsyncSession: SQLModel async session for database operations
|
|
164
|
+
"""
|
|
165
|
+
async with async_engine.connect() as connection:
|
|
166
|
+
transaction = await connection.begin()
|
|
167
|
+
session_factory = async_sessionmaker(bind=connection, class_=AsyncSession)
|
|
168
|
+
|
|
169
|
+
async with session_factory() as session:
|
|
170
|
+
yield session
|
|
171
|
+
|
|
172
|
+
# Clean up: rollback transaction
|
|
173
|
+
await transaction.rollback()
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
async def async_client_with_db(
|
|
178
|
+
app: FastAPI, async_db_session: AsyncSession
|
|
179
|
+
) -> AsyncGenerator[TestClient, None]:
|
|
180
|
+
"""Create a test client with async database dependency override."""
|
|
181
|
+
from app.components.backend.api.deps import get_async_db
|
|
182
|
+
|
|
183
|
+
async def get_test_async_db() -> AsyncGenerator[AsyncSession, None]:
|
|
184
|
+
yield async_db_session
|
|
185
|
+
|
|
186
|
+
app.dependency_overrides[get_async_db] = get_test_async_db
|
|
187
|
+
|
|
188
|
+
with TestClient(app) as test_client:
|
|
189
|
+
yield test_client
|
|
190
|
+
|
|
191
|
+
# Clean up dependency override
|
|
192
|
+
app.dependency_overrides.clear()
|
|
193
|
+
{% endif %}
|
|
194
|
+
|
|
195
|
+
|
aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Service tests
|