|
4 | 4 | from concurrent.futures import ThreadPoolExecutor |
5 | 5 | from dataclasses import replace |
6 | 6 | from datetime import datetime, timedelta, timezone |
| 7 | +from unittest.mock import Mock |
7 | 8 |
|
8 | 9 | import pytest |
9 | 10 | import sqlalchemy as sa |
|
18 | 19 | PluginPayloadSourceType, |
19 | 20 | PluginSourceCandidate, |
20 | 21 | TrustedPluginSourceType, |
| 22 | + WritePluginIdentityCommand, |
21 | 23 | plan_legacy_plugin_identity, |
22 | 24 | ) |
23 | 25 | from app.db.adapters.pluginidentity import TransactionalPluginIdentityStore |
@@ -199,6 +201,30 @@ def update(version: str) -> str: |
199 | 201 | assert identity_store.get("DemoPlugin").revision == 2 |
200 | 202 |
|
201 | 203 |
|
| 204 | +def test_write_command_rolls_back_when_conditional_replace_loses_race() -> None: |
| 205 | + """条件更新在最终 CAS 失利时必须确定回滚并报告竞争冲突。""" |
| 206 | + current = _identity() |
| 207 | + repository = Mock() |
| 208 | + repository.get.return_value = current |
| 209 | + repository.stage_replace.return_value = False |
| 210 | + unit_of_work = Mock() |
| 211 | + command = WritePluginIdentityCommand(repository, unit_of_work) |
| 212 | + |
| 213 | + with pytest.raises(PluginIdentityConflictError, match="其他任务更新"): |
| 214 | + command.execute( |
| 215 | + replace( |
| 216 | + current, |
| 217 | + declared_version="2.0.0", |
| 218 | + updated_at=NOW + timedelta(seconds=1), |
| 219 | + ), |
| 220 | + expected_revision=current.revision, |
| 221 | + ) |
| 222 | + |
| 223 | + repository.stage_replace.assert_called_once() |
| 224 | + unit_of_work.commit.assert_not_called() |
| 225 | + unit_of_work.rollback.assert_called_once() |
| 226 | + |
| 227 | + |
202 | 228 | def test_local_payload_preserves_trusted_online_binding() -> None: |
203 | 229 | """本地开发覆盖只改变载荷事实,不得抹掉可信在线更新仓库。""" |
204 | 230 | identity = replace( |
|
0 commit comments