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.
Files changed (392) hide show
  1. aegis/__init__.py +5 -0
  2. aegis/__main__.py +51 -0
  3. aegis/cli/__init__.py +6 -0
  4. aegis/cli/callbacks.py +114 -0
  5. aegis/cli/interactive.py +611 -0
  6. aegis/cli/utils.py +70 -0
  7. aegis/cli/validators.py +34 -0
  8. aegis/commands/__init__.py +6 -0
  9. aegis/commands/add.py +353 -0
  10. aegis/commands/add_service.py +332 -0
  11. aegis/commands/components.py +35 -0
  12. aegis/commands/init.py +370 -0
  13. aegis/commands/remove.py +227 -0
  14. aegis/commands/services.py +52 -0
  15. aegis/commands/update.py +252 -0
  16. aegis/commands/version.py +12 -0
  17. aegis/config/__init__.py +1 -0
  18. aegis/config/shared_files.py +136 -0
  19. aegis/core/CLAUDE.md +377 -0
  20. aegis/core/__init__.py +6 -0
  21. aegis/core/component_files.py +228 -0
  22. aegis/core/component_utils.py +220 -0
  23. aegis/core/components.py +127 -0
  24. aegis/core/copier_manager.py +315 -0
  25. aegis/core/copier_updater.py +475 -0
  26. aegis/core/dependency_resolver.py +119 -0
  27. aegis/core/manual_updater.py +554 -0
  28. aegis/core/post_gen_tasks.py +547 -0
  29. aegis/core/service_resolver.py +261 -0
  30. aegis/core/services.py +157 -0
  31. aegis/core/template_generator.py +266 -0
  32. aegis/core/version_compatibility.py +259 -0
  33. aegis/templates/CLAUDE.md +591 -0
  34. aegis/templates/cookiecutter-aegis-project/cookiecutter.json +39 -0
  35. aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py +214 -0
  36. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.dockerignore +71 -0
  37. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.env.example.j2 +130 -0
  38. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/.gitignore +131 -0
  39. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Dockerfile +53 -0
  40. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/Makefile +236 -0
  41. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/README.md.j2 +196 -0
  42. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/alembic.ini.j2 +111 -0
  43. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/env.py.j2 +91 -0
  44. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/script.py.mako +25 -0
  45. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/alembic/versions/001_initial_auth.py.j2 +51 -0
  46. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/__init__.py +5 -0
  47. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/__init__.py +6 -0
  48. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai.py.j2 +700 -0
  49. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/ai_rendering.py +361 -0
  50. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/auth.py.j2 +253 -0
  51. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/health.py.j2 +419 -0
  52. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/load_test.py.j2 +656 -0
  53. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/main.py.j2 +65 -0
  54. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/marko_terminal_renderer.py +489 -0
  55. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/tasks.py.j2 +328 -0
  56. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/cli/{% if cookiecutter.include_scheduler == /"yes/" %}tasks.py{% endif %}" +340 -0
  57. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/__init__.py +0 -0
  58. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/__init__.py +0 -0
  59. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/__init__.py +8 -0
  60. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/ai/router.py +329 -0
  61. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/__init__.py +1 -0
  62. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/auth/router.py +64 -0
  63. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/deps.py +58 -0
  64. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/health.py +163 -0
  65. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/models.py.j2 +280 -0
  66. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/routing.py.j2 +32 -0
  67. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/scheduler.py.j2 +121 -0
  68. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/api/worker.py.j2 +478 -0
  69. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/hooks.py +144 -0
  70. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/main.py +31 -0
  71. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/__init__.py +1 -0
  72. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/middleware/cors.py +20 -0
  73. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/__init__.py +1 -0
  74. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/shutdown/cleanup.py +14 -0
  75. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/__init__.py +1 -0
  76. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/component_health.py.j2 +418 -0
  77. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/backend/startup/database_init.py.j2 +83 -0
  78. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/__init__.py +5 -0
  79. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/__init__.py +27 -0
  80. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/table.py +78 -0
  81. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/controls/text.py +142 -0
  82. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/__init__.py.j2 +47 -0
  83. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
  84. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
  85. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/base_card.py +256 -0
  86. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
  87. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
  88. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/database_card.py +420 -0
  89. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
  90. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
  91. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
  92. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
  93. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/services_card.py +233 -0
  94. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
  95. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/main.py.j2 +653 -0
  96. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/frontend/theme.py +48 -0
  97. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/__init__.py +1 -0
  98. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/scheduler/main.py.j2 +156 -0
  99. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/CLAUDE.md.j2 +213 -0
  100. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/__init__.py +6 -0
  101. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/constants.py.j2 +30 -0
  102. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/pools.py +97 -0
  103. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/__init__.py +1 -0
  104. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/load_test.py +55 -0
  105. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/media.py +49 -0
  106. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/queues/system.py +44 -0
  107. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/registry.py +139 -0
  108. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/__init__.py +120 -0
  109. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/load_tasks.py +507 -0
  110. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/simple_system_tasks.py +33 -0
  111. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/components/worker/tasks/system_tasks.py +281 -0
  112. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2 +178 -0
  113. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/constants.py +58 -0
  114. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/db.py.j2 +176 -0
  115. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/log.py +92 -0
  116. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/security.py +62 -0
  117. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/__init__.py +1 -0
  118. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/webserver.py +40 -0
  119. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/entrypoints/{% if cookiecutter.include_scheduler == /"yes/" %}scheduler.py{% endif %}" +21 -0
  120. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/__init__.py +0 -0
  121. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/integrations/main.py +62 -0
  122. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/__init__.py +1 -0
  123. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/models/user.py +44 -0
  124. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/py.typed +0 -0
  125. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/__init__.py +1 -0
  126. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/__init__.py +8 -0
  127. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/config.py +130 -0
  128. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/conversation.py +213 -0
  129. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/health.py +96 -0
  130. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/models.py +229 -0
  131. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/providers.py +370 -0
  132. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/ai/service.py +388 -0
  133. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/__init__.py +1 -0
  134. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/auth_service.py +41 -0
  135. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/health.py +164 -0
  136. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/auth/user_service.py +83 -0
  137. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/middleware_inspector.py.j2 +223 -0
  138. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/models.py.j2 +70 -0
  139. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/backend/route_inspector.py.j2 +155 -0
  140. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test.py +679 -0
  141. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/load_test_models.py +266 -0
  142. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/__init__.py.j2 +21 -0
  143. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/models.py.j2 +119 -0
  144. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/scheduled_task_manager.py.j2 +273 -0
  145. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/scheduler/task_monitor.py.j2 +189 -0
  146. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/__init__.py +15 -0
  147. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/shared/models.py +26 -0
  148. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/__init__.py +52 -0
  149. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/alerts.py +94 -0
  150. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/backup.py.j2 +119 -0
  151. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/health.py.j2 +1333 -0
  152. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/models.py +243 -0
  153. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/services/system/ui.py +52 -0
  154. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-dark.png +0 -0
  155. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto-square-backup.png +0 -0
  156. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/assets/aegis-manifesto.png +0 -0
  157. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.dockerignore +71 -0
  158. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.env.example.j2 +64 -0
  159. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/.gitignore +131 -0
  160. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Dockerfile +53 -0
  161. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/Makefile +211 -0
  162. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/README.md.j2 +172 -0
  163. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/docker-compose.yml.j2 +78 -0
  164. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/mkdocs.yml.j2 +62 -0
  165. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/pyproject.toml.j2 +120 -0
  166. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/clean-validation/uv.lock +1673 -0
  167. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docker-compose.yml.j2 +200 -0
  168. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/api.md +191 -0
  169. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md +0 -0
  170. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/components/scheduler.md.j2 +621 -0
  171. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/development.md +215 -0
  172. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/health.md +240 -0
  173. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/javascripts/mermaid-config.js +62 -0
  174. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/docs/stylesheets/mermaid.css +95 -0
  175. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/mkdocs.yml.j2 +62 -0
  176. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/pyproject.toml.j2 +131 -0
  177. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh +87 -0
  178. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/entrypoint.sh.j2 +93 -0
  179. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/scripts/gen_docs.py +16 -0
  180. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/__init__.py +1 -0
  181. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_auth_endpoints.py.j2 +307 -0
  182. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_health_endpoints.py.j2 +262 -0
  183. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_scheduler_endpoints.py.j2 +214 -0
  184. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/api/test_worker_endpoints.py.j2 +165 -0
  185. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_ai_rendering.py +427 -0
  186. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/cli/test_conversation_memory.py +465 -0
  187. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/components/test_scheduler.py +43 -0
  188. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/conftest.py.j2 +195 -0
  189. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/__init__.py +1 -0
  190. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/__init__.py +1 -0
  191. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/conftest.py +78 -0
  192. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_health.py +157 -0
  193. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_models.py +164 -0
  194. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/ai/test_service.py +198 -0
  195. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_auth_integration.py.j2 +528 -0
  196. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_component_integration.py.j2 +387 -0
  197. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_conversation_persistence.py +342 -0
  198. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_health_logic.py.j2 +663 -0
  199. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_models.py +619 -0
  200. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_load_test_service.py +603 -0
  201. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_middleware_inspector.py.j2 +248 -0
  202. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_scheduled_task_manager.py.j2 +292 -0
  203. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_system_service.py +98 -0
  204. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/services/test_worker_health_registration.py.j2 +257 -0
  205. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/tests/test_core.py +49 -0
  206. aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/uv.lock +1673 -0
  207. aegis/templates/copier-aegis-project/{{ project_slug }}/.copier-answers.yml.jinja +21 -0
  208. aegis/templates/copier-aegis-project/{{ project_slug }}/.dockerignore +71 -0
  209. aegis/templates/copier-aegis-project/{{ project_slug }}/.env.example.jinja +130 -0
  210. aegis/templates/copier-aegis-project/{{ project_slug }}/.gitignore +131 -0
  211. aegis/templates/copier-aegis-project/{{ project_slug }}/Dockerfile +53 -0
  212. aegis/templates/copier-aegis-project/{{ project_slug }}/Makefile.jinja +236 -0
  213. aegis/templates/copier-aegis-project/{{ project_slug }}/README.md.jinja +196 -0
  214. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/alembic.ini.jinja +111 -0
  215. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/env.py.jinja +91 -0
  216. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/script.py.mako +25 -0
  217. aegis/templates/copier-aegis-project/{{ project_slug }}/alembic/versions/001_initial_auth.py.jinja +51 -0
  218. aegis/templates/copier-aegis-project/{{ project_slug }}/app/__init__.py.jinja +5 -0
  219. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/__init__.py.jinja +6 -0
  220. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai.py.jinja +700 -0
  221. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/ai_rendering.py +360 -0
  222. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/auth.py.jinja +253 -0
  223. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/health.py.jinja +419 -0
  224. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/load_test.py.jinja +656 -0
  225. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/main.py.jinja +65 -0
  226. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/marko_terminal_renderer.py +489 -0
  227. aegis/templates/copier-aegis-project/{{ project_slug }}/app/cli/tasks.py.jinja +328 -0
  228. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/__init__.py +0 -0
  229. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/__init__.py +0 -0
  230. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/__init__.py +8 -0
  231. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/ai/router.py +329 -0
  232. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/__init__.py +1 -0
  233. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/auth/router.py +64 -0
  234. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/deps.py +58 -0
  235. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/health.py.jinja +163 -0
  236. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/models.py.jinja +280 -0
  237. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/routing.py.jinja +32 -0
  238. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/scheduler.py.jinja +121 -0
  239. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/api/worker.py.jinja +478 -0
  240. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/hooks.py +144 -0
  241. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/main.py +31 -0
  242. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/__init__.py +1 -0
  243. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/middleware/cors.py +20 -0
  244. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/__init__.py +1 -0
  245. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/shutdown/cleanup.py +14 -0
  246. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/__init__.py +1 -0
  247. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/component_health.py.jinja +418 -0
  248. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/backend/startup/database_init.py.jinja +83 -0
  249. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/__init__.py +5 -0
  250. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/__init__.py +27 -0
  251. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/table.py +78 -0
  252. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/controls/text.py +142 -0
  253. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/__init__.py.jinja +47 -0
  254. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/ai_card.py +287 -0
  255. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/auth_card.py +198 -0
  256. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/base_card.py +256 -0
  257. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_factory.py +227 -0
  258. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/card_utils.py +333 -0
  259. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/database_card.py +420 -0
  260. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/fastapi_card.py +328 -0
  261. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/flet_card.py +267 -0
  262. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/redis_card.py +322 -0
  263. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/scheduler_card.py +352 -0
  264. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/services_card.py +233 -0
  265. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/dashboard/cards/worker_card.py +684 -0
  266. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/main.py.jinja +653 -0
  267. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/frontend/theme.py +48 -0
  268. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/__init__.py +1 -0
  269. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/scheduler/main.py.jinja +156 -0
  270. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/CLAUDE.md.jinja +213 -0
  271. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/__init__.py +6 -0
  272. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/constants.py.jinja +30 -0
  273. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/pools.py +97 -0
  274. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/__init__.py +1 -0
  275. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/load_test.py +55 -0
  276. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/media.py +49 -0
  277. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/queues/system.py +44 -0
  278. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/registry.py +139 -0
  279. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/__init__.py +120 -0
  280. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/load_tasks.py +507 -0
  281. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/simple_system_tasks.py +33 -0
  282. aegis/templates/copier-aegis-project/{{ project_slug }}/app/components/worker/tasks/system_tasks.py +281 -0
  283. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/config.py.jinja +178 -0
  284. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/constants.py +58 -0
  285. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/db.py.jinja +176 -0
  286. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/log.py +92 -0
  287. aegis/templates/copier-aegis-project/{{ project_slug }}/app/core/security.py +62 -0
  288. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/__init__.py +1 -0
  289. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/scheduler.py.jinja +21 -0
  290. aegis/templates/copier-aegis-project/{{ project_slug }}/app/entrypoints/webserver.py +39 -0
  291. aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/__init__.py +0 -0
  292. aegis/templates/copier-aegis-project/{{ project_slug }}/app/integrations/main.py +61 -0
  293. aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/__init__.py +1 -0
  294. aegis/templates/copier-aegis-project/{{ project_slug }}/app/models/user.py +44 -0
  295. aegis/templates/copier-aegis-project/{{ project_slug }}/app/py.typed +0 -0
  296. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/__init__.py +1 -0
  297. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/__init__.py +8 -0
  298. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/config.py +130 -0
  299. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/conversation.py +213 -0
  300. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/health.py +96 -0
  301. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/models.py +229 -0
  302. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/providers.py.jinja +370 -0
  303. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/ai/service.py +387 -0
  304. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/__init__.py +1 -0
  305. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/auth_service.py +40 -0
  306. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/health.py +162 -0
  307. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/auth/user_service.py +82 -0
  308. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/middleware_inspector.py.jinja +223 -0
  309. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/models.py.jinja +70 -0
  310. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/backend/route_inspector.py.jinja +155 -0
  311. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test.py +678 -0
  312. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/load_test_models.py +265 -0
  313. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/__init__.py.jinja +21 -0
  314. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/models.py.jinja +119 -0
  315. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/scheduled_task_manager.py.jinja +273 -0
  316. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/scheduler/task_monitor.py.jinja +189 -0
  317. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/__init__.py +15 -0
  318. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/shared/models.py +26 -0
  319. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/__init__.py +52 -0
  320. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/alerts.py +94 -0
  321. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/backup.py.jinja +119 -0
  322. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/health.py.jinja +1333 -0
  323. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/models.py +243 -0
  324. aegis/templates/copier-aegis-project/{{ project_slug }}/app/services/system/ui.py +52 -0
  325. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57223!aegis-manifesto.png +0 -0
  326. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57224!aegis-manifesto-dark.png +0 -0
  327. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57225!aegis-manifesto-square-backup.png +0 -0
  328. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57533!aegis-manifesto.png +0 -0
  329. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57534!aegis-manifesto-dark.png +0 -0
  330. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57538!aegis-manifesto-square-backup.png +0 -0
  331. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57897!aegis-manifesto.png +0 -0
  332. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57898!aegis-manifesto-dark.png +0 -0
  333. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!57904!aegis-manifesto-square-backup.png +0 -0
  334. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58315!aegis-manifesto.png +0 -0
  335. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58316!aegis-manifesto-dark.png +0 -0
  336. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58324!aegis-manifesto-square-backup.png +0 -0
  337. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58837!aegis-manifesto.png +0 -0
  338. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58838!aegis-manifesto-dark.png +0 -0
  339. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/.!58849!aegis-manifesto-square-backup.png +0 -0
  340. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-dark.png +0 -0
  341. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto-square-backup.png +0 -0
  342. aegis/templates/copier-aegis-project/{{ project_slug }}/assets/aegis-manifesto.png +0 -0
  343. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/.env.example.jinja +64 -0
  344. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/README.md.jinja +172 -0
  345. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/docker-compose.yml.jinja +78 -0
  346. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/mkdocs.yml.jinja +62 -0
  347. aegis/templates/copier-aegis-project/{{ project_slug }}/clean-validation/pyproject.toml.jinja +120 -0
  348. aegis/templates/copier-aegis-project/{{ project_slug }}/docker-compose.yml.jinja +200 -0
  349. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/api.md.jinja +191 -0
  350. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md +0 -0
  351. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/components/scheduler.md.jinja +621 -0
  352. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/development.md.jinja +215 -0
  353. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/health.md.jinja +240 -0
  354. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/javascripts/mermaid-config.js +62 -0
  355. aegis/templates/copier-aegis-project/{{ project_slug }}/docs/stylesheets/mermaid.css +95 -0
  356. aegis/templates/copier-aegis-project/{{ project_slug }}/mkdocs.yml.jinja +62 -0
  357. aegis/templates/copier-aegis-project/{{ project_slug }}/pyproject.toml.jinja +131 -0
  358. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh +87 -0
  359. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/entrypoint.sh.jinja +93 -0
  360. aegis/templates/copier-aegis-project/{{ project_slug }}/scripts/gen_docs.py +16 -0
  361. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/__init__.py +1 -0
  362. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_auth_endpoints.py.jinja +307 -0
  363. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_health_endpoints.py.jinja +262 -0
  364. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_scheduler_endpoints.py.jinja +214 -0
  365. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/api/test_worker_endpoints.py.jinja +165 -0
  366. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_ai_rendering.py +427 -0
  367. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/cli/test_conversation_memory.py +465 -0
  368. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/components/test_scheduler.py +43 -0
  369. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/conftest.py.jinja +195 -0
  370. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/__init__.py +1 -0
  371. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/__init__.py +1 -0
  372. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/conftest.py +78 -0
  373. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_health.py +157 -0
  374. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_models.py +164 -0
  375. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/ai/test_service.py +198 -0
  376. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_auth_integration.py.jinja +528 -0
  377. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_component_integration.py.jinja +387 -0
  378. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_conversation_persistence.py +342 -0
  379. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_health_logic.py.jinja +663 -0
  380. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_models.py +619 -0
  381. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_load_test_service.py +603 -0
  382. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_middleware_inspector.py.jinja +248 -0
  383. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_scheduled_task_manager.py.jinja +292 -0
  384. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_system_service.py +98 -0
  385. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/services/test_worker_health_registration.py.jinja +257 -0
  386. aegis/templates/copier-aegis-project/{{ project_slug }}/tests/test_core.py +49 -0
  387. aegis/templates/copier-aegis-project/{{ project_slug }}/uv.lock +1673 -0
  388. aegis_stack-0.2.0rc2.dist-info/METADATA +165 -0
  389. aegis_stack-0.2.0rc2.dist-info/RECORD +392 -0
  390. aegis_stack-0.2.0rc2.dist-info/WHEEL +4 -0
  391. aegis_stack-0.2.0rc2.dist-info/entry_points.txt +3 -0
  392. 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 include_database %}
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 include_auth %}
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 include_database %}
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 include_database %}
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
+