mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
refactor: finish transactional runtime migration
This commit is contained in:
+22
-1
@@ -6,6 +6,7 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# 必须早于首个牵入 app.runtime.config 的 import(app.db / app.chain.* 都会牵入):引擎本身已惰性,
|
||||
# import app.db 不再连库,但 settings 在 import 期就把 CONFIG_DIR 读进字段并建好配置目录,之后
|
||||
@@ -47,7 +48,11 @@ def configure_plugin_system_services():
|
||||
get_async_db,
|
||||
get_db,
|
||||
)
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
from app.db.uow import (
|
||||
SqlAlchemyAsyncUnitOfWork,
|
||||
SqlAlchemyUnitOfWork,
|
||||
configure_transaction_runners,
|
||||
)
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
|
||||
configure_token_codec(create_access_token, decode_access_token)
|
||||
@@ -113,6 +118,22 @@ def configure_plugin_system_services():
|
||||
from app.db.oper.passkey import PassKeyOper
|
||||
from app.startup.subscription import TransactionalSubscribeWriter
|
||||
from app.startup.workflow import TransactionalWorkflowExecutionService
|
||||
from app.startup.transaction import TransactionalWriteRunner
|
||||
|
||||
def compatibility_sync_session() -> Session:
|
||||
"""动态读取可被存量隔离数据库用例替换的 ScopedSession。"""
|
||||
from app.db import decorators
|
||||
|
||||
return decorators.ScopedSession()
|
||||
|
||||
transaction_runner = TransactionalWriteRunner(
|
||||
sync_session=compatibility_sync_session,
|
||||
async_session=async_session_scope,
|
||||
)
|
||||
configure_transaction_runners(
|
||||
sync=transaction_runner.sync,
|
||||
async_=transaction_runner.async_,
|
||||
)
|
||||
|
||||
configure_workflow_legacy_writer(
|
||||
TransactionalWorkflowExecutionService(SessionFactory)
|
||||
|
||||
+26
-22
@@ -13,8 +13,8 @@
|
||||
"runtime_to_db": [],
|
||||
"workflow_to_db": []
|
||||
},
|
||||
"edge_count": 6376,
|
||||
"edge_sha256": "11152e1c89a0d5f07d5460e8a21a8399b9280b8e3027f66621d839817ac3db21",
|
||||
"edge_count": 6379,
|
||||
"edge_sha256": "1c619d72157004590838a85c497330b8ca462d3bc1eb490fd5e7bac97e6190b8",
|
||||
"edges": [
|
||||
"app -> app.runtime",
|
||||
"app -> app.runtime.compat",
|
||||
@@ -1506,21 +1506,18 @@
|
||||
"app.api.context -> app.startup.context",
|
||||
"app.api.dependencies.agent -> app.api",
|
||||
"app.api.dependencies.agent -> app.api.context",
|
||||
"app.api.dependencies.agent -> app.api.data",
|
||||
"app.api.dependencies.agent -> app.api.dependencies",
|
||||
"app.api.dependencies.agent -> app.api.dependencies.data",
|
||||
"app.api.dependencies.agent -> app.application",
|
||||
"app.api.dependencies.agent -> app.application.messaging",
|
||||
"app.api.dependencies.agent -> app.application.messaging.chat",
|
||||
"app.api.dependencies.agent -> app.application.messaging.message",
|
||||
"app.api.dependencies.agent -> app.startup",
|
||||
"app.api.dependencies.agent -> app.startup.context",
|
||||
"app.api.dependencies.auth -> app.adapters",
|
||||
"app.api.dependencies.auth -> app.adapters.web",
|
||||
"app.api.dependencies.auth -> app.adapters.web.security",
|
||||
"app.api.dependencies.auth -> app.adapters.web.security.access",
|
||||
"app.api.dependencies.auth -> app.api",
|
||||
"app.api.dependencies.auth -> app.api.data",
|
||||
"app.api.dependencies.auth -> app.api.dependencies",
|
||||
"app.api.dependencies.auth -> app.api.dependencies.data",
|
||||
"app.api.dependencies.auth -> app.api.context",
|
||||
"app.api.dependencies.auth -> app.application",
|
||||
"app.api.dependencies.auth -> app.application.security",
|
||||
"app.api.dependencies.auth -> app.application.security.auth",
|
||||
@@ -1528,12 +1525,12 @@
|
||||
"app.api.dependencies.auth -> app.application.security.user",
|
||||
"app.api.dependencies.auth -> app.schemas",
|
||||
"app.api.dependencies.auth -> app.schemas.token",
|
||||
"app.api.dependencies.auth -> app.startup",
|
||||
"app.api.dependencies.auth -> app.startup.context",
|
||||
"app.api.dependencies.data -> app.api",
|
||||
"app.api.dependencies.data -> app.api.data",
|
||||
"app.api.dependencies.history -> app.api",
|
||||
"app.api.dependencies.history -> app.api.data",
|
||||
"app.api.dependencies.history -> app.api.dependencies",
|
||||
"app.api.dependencies.history -> app.api.dependencies.data",
|
||||
"app.api.dependencies.history -> app.api.context",
|
||||
"app.api.dependencies.history -> app.application",
|
||||
"app.api.dependencies.history -> app.application.dashboard",
|
||||
"app.api.dependencies.history -> app.application.history",
|
||||
@@ -1546,6 +1543,8 @@
|
||||
"app.api.dependencies.history -> app.schemas",
|
||||
"app.api.dependencies.history -> app.schemas.types",
|
||||
"app.api.dependencies.history -> app.schemas.workflow",
|
||||
"app.api.dependencies.history -> app.startup",
|
||||
"app.api.dependencies.history -> app.startup.context",
|
||||
"app.api.dependencies.plugin -> app.application",
|
||||
"app.api.dependencies.plugin -> app.application.commands",
|
||||
"app.api.dependencies.plugin -> app.application.plugin",
|
||||
@@ -1559,9 +1558,7 @@
|
||||
"app.api.dependencies.plugin -> app.schemas.event",
|
||||
"app.api.dependencies.plugin -> app.schemas.types",
|
||||
"app.api.dependencies.site -> app.api",
|
||||
"app.api.dependencies.site -> app.api.data",
|
||||
"app.api.dependencies.site -> app.api.dependencies",
|
||||
"app.api.dependencies.site -> app.api.dependencies.data",
|
||||
"app.api.dependencies.site -> app.api.context",
|
||||
"app.api.dependencies.site -> app.application",
|
||||
"app.api.dependencies.site -> app.application.site",
|
||||
"app.api.dependencies.site -> app.application.site.mutation",
|
||||
@@ -1574,14 +1571,13 @@
|
||||
"app.api.dependencies.site -> app.runtime.events",
|
||||
"app.api.dependencies.site -> app.schemas",
|
||||
"app.api.dependencies.site -> app.schemas.types",
|
||||
"app.api.dependencies.site -> app.startup",
|
||||
"app.api.dependencies.site -> app.startup.context",
|
||||
"app.api.dependencies.subscription -> app.adapters",
|
||||
"app.api.dependencies.subscription -> app.adapters.external",
|
||||
"app.api.dependencies.subscription -> app.adapters.external.server",
|
||||
"app.api.dependencies.subscription -> app.api",
|
||||
"app.api.dependencies.subscription -> app.api.context",
|
||||
"app.api.dependencies.subscription -> app.api.data",
|
||||
"app.api.dependencies.subscription -> app.api.dependencies",
|
||||
"app.api.dependencies.subscription -> app.api.dependencies.data",
|
||||
"app.api.dependencies.subscription -> app.application",
|
||||
"app.api.dependencies.subscription -> app.application.outbox",
|
||||
"app.api.dependencies.subscription -> app.application.scheduling",
|
||||
@@ -1597,18 +1593,20 @@
|
||||
"app.api.dependencies.subscription -> app.runtime.log",
|
||||
"app.api.dependencies.subscription -> app.schemas",
|
||||
"app.api.dependencies.subscription -> app.schemas.types",
|
||||
"app.api.dependencies.subscription -> app.startup",
|
||||
"app.api.dependencies.subscription -> app.startup.context",
|
||||
"app.api.dependencies.workflow -> app.adapters",
|
||||
"app.api.dependencies.workflow -> app.adapters.external",
|
||||
"app.api.dependencies.workflow -> app.adapters.external.server",
|
||||
"app.api.dependencies.workflow -> app.api",
|
||||
"app.api.dependencies.workflow -> app.api.data",
|
||||
"app.api.dependencies.workflow -> app.api.dependencies",
|
||||
"app.api.dependencies.workflow -> app.api.dependencies.data",
|
||||
"app.api.dependencies.workflow -> app.api.context",
|
||||
"app.api.dependencies.workflow -> app.application",
|
||||
"app.api.dependencies.workflow -> app.application.scheduling",
|
||||
"app.api.dependencies.workflow -> app.application.workflow",
|
||||
"app.api.dependencies.workflow -> app.runtime",
|
||||
"app.api.dependencies.workflow -> app.runtime.config",
|
||||
"app.api.dependencies.workflow -> app.startup",
|
||||
"app.api.dependencies.workflow -> app.startup.context",
|
||||
"app.api.dependencies.workflow -> app.workflow",
|
||||
"app.api.deps -> app.api",
|
||||
"app.api.deps -> app.api.dependencies",
|
||||
@@ -3422,6 +3420,7 @@
|
||||
"app.command -> app.schemas.types",
|
||||
"app.db.base -> app.db",
|
||||
"app.db.base -> app.db.decorators",
|
||||
"app.db.base -> app.db.uow",
|
||||
"app.db.base -> app.runtime",
|
||||
"app.db.base -> app.runtime.config",
|
||||
"app.db.decorators -> app.db",
|
||||
@@ -3445,6 +3444,7 @@
|
||||
"app.db.maintenance -> app.db.models.message",
|
||||
"app.db.maintenance -> app.db.models.siteuserdata",
|
||||
"app.db.maintenance -> app.db.models.transferhistory",
|
||||
"app.db.maintenance -> app.db.uow",
|
||||
"app.db.models -> app.db",
|
||||
"app.db.models -> app.db.models._identity",
|
||||
"app.db.models._identity -> app.runtime",
|
||||
@@ -3464,7 +3464,6 @@
|
||||
"app.db.models.agenttaskrun -> app.db.models.agenttask",
|
||||
"app.db.models.downloadfailure -> app.db",
|
||||
"app.db.models.downloadfailure -> app.db.base",
|
||||
"app.db.models.downloadfailure -> app.db.decorators",
|
||||
"app.db.models.downloadfailure -> app.db.models",
|
||||
"app.db.models.downloadfailure -> app.db.models._constraints",
|
||||
"app.db.models.downloadhistory -> app.db",
|
||||
@@ -6114,6 +6113,7 @@
|
||||
"app.startup.modules_initializer -> app.runtime.extensions.plugin_manager",
|
||||
"app.startup.modules_initializer -> app.runtime.extensions.service_config",
|
||||
"app.startup.modules_initializer -> app.runtime.log",
|
||||
"app.startup.modules_initializer -> app.runtime.observability",
|
||||
"app.startup.modules_initializer -> app.runtime.state",
|
||||
"app.startup.modules_initializer -> app.runtime.thread",
|
||||
"app.startup.modules_initializer -> app.scheduler",
|
||||
@@ -6129,6 +6129,7 @@
|
||||
"app.startup.modules_initializer -> app.startup.managed_resources_initializer",
|
||||
"app.startup.modules_initializer -> app.startup.outbox",
|
||||
"app.startup.modules_initializer -> app.startup.subscription",
|
||||
"app.startup.modules_initializer -> app.startup.transaction",
|
||||
"app.startup.modules_initializer -> app.startup.workflow",
|
||||
"app.startup.monitor_initializer -> app.monitor",
|
||||
"app.startup.outbox -> app.application",
|
||||
@@ -6208,6 +6209,8 @@
|
||||
"app.startup.subscription -> app.schemas.types",
|
||||
"app.startup.subscription -> app.startup",
|
||||
"app.startup.subscription -> app.startup.outbox",
|
||||
"app.startup.transaction -> app.db",
|
||||
"app.startup.transaction -> app.db.uow",
|
||||
"app.startup.transfer_initializer -> app.chain",
|
||||
"app.startup.transfer_initializer -> app.chain.transfer",
|
||||
"app.startup.workflow -> app.application",
|
||||
@@ -6393,7 +6396,7 @@
|
||||
"app.workflow.actions.transfer_file -> app.workflow",
|
||||
"app.workflow.actions.transfer_file -> app.workflow.actions"
|
||||
],
|
||||
"module_count": 790,
|
||||
"module_count": 791,
|
||||
"modules": [
|
||||
"app",
|
||||
"app.adapters",
|
||||
@@ -7161,6 +7164,7 @@
|
||||
"app.startup.routers_initializer",
|
||||
"app.startup.scheduler_initializer",
|
||||
"app.startup.subscription",
|
||||
"app.startup.transaction",
|
||||
"app.startup.transfer_initializer",
|
||||
"app.startup.workflow",
|
||||
"app.startup.workflow_initializer",
|
||||
|
||||
+3
-228
@@ -2,11 +2,11 @@
|
||||
"model_decorators": {
|
||||
"by_kind": {
|
||||
"async_db_query": 49,
|
||||
"async_db_update": 12,
|
||||
"async_db_update": 0,
|
||||
"db_query": 74,
|
||||
"db_update": 33
|
||||
"db_update": 0
|
||||
},
|
||||
"count": 168,
|
||||
"count": 123,
|
||||
"methods": [
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
@@ -28,11 +28,6 @@
|
||||
"file": "app/db/models/agentchat.py",
|
||||
"method": "AgentChat.list_by_page"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/agenttask.py",
|
||||
"method": "AgentTask.add_task"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/agenttask.py",
|
||||
@@ -43,56 +38,16 @@
|
||||
"file": "app/db/models/agenttask.py",
|
||||
"method": "AgentTask.list_for_user"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/agenttask.py",
|
||||
"method": "AgentTask.update_task"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/agenttaskrun.py",
|
||||
"method": "AgentTaskRun.begin_run"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/agenttaskrun.py",
|
||||
"method": "AgentTaskRun.delete_task_and_runs"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/agenttaskrun.py",
|
||||
"method": "AgentTaskRun.finish_run"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/agenttaskrun.py",
|
||||
"method": "AgentTaskRun.get_by_run_id"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/agenttaskrun.py",
|
||||
"method": "AgentTaskRun.interrupt_task"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/agenttaskrun.py",
|
||||
"method": "AgentTaskRun.list_for_task"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/downloadfailure.py",
|
||||
"method": "DownloadFailure.delete_expired"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/downloadhistory.py",
|
||||
"method": "DownloadFiles.delete_by_fullpath"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/downloadhistory.py",
|
||||
"method": "DownloadFiles.delete_orphans"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/downloadhistory.py",
|
||||
@@ -128,11 +83,6 @@
|
||||
"file": "app/db/models/downloadhistory.py",
|
||||
"method": "DownloadHistory.async_list_by_title"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/downloadhistory.py",
|
||||
"method": "DownloadHistory.delete_before"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/downloadhistory.py",
|
||||
@@ -193,21 +143,6 @@
|
||||
"file": "app/db/models/mediaserver.py",
|
||||
"method": "MediaServerItem.async_get_by_itemid"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/mediaserver.py",
|
||||
"method": "MediaServerItem.delete_excluded_servers"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/mediaserver.py",
|
||||
"method": "MediaServerItem.delete_stale"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/mediaserver.py",
|
||||
"method": "MediaServerItem.empty"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/mediaserver.py",
|
||||
@@ -238,16 +173,6 @@
|
||||
"file": "app/db/models/message.py",
|
||||
"method": "Message.async_list_sent_by_page"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/message.py",
|
||||
"method": "Message.create_and_to_dict"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/message.py",
|
||||
"method": "Message.delete_before"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/message.py",
|
||||
@@ -258,11 +183,6 @@
|
||||
"file": "app/db/models/message.py",
|
||||
"method": "Message.list_by_page"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/passkey.py",
|
||||
"method": "PassKey.async_delete_by_id"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
"file": "app/db/models/passkey.py",
|
||||
@@ -278,16 +198,6 @@
|
||||
"file": "app/db/models/passkey.py",
|
||||
"method": "PassKey.async_get_by_user_id"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/passkey.py",
|
||||
"method": "PassKey.async_update_last_used"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/passkey.py",
|
||||
"method": "PassKey.delete_by_id"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/passkey.py",
|
||||
@@ -303,11 +213,6 @@
|
||||
"file": "app/db/models/passkey.py",
|
||||
"method": "PassKey.get_by_user_id"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/passkey.py",
|
||||
"method": "PassKey.update_last_used"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
"file": "app/db/models/plugindata.py",
|
||||
@@ -323,16 +228,6 @@
|
||||
"file": "app/db/models/plugindata.py",
|
||||
"method": "PluginData.async_get_plugin_data_by_plugin_id"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/plugindata.py",
|
||||
"method": "PluginData.del_plugin_data"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/plugindata.py",
|
||||
"method": "PluginData.del_plugin_data_by_key"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/plugindata.py",
|
||||
@@ -368,11 +263,6 @@
|
||||
"file": "app/db/models/site.py",
|
||||
"method": "Site.async_list_order_by_pri"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/site.py",
|
||||
"method": "Site.async_reset"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/site.py",
|
||||
@@ -393,11 +283,6 @@
|
||||
"file": "app/db/models/site.py",
|
||||
"method": "Site.list_order_by_pri"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/site.py",
|
||||
"method": "Site.reset"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
"file": "app/db/models/siteicon.py",
|
||||
@@ -418,11 +303,6 @@
|
||||
"file": "app/db/models/sitestatistic.py",
|
||||
"method": "SiteStatistic.get_by_domain"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/sitestatistic.py",
|
||||
"method": "SiteStatistic.reset"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
"file": "app/db/models/siteuserdata.py",
|
||||
@@ -433,11 +313,6 @@
|
||||
"file": "app/db/models/siteuserdata.py",
|
||||
"method": "SiteUserData.async_get_latest"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/siteuserdata.py",
|
||||
"method": "SiteUserData.delete_before"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/siteuserdata.py",
|
||||
@@ -568,11 +443,6 @@
|
||||
"file": "app/db/models/systemconfig.py",
|
||||
"method": "SystemConfig.async_get_by_key"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/systemconfig.py",
|
||||
"method": "SystemConfig.delete_by_key"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/systemconfig.py",
|
||||
@@ -613,11 +483,6 @@
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
"method": "TransferHistory.count_by_title"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
"method": "TransferHistory.delete_before"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
@@ -683,51 +548,16 @@
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
"method": "TransferHistory.monthly_media_statistics"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
"method": "TransferHistory.replace_by_src"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
"method": "TransferHistory.statistic"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/transferhistory.py",
|
||||
"method": "TransferHistory.update_download_hash"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/transferpending.py",
|
||||
"method": "TransferPending.clear"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/transferpending.py",
|
||||
"method": "TransferPending.discard"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/transferpending.py",
|
||||
"method": "TransferPending.list_all"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/transferpending.py",
|
||||
"method": "TransferPending.register"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.async_delete_by_id"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.async_delete_by_name"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
"file": "app/db/models/user.py",
|
||||
@@ -738,21 +568,6 @@
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.async_get_by_name"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.async_update_otp_by_name"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.delete_by_id"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.delete_by_name"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/user.py",
|
||||
@@ -763,26 +578,11 @@
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.get_by_name"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/user.py",
|
||||
"method": "User.update_otp_by_name"
|
||||
},
|
||||
{
|
||||
"decorator": "db_update",
|
||||
"file": "app/db/models/userconfig.py",
|
||||
"method": "UserConfig.delete_by_key"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/userconfig.py",
|
||||
"method": "UserConfig.get_by_key"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_fail"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_query",
|
||||
"file": "app/db/models/workflow.py",
|
||||
@@ -803,31 +603,6 @@
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_get_timer_triggered_workflows"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_reset"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_start"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_success"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_update_current_action"
|
||||
},
|
||||
{
|
||||
"decorator": "async_db_update",
|
||||
"file": "app/db/models/workflow.py",
|
||||
"method": "Workflow.async_update_state"
|
||||
},
|
||||
{
|
||||
"decorator": "db_query",
|
||||
"file": "app/db/models/workflow.py",
|
||||
|
||||
@@ -7,12 +7,16 @@ import pytest
|
||||
from sqlalchemy import event
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from app.agent import AgentManager
|
||||
from app.agent.orchestrator import AgentManager
|
||||
from app.agent.tools.impl.query_agent_tasks import QueryAgentTasksTool
|
||||
from app.db import Engine, SessionFactory
|
||||
from app.db.engine import get_engine
|
||||
from app.db.oper.agenttask import AgentTaskOper
|
||||
from app.db.models.agenttask import AgentTask
|
||||
from app.db.models.agenttaskrun import AgentTaskRun
|
||||
from app.db.session import SessionFactory
|
||||
|
||||
|
||||
Engine = get_engine()
|
||||
|
||||
|
||||
def _add_task(prefix: str, *, trigger_type: str = "cron") -> AgentTask:
|
||||
@@ -135,17 +139,15 @@ def test_begin_run_rolls_back_task_claim_when_run_insert_fails() -> None:
|
||||
first_task = _add_task("run-rollback-first")
|
||||
second_task = _add_task("run-rollback-second")
|
||||
run_id = uuid4().hex
|
||||
assert AgentTaskRun.begin_run(
|
||||
None,
|
||||
assert AgentTaskOper().begin_run(
|
||||
task_id=first_task.id,
|
||||
run_id=run_id,
|
||||
trigger_source="scheduled",
|
||||
started_at="2026-08-13 20:00:00",
|
||||
) == run_id
|
||||
).run_id == run_id
|
||||
|
||||
with pytest.raises(IntegrityError):
|
||||
AgentTaskRun.begin_run(
|
||||
None,
|
||||
AgentTaskOper().begin_run(
|
||||
task_id=second_task.id,
|
||||
run_id=run_id,
|
||||
trigger_source="manual",
|
||||
|
||||
@@ -120,13 +120,15 @@ def test_runtime_contract_baseline_excludes_diagnostic_line_numbers():
|
||||
|
||||
|
||||
def test_transaction_debt_baseline_is_a_model_and_oper_ratchet() -> None:
|
||||
"""事务 fixture 必须冻结存量 Model 自动提交,并保持 Oper 自提交为零。"""
|
||||
"""事务 fixture 必须保持 Model 写装饰器归零,并冻结剩余查询债务。"""
|
||||
baseline_path = BASELINE_ROOT / "transaction-debt-baseline.json"
|
||||
baseline = json.loads(baseline_path.read_text(encoding="utf-8"))
|
||||
|
||||
assert baseline["schema_version"] == 1
|
||||
assert baseline["model_decorators"]["count"] == 168
|
||||
assert sum(baseline["model_decorators"]["by_kind"].values()) == 168
|
||||
assert baseline["model_decorators"]["count"] == 123
|
||||
assert sum(baseline["model_decorators"]["by_kind"].values()) == 123
|
||||
assert baseline["model_decorators"]["by_kind"]["db_update"] == 0
|
||||
assert baseline["model_decorators"]["by_kind"]["async_db_update"] == 0
|
||||
assert baseline["model_transaction_calls"] == {"count": 0, "calls": []}
|
||||
assert baseline["model_session_factories"] == {"count": 0, "calls": []}
|
||||
assert baseline["oper_transaction_calls"] == {"count": 0, "calls": []}
|
||||
|
||||
@@ -18,11 +18,25 @@ class FakeCleanupRepository:
|
||||
self.failing_table = failing_table
|
||||
self.calls: list[str] = []
|
||||
self._message_results = iter((2, 1, 0))
|
||||
self.commits = 0
|
||||
self.rollbacks = 0
|
||||
|
||||
def session(self):
|
||||
"""返回无需真实数据库的上下文。"""
|
||||
return nullcontext(object())
|
||||
|
||||
def unit_of_work(self, db):
|
||||
"""返回记录提交和回滚次数的测试事务边界。"""
|
||||
return self
|
||||
|
||||
def commit(self) -> None:
|
||||
"""记录一个成功清理批次。"""
|
||||
self.commits += 1
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""记录一个失败清理批次。"""
|
||||
self.rollbacks += 1
|
||||
|
||||
def _delete(self, name: str) -> int:
|
||||
"""记录删除调用并按配置模拟结果或异常。"""
|
||||
self.calls.append(name)
|
||||
@@ -87,6 +101,8 @@ def test_cleanup_service_owns_batching_report_and_progress() -> None:
|
||||
assert report["tables"]["message"]["deleted"] == 3
|
||||
assert report["tables"]["message"]["batches"] == 2
|
||||
assert report["total_deleted"] == 3
|
||||
assert repository.commits == 2
|
||||
assert repository.rollbacks == 0
|
||||
assert repository.calls == [
|
||||
"message",
|
||||
"message",
|
||||
@@ -113,6 +129,7 @@ def test_cleanup_service_finishes_other_tables_before_raising_partial_failure()
|
||||
service.execute(batch_size=2)
|
||||
|
||||
assert repository.calls[-1] == "downloadfailure"
|
||||
assert repository.rollbacks == 1
|
||||
|
||||
|
||||
def test_scheduler_cleanup_is_a_compatibility_delegate() -> None:
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.db.models.passkey import PassKey
|
||||
from app.db.models.systemconfig import SystemConfig
|
||||
from app.db.models.user import User
|
||||
from app.db.models.userconfig import UserConfig
|
||||
from app.db.oper.passkey import PassKeyOper
|
||||
from app.db.oper.user import UserOper
|
||||
|
||||
|
||||
@@ -171,12 +172,12 @@ def test_user_async_mutations_match_sync_behaviour(db):
|
||||
db.add(User(name="mp-test-async-otp", hashed_password="x", is_otp=False))
|
||||
oper = UserOper()
|
||||
|
||||
assert asyncio.run(User.async_update_otp_by_name(
|
||||
assert asyncio.run(oper.async_update_otp_by_name(
|
||||
name="mp-test-async-otp", otp=True, secret="S2")) is True
|
||||
assert asyncio.run(User.async_update_otp_by_name(
|
||||
assert asyncio.run(oper.async_update_otp_by_name(
|
||||
name="mp-test-nobody", otp=True, secret="S2")) is False
|
||||
|
||||
assert asyncio.run(User().async_delete_by_name(name="mp-test-async-otp")) is True
|
||||
assert asyncio.run(oper.async_delete_by_name(name="mp-test-async-otp")) is True
|
||||
assert User.get_by_name(db.session, "mp-test-async-otp") is None
|
||||
|
||||
asyncio.run(oper.async_delete(async_id_user.id))
|
||||
@@ -253,8 +254,9 @@ def test_passkey_async_delete_enforces_the_same_ownership_rule(db):
|
||||
"""
|
||||
victim = db.add(_passkey(9006, "cred-async-victim"))
|
||||
|
||||
assert asyncio.run(PassKey.async_delete_by_id(passkey_id=victim.id, user_id=9999)) is False
|
||||
assert asyncio.run(PassKey.async_delete_by_id(passkey_id=victim.id, user_id=9006)) is True
|
||||
oper = PassKeyOper()
|
||||
assert asyncio.run(oper.async_delete_by_id(passkey_id=victim.id, user_id=9999)) is False
|
||||
assert asyncio.run(oper.async_delete_by_id(passkey_id=victim.id, user_id=9006)) is True
|
||||
assert PassKey.get_by_id(db.session, victim.id) is None
|
||||
|
||||
|
||||
|
||||
@@ -258,6 +258,7 @@ def test_message_oper_listing_entry_points(db):
|
||||
oper = MessageOper(db=db.session)
|
||||
oper.add(title="分页消息", text="正文", source="op-msg-2",
|
||||
reg_time="2026-08-13 10:00:00")
|
||||
db.session.commit()
|
||||
|
||||
assert [m.title for m in oper.list_by_page(page=1, count=1)] == ["分页消息"]
|
||||
assert [m.title for m in asyncio.run(oper.async_list_by_page(page=1, count=1))] == \
|
||||
|
||||
@@ -10,6 +10,7 @@ import asyncio
|
||||
import pytest
|
||||
|
||||
from app.db.models.workflow import Workflow
|
||||
from app.db.session import async_session_scope
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -26,6 +27,18 @@ def _flow(name: str, trigger_type: str = "timer", state: str = "W",
|
||||
actions=[], flows=[], context={}, execution_state={})
|
||||
|
||||
|
||||
async def _stage_async_action(workflow_id: int, action_id: str) -> None:
|
||||
"""用独占异步会话提交一次模型级暂存,模拟 Application UoW 边界。"""
|
||||
async with async_session_scope() as session:
|
||||
await Workflow.async_update_current_action(
|
||||
session,
|
||||
wid=workflow_id,
|
||||
action_id=action_id,
|
||||
context={},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# 列表查询
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -230,8 +243,7 @@ def test_update_current_action_matches_async_twin(db):
|
||||
Workflow.update_current_action(db.session, sync_flow.id, action, {})
|
||||
# 同步 Model 方法只暂存 SQL;由测试持有的事务边界先提交,避免与异步会话争锁。
|
||||
db.session.commit()
|
||||
asyncio.run(Workflow.async_update_current_action(
|
||||
wid=async_flow.id, action_id=action, context={}))
|
||||
asyncio.run(_stage_async_action(async_flow.id, action))
|
||||
|
||||
assert Workflow.get_by_name(db.session, "wf-sync-action").current_action == \
|
||||
Workflow.get_by_name(db.session, "wf-async-action").current_action
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""类型化 HostRuntime 与 FastAPI AppState 注入测试。"""
|
||||
|
||||
import ast
|
||||
from dataclasses import FrozenInstanceError
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
@@ -11,13 +13,18 @@ from app.api.context import (
|
||||
get_agent_chat_repository,
|
||||
get_agent_chat_transaction,
|
||||
)
|
||||
from app.api.data import (
|
||||
ApiDataPorts,
|
||||
configure_api_data_runtime,
|
||||
get_api_data_ports,
|
||||
)
|
||||
from app.startup import lifecycle
|
||||
from app.startup.context import AgentChatRuntime, HostRuntime, SubscriptionRuntime
|
||||
from app.startup.context import (
|
||||
AgentChatRuntime,
|
||||
AuthenticationRuntime,
|
||||
HistoryRuntime,
|
||||
HostRuntime,
|
||||
MessagingRuntime,
|
||||
PersistenceRuntime,
|
||||
SiteRuntime,
|
||||
SubscriptionRuntime,
|
||||
WorkflowRuntime,
|
||||
)
|
||||
from app.application.configuration import (
|
||||
ApiRuntimeConfig,
|
||||
ChainRuntimeConfig,
|
||||
@@ -26,6 +33,9 @@ from app.application.configuration import (
|
||||
)
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parents[1]
|
||||
|
||||
|
||||
class _Repository:
|
||||
"""记录绑定会话的 Agent 会话仓储替身。"""
|
||||
|
||||
@@ -48,6 +58,20 @@ class _UnitOfWork:
|
||||
"""模拟回滚。"""
|
||||
|
||||
|
||||
class _SyncUnitOfWork:
|
||||
"""记录绑定会话的同步事务替身。"""
|
||||
|
||||
def __init__(self, session: object) -> None:
|
||||
"""保存与仓储相同的请求会话。"""
|
||||
self.session = session
|
||||
|
||||
def commit(self) -> None:
|
||||
"""模拟提交。"""
|
||||
|
||||
def rollback(self) -> None:
|
||||
"""模拟回滚。"""
|
||||
|
||||
|
||||
class _Outbox:
|
||||
"""记录绑定会话的异步 outbox 替身。"""
|
||||
|
||||
@@ -73,19 +97,31 @@ def _runtime() -> HostRuntime:
|
||||
if False:
|
||||
yield object()
|
||||
|
||||
compatibility = ApiDataPorts(
|
||||
sync_session=sync_session,
|
||||
async_session=async_session,
|
||||
repositories={},
|
||||
standalone={},
|
||||
unit_of_work={},
|
||||
)
|
||||
return HostRuntime(
|
||||
agent_chat=AgentChatRuntime(
|
||||
async_session=async_session,
|
||||
repository=_Repository,
|
||||
transaction=_UnitOfWork,
|
||||
),
|
||||
persistence=PersistenceRuntime(
|
||||
sync_session=sync_session,
|
||||
async_session=async_session,
|
||||
sync_transaction=_SyncUnitOfWork,
|
||||
async_transaction=_UnitOfWork,
|
||||
),
|
||||
authentication=AuthenticationRuntime(
|
||||
user_repository=_Repository,
|
||||
standalone_user=lambda: _Repository(object()),
|
||||
system_config=lambda: _Repository(object()),
|
||||
passkey=lambda: _Repository(object()),
|
||||
),
|
||||
messaging=MessagingRuntime(repository=_Repository),
|
||||
history=HistoryRuntime(
|
||||
download_repository=_Repository,
|
||||
transfer_repository=_Repository,
|
||||
media_server_repository=_Repository,
|
||||
),
|
||||
site=SiteRuntime(repository=_Repository),
|
||||
subscription=SubscriptionRuntime(
|
||||
async_session=async_session,
|
||||
repository=_Repository,
|
||||
@@ -93,6 +129,10 @@ def _runtime() -> HostRuntime:
|
||||
transaction=_UnitOfWork,
|
||||
outbox=_Outbox,
|
||||
),
|
||||
workflow=WorkflowRuntime(
|
||||
repository=_Repository,
|
||||
system_config=lambda: _Repository(object()),
|
||||
),
|
||||
configuration=RuntimeConfiguration(
|
||||
api=lambda: ApiRuntimeConfig(False, 60, False, True),
|
||||
scheduler=lambda: SchedulerRuntimeConfig(
|
||||
@@ -101,18 +141,20 @@ def _runtime() -> HostRuntime:
|
||||
),
|
||||
chain=lambda: ChainRuntimeConfig(media_extensions=(".mkv",)),
|
||||
),
|
||||
compatibility_api_data=compatibility,
|
||||
)
|
||||
|
||||
|
||||
def test_host_runtime_is_frozen_slotted_and_reuses_compatibility_facade() -> None:
|
||||
"""运行时不可动态扩字段,旧 Facade 必须指向同一个端口实例。"""
|
||||
def test_host_runtime_is_frozen_slotted_and_covers_all_api_domains() -> None:
|
||||
"""运行时不可动态扩字段,且全部正式 API 领域都有命名能力。"""
|
||||
runtime = _runtime()
|
||||
|
||||
configure_api_data_runtime(runtime.compatibility_api_data)
|
||||
|
||||
assert not hasattr(runtime, "__dict__")
|
||||
assert get_api_data_ports() is runtime.compatibility_api_data
|
||||
assert runtime.authentication.user_repository is _Repository
|
||||
assert runtime.messaging.repository is _Repository
|
||||
assert runtime.history.download_repository is _Repository
|
||||
assert runtime.site.repository is _Repository
|
||||
assert runtime.subscription.repository is _Repository
|
||||
assert runtime.workflow.repository is _Repository
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
runtime.agent_chat = runtime.agent_chat
|
||||
|
||||
@@ -137,6 +179,30 @@ def test_fastapi_dependencies_use_fake_runtime_without_real_services() -> None:
|
||||
assert response.json() == {"same_session": True}
|
||||
|
||||
|
||||
def test_official_api_dependencies_do_not_use_string_data_locator() -> None:
|
||||
"""正式业务依赖只能读取 HostRuntime 命名领域,禁止回退字符串注册表。"""
|
||||
dependency_root = PROJECT_ROOT / "app" / "api" / "dependencies"
|
||||
official_modules = {
|
||||
"agent.py",
|
||||
"auth.py",
|
||||
"history.py",
|
||||
"site.py",
|
||||
"subscription.py",
|
||||
"workflow.py",
|
||||
}
|
||||
for filename in official_modules:
|
||||
tree = ast.parse(
|
||||
(dependency_root / filename).read_text(encoding="utf-8")
|
||||
)
|
||||
imported_modules = {
|
||||
node.module
|
||||
for node in ast.walk(tree)
|
||||
if isinstance(node, ast.ImportFrom) and node.module
|
||||
}
|
||||
assert "app.api.data" not in imported_modules
|
||||
assert "app.api.dependencies.data" not in imported_modules
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_component_attaches_init_modules_result(monkeypatch) -> None:
|
||||
"""模块组件把 init_modules 的构建结果发布到当前 AppState。"""
|
||||
|
||||
@@ -62,11 +62,13 @@ def test_dispatcher_retries_then_dead_letters_with_stable_key() -> None:
|
||||
ClaimedOutboxMessage(1, "subscribe.added:42:v1", "subscribe.added", {}, 1, 2),
|
||||
]
|
||||
handler = MagicMock(side_effect=RuntimeError("temporary"))
|
||||
failure_observer = MagicMock()
|
||||
dispatcher = OutboxDispatcher(
|
||||
repository,
|
||||
{"subscribe.added": handler},
|
||||
max_attempts=2,
|
||||
clock=lambda: now,
|
||||
failure_observer=failure_observer,
|
||||
)
|
||||
|
||||
assert dispatcher.dispatch_one() is True
|
||||
@@ -77,6 +79,10 @@ def test_dispatcher_retries_then_dead_letters_with_stable_key() -> None:
|
||||
"subscribe.added:42:v1",
|
||||
"subscribe.added:42:v1",
|
||||
]
|
||||
assert [call.args[0] for call in failure_observer.call_args_list] == [
|
||||
False,
|
||||
True,
|
||||
]
|
||||
|
||||
|
||||
def test_dispatcher_marks_success_and_closes_owned_resource() -> None:
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""用户应用服务的请求级事务边界测试。"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.application.security.user import UserService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_service_commits_staged_mutation() -> None:
|
||||
"""正式用户写用例必须在仓储暂存成功后提交请求 UoW。"""
|
||||
repository = MagicMock()
|
||||
repository.async_create = AsyncMock(return_value={"id": 7})
|
||||
unit_of_work = MagicMock()
|
||||
unit_of_work.commit = AsyncMock()
|
||||
unit_of_work.rollback = AsyncMock()
|
||||
service = UserService(repository, unit_of_work)
|
||||
|
||||
assert await service.create({"name": "demo"}) == {"id": 7}
|
||||
unit_of_work.commit.assert_awaited_once_with()
|
||||
unit_of_work.rollback.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_service_rolls_back_failed_mutation() -> None:
|
||||
"""用户仓储写入失败时不得提交部分事务。"""
|
||||
repository = MagicMock()
|
||||
repository.async_delete = AsyncMock(side_effect=RuntimeError("write failed"))
|
||||
unit_of_work = MagicMock()
|
||||
unit_of_work.commit = AsyncMock()
|
||||
unit_of_work.rollback = AsyncMock()
|
||||
service = UserService(repository, unit_of_work)
|
||||
|
||||
with pytest.raises(RuntimeError, match="write failed"):
|
||||
await service.delete(7)
|
||||
|
||||
unit_of_work.rollback.assert_awaited_once_with()
|
||||
unit_of_work.commit.assert_not_awaited()
|
||||
Reference in New Issue
Block a user