refactor: migrate high-risk write transactions

This commit is contained in:
jxxghp
2026-08-21 20:46:57 +08:00
parent 7de72e0e83
commit b598b516d5
12 changed files with 312 additions and 9 deletions
+7 -3
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
"edge_count": 6092,
"edge_sha256": "1935d43d8d3c0c3e3687109f0b81a4e56cbc4bc2ed4763a706e2b0d7c119f378",
"edge_count": 6095,
"edge_sha256": "06c09c175ac007c7ef891e2a25f5036c8bc3993a817837c03824bb591168ea73",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -5890,12 +5890,15 @@
"app.startup.plugins_initializer -> app.application",
"app.startup.plugins_initializer -> app.application.plugin",
"app.startup.plugins_initializer -> app.application.plugin.catalog",
"app.startup.plugins_initializer -> app.application.plugin.data",
"app.startup.plugins_initializer -> app.application.plugin.routes",
"app.startup.plugins_initializer -> app.application.site",
"app.startup.plugins_initializer -> app.db",
"app.startup.plugins_initializer -> app.db.oper",
"app.startup.plugins_initializer -> app.db.oper.plugindata",
"app.startup.plugins_initializer -> app.db.oper.systemconfig",
"app.startup.plugins_initializer -> app.db.session",
"app.startup.plugins_initializer -> app.db.uow",
"app.startup.plugins_initializer -> app.foundation",
"app.startup.plugins_initializer -> app.foundation.version",
"app.startup.plugins_initializer -> app.runtime",
@@ -6109,7 +6112,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
"module_count": 757,
"module_count": 758,
"modules": [
"app",
"app.adapters",
@@ -6379,6 +6382,7 @@
"app.application.plugin",
"app.application.plugin.catalog",
"app.application.plugin.config",
"app.application.plugin.data",
"app.application.plugin.folders",
"app.application.plugin.install",
"app.application.plugin.routes",
+92
View File
@@ -0,0 +1,92 @@
"""Agent 会话删除的请求级事务边界测试。"""
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from app.application.messaging.chat import AgentChatService
def _principal() -> SimpleNamespace:
"""构造拥有目标会话的普通用户。"""
return SimpleNamespace(id=1, name="alice", is_superuser=False)
def _chat() -> SimpleNamespace:
"""构造应用服务投影所需的最小会话记录。"""
return SimpleNamespace(
id=9,
session_id="session-9",
client_session_id=None,
title="事务会话",
channel=None,
source=None,
user_id="1",
username="alice",
original_chat_id=None,
message_count=0,
created_at=None,
updated_at=None,
display_messages=[],
)
@pytest.mark.asyncio
async def test_delete_stages_then_commits_once() -> None:
"""有权限的会话删除只能由 Service 在暂存成功后提交一次。"""
calls: list[str] = []
repository = Mock()
repository.async_get = AsyncMock(return_value=_chat())
repository.async_stage_delete = AsyncMock(
side_effect=lambda **_kwargs: calls.append("stage") or True
)
unit_of_work = Mock()
unit_of_work.commit = AsyncMock(
side_effect=lambda: calls.append("commit")
)
unit_of_work.rollback = AsyncMock()
service = AgentChatService(repository, unit_of_work)
assert await service.delete("session-9", _principal()) is True
assert calls == ["stage", "commit"]
unit_of_work.rollback.assert_not_awaited()
@pytest.mark.asyncio
async def test_delete_rolls_back_flush_failure() -> None:
"""暂存删除失败必须回滚并传播原异常。"""
error = RuntimeError("flush failed")
repository = Mock()
repository.async_get = AsyncMock(return_value=_chat())
repository.async_stage_delete = AsyncMock(side_effect=error)
unit_of_work = Mock()
unit_of_work.commit = AsyncMock()
unit_of_work.rollback = AsyncMock()
service = AgentChatService(repository, unit_of_work)
with pytest.raises(RuntimeError) as raised:
await service.delete("session-9", _principal())
assert raised.value is error
unit_of_work.commit.assert_not_awaited()
unit_of_work.rollback.assert_awaited_once_with()
@pytest.mark.asyncio
async def test_delete_missing_chat_does_not_open_write_transaction() -> None:
"""会话不存在时保持旧 False 返回,且不执行 stage 或 commit。"""
repository = Mock()
repository.async_get = AsyncMock(return_value=None)
repository.async_stage_delete = AsyncMock()
unit_of_work = Mock()
unit_of_work.commit = AsyncMock()
unit_of_work.rollback = AsyncMock()
service = AgentChatService(repository, unit_of_work)
assert await service.delete("missing", _principal()) is False
repository.async_stage_delete.assert_not_awaited()
unit_of_work.commit.assert_not_awaited()
unit_of_work.rollback.assert_not_awaited()
+60
View File
@@ -0,0 +1,60 @@
"""插件持久化数据删除的事务所有权测试。"""
from unittest.mock import Mock
import pytest
from app.application.plugin.data import DeletePluginDataCommand
from app.db.models.plugindata import PluginData
from app.db.oper.plugindata import PluginDataOper
def test_delete_plugin_data_stages_then_commits() -> None:
"""插件重置必须在仓储暂存完成后由 Application Command 提交。"""
calls: list[str] = []
repository = Mock()
repository.stage_delete.side_effect = lambda _plugin_id: calls.append("stage")
unit_of_work = Mock()
unit_of_work.commit.side_effect = lambda: calls.append("commit")
command = DeletePluginDataCommand(repository, unit_of_work)
command.execute("Demo")
assert calls == ["stage", "commit"]
repository.stage_delete.assert_called_once_with("Demo")
unit_of_work.rollback.assert_not_called()
def test_delete_plugin_data_rolls_back_commit_failure() -> None:
"""插件数据删除提交失败必须回滚并保留原异常。"""
error = RuntimeError("commit failed")
repository = Mock()
unit_of_work = Mock()
unit_of_work.commit.side_effect = error
command = DeletePluginDataCommand(repository, unit_of_work)
with pytest.raises(RuntimeError) as raised:
command.execute("Demo")
assert raised.value is error
unit_of_work.rollback.assert_called_once_with()
def test_plugin_data_oper_stage_delete_does_not_commit(db, monkeypatch) -> None:
"""Oper 只暂存目标插件删除,其他插件数据与提交权均不受影响。"""
db.add(
PluginData(plugin_id="Target", key="one", value=1),
PluginData(plugin_id="Other", key="two", value=2),
)
commit = Mock(wraps=db.session.commit)
monkeypatch.setattr(db.session, "commit", commit)
oper = PluginDataOper(db.session)
oper.stage_delete("Target")
remaining = PluginData.get_plugin_data(db.session, "Other")
deleted = PluginData.get_plugin_data(db.session, "Target")
assert [item.key for item in remaining] == ["two"]
assert deleted == []
commit.assert_not_called()
db.session.rollback()