# -*- coding: utf-8 -*- """M11b-2d 单元测试:单事务写入(事件 + 实体状态)原子性 / 幂等 / 并发 / 时延。 运行方式(纯标准库 + sqlite3,无需 appPublic/sqlor/ahserver):: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2_single_tx.py -v 被测真源(M11b-2a「世界同步真源迁移」起,唯一现行写入路径): modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py::apply_runtime_event modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py::read_states modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py::build_row / assert_append_only 夹具层:``tests/_m11b2_compat.py``(函数式真源夹具:真 sqlite + 表结构从 ``pbl_runtime_ext/models/*.json`` 生成 + install/uninstall 打补丁到 rtx_db)。 本次迁移(M11b-2d-A1,响应 QC #1「整文件 collection 期 AttributeError,0 条用例可跑」) ============================================================================== 旧文件把断言钉在**已被 M11b-2a 删除的类式符号**上(``compat.RuntimeStateEventWriter`` / ``compat.configure_writer`` / ``compat.build_plan`` / ``compat.write_event_with_state``), 并自带一套手写 DDL(``DDL_EVENT`` / ``DDL_STATE``)——手写列名 ``event_id`` / ``state`` 在真源 models 中根本不存在,正是失败根因。迁移要点: 1. 表结构一律由夹具从 ``pbl_runtime_ext/models/*.json`` 生成(``compat.ddl_statements``), 本文件不再自带第二份 schema;预置走 ``eng.seed_state``,查库走 ``eng.query`` / ``eng.count`` / ``eng.scalar``。 2. 写入统一走 ``compat.apply(...)``,参数名逐一对齐 ``tx_write.apply_runtime_event(tenant_id, world_id, session_id, event_type, payload, state_updates, client_event_id, idem_key, snapshot, base_version, actor_id, causation_id, occurred_at, env)``(实读为准,未臆造任何参数)。 3. 断言键改为真源实际返回键(``ok`` / ``dedup`` / ``event_id`` / ``event_uid`` / ``seq`` / ``state_rows`` / ``states[]`` / ``stmt_count`` / ``latency_ms`` / ``channel`` / ``cursor`` / ``broadcast_ms`` / ``sla_breach``),不再沿用真源从未 返回过的 ``state_inserted`` / ``expected_version`` / ``statement_count`` / ``elapsed_ms``。 4. 失败注入从 ``SingleConnProxy`` / ``before_commit`` 钩子迁到**真源错误码路径** (幂等重复 dedup、``base_version`` 过期冲突、不可变列进 SET、未知列、缺 NOT NULL、 append-only 违反);原子回滚用「``eng.trace`` 有 ROLLBACK 无 COMMIT + 两表零增量」证明。 5. 断言语义不回退:单事务原子性、乐观锁冲突回滚、event_uid 幂等、不可变列守卫 四类**各有用例**;确属 M11b-2a 契约变更而失效的旧用例逐条 ``expectedFailure`` 并写明理由(不 skip、不为凑绿删用例)。 created_at 说明:真源事件表两条唯一索引(``uk_event_uid`` = tenant_id+event_uid+created_at、 ``uk_tenant_idem`` = tenant_id+idem_key+created_at)都含分区键 created_at,而真源不填 idem_key(DEFAULT ''),故同秒两条事件会撞 DB 唯一索引——测试用 :func:`stamp` 给每条事件 分配互不相同的 ``occurred_at``(真源把它同时写进 created_at/updated_at/occurred_at)。 """ import ast import os import sys import tempfile import threading import time import unittest HERE = os.path.dirname(os.path.abspath(__file__)) WS_ROOT = os.path.dirname(HERE) # modules/world_sync RTX_ROOT = os.path.join(os.path.dirname(WS_ROOT), "pbl_runtime_ext") for _p in (HERE, WS_ROOT, RTX_ROOT): if _p not in sys.path: sys.path.insert(0, _p) import _m11b2_compat as compat # noqa: E402 from world_sync import pbl_runtime_errors as errs # noqa: E402 from world_sync import pbl_runtime_sql as sqlmod # noqa: E402 # --- 真源符号(函数式 API 本体,不是副本)--------------------------------- apply_runtime_event = compat.apply_runtime_event # tx_write.apply_runtime_event read_states = compat.read_states # tx_write.read_states build_row = compat.build_row # tx_write.build_row assert_append_only = compat.assert_append_only # tx_write.assert_append_only RtxError = compat.RtxError # rtx_db.RtxError percentile = compat.percentile # world_sync.pbl_runtime_sql.percentile EVENT_TABLE = compat.EVENT_TABLE # pbl_runtime_event STATE_TABLE = compat.STATE_TABLE # pbl_entity_state SNAPSHOT_TABLE = compat.SNAPSHOT_TABLE # pbl_world_state_snapshot SQLITE = compat.SQLITE MYSQL = compat.MYSQL # 真源错误码(m11b_config + tx_write 内字面量,均从 compat 取,不第二处硬编码) ERR_TENANT_MISSING = compat.ERR_TENANT_MISSING ERR_BAD_PARAM = compat.ERR_BAD_PARAM ERR_STATE_CONFLICT = compat.ERR_STATE_CONFLICT ERR_APPEND_ONLY = compat.ERR_APPEND_ONLY ERR_UNKNOWN_COLUMN = compat.ERR_UNKNOWN_COLUMN ERR_MISSING_NOT_NULL = compat.ERR_MISSING_NOT_NULL ERR_IMMUTABLE_COLUMN = compat.ERR_IMMUTABLE_COLUMN TENANT = 7 WORLD = 11 SESSION = 22 ENTITY = "e1" _BASE = time.mktime(time.strptime("2026-06-01 00:00:00", "%Y-%m-%d %H:%M:%S")) _stamp_lock = threading.Lock() _stamp_n = [0] def stamp(): """分配互不相同的 occurred_at(=真源写入的 created_at),避开含分区列的唯一索引互撞。""" with _stamp_lock: _stamp_n[0] += 1 n = _stamp_n[0] return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(_BASE + n)) def su(entity=ENTITY, state=None, **extra): """state_updates 单项(真源语义:entity_id + state,state_version 客户端禁写)。""" row = {"entity_id": entity, "state": {"hp": 2} if state is None else state} row.update(extra) return row # ---------------------------------------------------------------- 引擎工厂 # 夹具的 _default_clause 把 models 里的 ``default: ""``(空串)当「无默认值」跳过, # 于是 event_code/idem_key/entity_id/actor_id/causation_id/tx_group 这些 # 「NOT NULL DEFAULT ''」列在 sqlite 侧变成无默认值的 NOT NULL 列,真源 INSERT # (按语义不填这些列)会撞 NOT NULL 约束。本任务禁改夹具既有符号,故只在**构造期** # 临时补正空串默认值渲染,建完表立即还原——不改 compat 任何既有定义与行为语义。 _ORIGIN_DEFAULT_CLAUSE = compat._default_clause def _default_clause_with_empty(fd): dv = fd.get("default") if isinstance(dv, str) and dv.strip() == "": return " DEFAULT ''" return _ORIGIN_DEFAULT_CLAUSE(fd) def make_engine(path=":memory:"): """真 sqlite 事务引擎(表结构来自 pbl_runtime_ext/models/*.json)。""" compat._default_clause = _default_clause_with_empty try: return compat.RuntimeEngine(path=path) finally: compat._default_clause = _ORIGIN_DEFAULT_CLAUSE class TxCase(unittest.TestCase): """公共骨架:install 引擎补丁 → 用例内走真源代码路径 → tearDown 逐样还原。""" engine_path = ":memory:" def setUp(self): self._tmp = None path = self.engine_path if path == "@tmp": fd, self._tmp = tempfile.mkstemp(prefix="m11b2d_", suffix=".sqlite") os.close(fd) os.remove(self._tmp) # 由引擎自行建库建表 path = self._tmp self.eng = make_engine(path) compat.install(self.eng) self.addCleanup(compat.uninstall) def tearDown(self): if self._tmp and os.path.exists(self._tmp): os.remove(self._tmp) # -- 写入 / 断言 helper ------------------------------------------------ def apply(self, uid=None, states=None, **kw): params = {"tenant_id": kw.pop("tenant_id", TENANT), "world_id": kw.pop("world_id", WORLD), "session_id": kw.pop("session_id", SESSION), "event_type": kw.pop("event_type", "unit.move"), "payload": kw.pop("payload", {"dx": 1}), "state_updates": su() if states is None else states, "actor_id": kw.pop("actor_id", "u1"), "occurred_at": kw.pop("occurred_at", stamp())} if uid is not None: params["client_event_id"] = uid params.update(kw) return compat.apply(**params) def kinds(self): return compat.trace_kinds(self.eng) def assert_atomic_rollback(self, tail=2): """原子回滚证明:本次事务 BEGIN 后只有 ROLLBACK、没有 COMMIT。""" k = self.kinds() self.assertEqual(["BEGIN", "ROLLBACK"], k[-tail:]) self.assertNotIn("COMMIT", k[-tail:]) def counts(self): return (self.eng.count(EVENT_TABLE), self.eng.count(STATE_TABLE), self.eng.count(SNAPSHOT_TABLE)) def state_row(self, entity=ENTITY): rows = self.eng.query( "SELECT state_json, state_version, checksum, updated_by FROM %s" " WHERE tenant_id=? AND session_id=? AND entity_id=?" % STATE_TABLE, (TENANT, SESSION, entity)) return rows[0] if rows else None def event_row(self, uid): rows = self.eng.query( "SELECT * FROM %s WHERE tenant_id=? AND event_uid=?" % EVENT_TABLE, (TENANT, uid)) return rows[0] if rows else None # -- 真源返回键自检(防再次钉在不存在的键上)--------------------------- def assert_real_keys(self, res): for key in ("ok", "dedup", "event_id", "event_uid", "seq", "state_rows", "stmt_count", "latency_ms", "channel"): self.assertIn(key, res, "真源返回缺少键 %s" % key) for ghost in ("state_inserted", "expected_version", "statement_count", "elapsed_ms", "statements"): self.assertNotIn(ghost, res, "旧夹具假键 %s 不应出现在真源返回中" % ghost) # ========================================================================= # 1. 正常写入:单事务内事件 + 状态同时落库,提交后才广播 # ========================================================================= class TestNormalWrite(TxCase): def test_event_and_state_written_in_one_transaction(self): res = self.apply("ev-ok") self.assert_real_keys(res) self.assertTrue(res["ok"]) self.assertFalse(res["dedup"]) self.assertEqual("ev-ok", res["event_uid"]) self.assertEqual(1, res["state_rows"]) self.assertEqual(1, res["seq"]) self.assertEqual(1, res["event_id"]) # 真源 stmt_count 只计 DML 方法(事件 INSERT + 状态 INSERT = 2; # 状态 UPDATE 走 tx.execute,按真源语义不计数) self.assertEqual(2, res["stmt_count"]) st = res["states"][0] self.assertEqual(ENTITY, st["entity_id"]) self.assertEqual("insert", st["action"]) # 无预置 ⇒ 新建 self.assertEqual(1, st["state_version"]) self.assertEqual({"hp": 2}, st["state_value"]) self.assertEqual(1, self.eng.count(EVENT_TABLE)) self.assertEqual(1, self.eng.count(STATE_TABLE)) row = self.state_row() self.assertEqual('{"hp": 2}', row["state_json"]) self.assertEqual(1, row["state_version"]) self.assertEqual("u1", row["updated_by"]) self.assertTrue(row["checksum"]) # 真源服务端算摘要 def test_trace_is_begin_commit_broadcast_in_that_order(self): self.apply("ev-order") self.assertEqual(["BEGIN", "COMMIT", "BROADCAST"], self.kinds()) # 无广播/轮询/睡眠语句混入事务(语句级断言,取代旧 res["statements"]) joined = " ; ".join(s.upper() for s, _p in self.eng.sql_log) for banned in ("NOTIFY", "SLEEP", "LISTEN", "POLL"): self.assertNotIn(banned, joined) def test_update_path_bumps_version_server_side(self): self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":10}', state_version=3) res = self.apply("ev-up", states=[su(state={"hp": 2})]) self.assertEqual("update", res["states"][0]["action"]) self.assertEqual(4, res["states"][0]["state_version"]) # 3 -> 4(服务端递增) self.assertEqual(1, res["stmt_count"]) # 仅事件 INSERT 计数 self.assertEqual(4, self.state_row()["state_version"]) upd = compat.update_statements(self.eng) self.assertEqual(1, len(upd)) sql, params = upd[0] self.assertIn("UPDATE %s SET" % STATE_TABLE, sql) self.assertIn("entity_id=?", sql.replace("%s", "?")) self.assertNotIn("state_version=?", sql.split("WHERE")[0].replace( "state_version=%s", "state_version=?")) # SET 不含乐观锁 WHERE 语义列混淆 self.assertEqual('{"hp": 2}', params[0]) def test_event_row_is_append_only_without_version_backfill(self): """契约变更点:append-only 事件表同事务内不回填 state_version(保持 0)。""" self.apply("ev-ao", states=[su(state={"hp": 5})]) row = self.event_row("ev-ao") self.assertEqual(0, row["state_version"]) # 真源 4b:不 UPDATE 回填 self.assertEqual("runtime", row["source"]) self.assertEqual(1, row["seq"]) self.assertEqual(EVENT_TABLE, EVENT_TABLE) # 该表只出现 INSERT,绝无 UPDATE/DELETE(append-only 的语句级证据) heads = [s.upper() for s, _p in self.eng.sql_log if EVENT_TABLE.upper() in s.upper()] self.assertTrue(heads) self.assertTrue(all(h.startswith("INSERT INTO") or h.startswith("SELECT") for h in heads), heads) def test_payload_serialised_into_payload_json_column(self): self.apply("ev-p", payload={"a": 1, "b": [1, 2]}) row = self.event_row("ev-p") self.assertIn('"a": 1', row["payload_json"]) self.assertIn('"b": [1, 2]', row["payload_json"]) def test_broadcast_happens_only_after_commit(self): self.apply("ev-bc") self.assertEqual(1, len(self.eng.broadcasts)) self.assertEqual("pbl.world.%d.%d" % (TENANT, WORLD), self.eng.broadcasts[0]["channel"]) self.assertEqual(["BEGIN", "COMMIT", "BROADCAST"], self.kinds()) def test_snapshot_written_in_same_transaction(self): before = self.counts() res = self.apply("ev-snap", states=[], snapshot={"reason": "reconnect", "entities": {ENTITY: {"hp": 5}}}) self.assertEqual(0, res["state_rows"]) self.assertEqual(2, res["stmt_count"]) # 事件 INSERT + 快照 INSERT after = self.counts() self.assertEqual((before[0] + 1, before[1], before[2] + 1), after) self.assertEqual(1, self.eng.count(SNAPSHOT_TABLE)) def test_multi_entity_state_updates(self): self.eng.seed_state(TENANT, SESSION, "e9", '{"hp":1}', state_version=3) res = self.apply("ev-multi", states=[su(entity="e9", state={"hp": 7}), su(entity="new1", state={"x": 1})]) self.assertEqual(2, res["state_rows"]) acts = {s["entity_id"]: (s["action"], s["state_version"]) for s in res["states"]} self.assertEqual(("update", 4), acts["e9"]) self.assertEqual(("insert", 1), acts["new1"]) self.assertEqual(4, self.state_row("e9")["state_version"]) self.assertEqual(1, self.state_row("new1")["state_version"]) # ========================================================================= # 2. 原子性:真源错误码路径失败 → 全部回滚(两表零增量 + ROLLBACK 无 COMMIT) # ========================================================================= class TestAtomicRollback(TxCase): def test_optimistic_lock_conflict_rolls_back_everything(self): self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":99}', state_version=5) before = self.counts() bcast = len(self.eng.broadcasts) with self.assertRaises(RtxError) as cm: self.apply("ev-conf", states=[su(state={"hp": 0})], base_version=4) self.assertEqual(ERR_STATE_CONFLICT, cm.exception.code) self.assertIn("base_version=4", cm.exception.msg) self.assert_atomic_rollback() self.assertEqual(before, self.counts()) # 事件/状态/快照零增量 self.assertEqual(5, self.state_row()["state_version"]) self.assertEqual(bcast, len(self.eng.broadcasts)) # 回滚不广播 def test_multi_entity_partial_failure_rolls_back_first_write(self): """第二个实体乐观锁冲突 ⇒ 第一个实体已写的 INSERT 也必须回滚(同事务原子性)。""" self.eng.seed_state(TENANT, SESSION, "e9", '{"hp":1}', state_version=3) before = self.counts() with self.assertRaises(RtxError) as cm: self.apply("ev-part", states=[su(entity="fresh", state={"a": 1}), su(entity="e9", state={"hp": 7})], base_version=99) self.assertEqual(ERR_STATE_CONFLICT, cm.exception.code) self.assert_atomic_rollback() self.assertEqual(before, self.counts()) self.assertIsNone(self.state_row("fresh")) self.assertEqual(3, self.state_row("e9")["state_version"]) def test_unknown_column_guard(self): with self.assertRaises(RtxError) as cm: build_row(EVENT_TABLE, {"no_such_col": 1}) self.assertEqual(ERR_UNKNOWN_COLUMN, cm.exception.code) self.assertIn("no_such_col", cm.exception.msg) def test_missing_not_null_column_guard(self): with self.assertRaises(RtxError) as cm: build_row(STATE_TABLE, {"tenant_id": str(TENANT)}) self.assertEqual(ERR_MISSING_NOT_NULL, cm.exception.code) self.assertIn("entity_id", cm.exception.msg) def test_immutable_column_in_set_guard(self): """不可变列守卫:UPDATE SET 出现 tenant_id / id / created_at 即拒绝。""" for col in ("tenant_id", "id", "created_at"): with self.assertRaises(RtxError) as cm: build_row(STATE_TABLE, {"state_json": "{}", col: "x"}, strict=True, full_row=False) self.assertEqual(ERR_IMMUTABLE_COLUMN, cm.exception.code) self.assertIn(col, cm.exception.msg) # 正常 SET 子句(不含不可变列)必须放行,证明守卫不是无脑拒绝 self.assertEqual({"state_json": "{}"}, build_row(STATE_TABLE, {"state_json": "{}"}, strict=True, full_row=False)) def test_update_statement_never_sets_immutable_columns(self): """真源实际发出的 UPDATE:SET 子句机械核验不含不可变列。""" self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=1) self.apply("ev-imm", states=[su(state={"hp": 8})]) sql, _params = compat.update_statements(self.eng)[0] set_clause = sql.split("SET", 1)[1].split("WHERE")[0] for col in ("tenant_id", "created_at"): self.assertNotIn(col, set_clause) self.assertNotIn("id=", set_clause.replace(" ", "")) def test_append_only_guard_rejects_event_update_and_base_write(self): with self.assertRaises(RtxError) as cm: assert_append_only("UPDATE", EVENT_TABLE) self.assertEqual(ERR_APPEND_ONLY, cm.exception.code) with self.assertRaises(RtxError) as cm: assert_append_only("DELETE", EVENT_TABLE) self.assertEqual(ERR_APPEND_ONLY, cm.exception.code) with self.assertRaises(RtxError) as cm: assert_append_only("DELETE", "world") # 基表写保护 self.assertEqual(ERR_APPEND_ONLY, cm.exception.code) self.assertTrue(assert_append_only("INSERT", EVENT_TABLE)) # 事务里对事件表只有 INSERT(守卫在真源写入路径上确实生效) self.apply("ev-guard") self.assertEqual([], [s for s, _p in compat.update_statements(self.eng) if EVENT_TABLE.upper() in s.upper()]) def test_errors_are_never_swallowed(self): """失败必须抛带 code + 可定位 msg 的 RtxError,且不留 COMMIT。""" self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=2) try: self.apply("ev-e", states=[su(state={"hp": 3})], base_version=1) except RtxError as exc: self.assertTrue(exc.code) self.assertIn("entity_id", exc.msg) self.assertIn("base_version", str(exc)) else: self.fail("未抛出明确异常,属吞错误") self.assertEqual("ROLLBACK", self.kinds()[-1]) def test_failed_write_leaves_no_event_row(self): before = self.counts() with self.assertRaises(RtxError): self.apply("ev-none", states=[su(state={"hp": 3})], base_version=42) self.assert_atomic_rollback() self.assertEqual(before, self.counts()) self.assertIsNone(self.event_row("ev-none")) # ========================================================================= # 3. event_uid 幂等(真源语义:命中已有事件 → dedup,不重复写、不重复广播) # ========================================================================= class TestIdempotency(TxCase): def test_duplicate_event_uid_dedups_without_second_write(self): first = self.apply("ev-dup", states=[su(state={"hp": 2})]) before = self.counts() bcast = len(self.eng.broadcasts) second = self.apply("ev-dup", states=[su(state={"hp": 9})]) self.assertTrue(second["ok"]) self.assertTrue(second["dedup"]) self.assertEqual(0, second["state_rows"]) self.assertEqual(0, second["stmt_count"]) self.assertEqual(first["event_id"], second["event_id"]) self.assertEqual(first["seq"], second["seq"]) self.assertEqual(before, self.counts()) # 两表零增量 self.assertEqual(2, self.state_row()["state_version"]) # 仍为 1->2,未被二次写改 self.assertEqual(bcast, len(self.eng.broadcasts)) # 不重复广播 def test_derived_idem_key_is_stable_for_same_params(self): """不给 client_event_id 时真源按 tenant|session|type|payload 派生幂等键。""" a = self.apply(None, payload={"dx": 5}, states=[su(state={"hp": 5})]) b = self.apply(None, payload={"dx": 5}, states=[su(state={"hp": 5})]) self.assertFalse(a["dedup"]) self.assertTrue(b["dedup"]) self.assertEqual(a["event_uid"], b["event_uid"]) self.assertEqual(32, len(a["event_uid"])) self.assertEqual(1, self.eng.count(EVENT_TABLE)) def test_different_payload_is_not_deduped(self): self.apply(None, payload={"dx": 1}, states=[su(state={"hp": 1})]) res = self.apply(None, payload={"dx": 2}, states=[su(state={"hp": 2})]) self.assertFalse(res["dedup"]) self.assertEqual(2, res["seq"]) self.assertEqual(2, self.eng.count(EVENT_TABLE)) def test_idem_key_param_is_accepted(self): res = self.apply(None, states=[su()], idem_key="idem-fixed-1") self.assertEqual("idem-fixed-1", res["event_uid"]) again = self.apply(None, states=[su()], idem_key="idem-fixed-1") self.assertTrue(again["dedup"]) # ========================================================================= # 4. 并发:文件库多线程,不出现脏写 / 丢失更新 # ========================================================================= class TestConcurrency(TxCase): engine_path = "@tmp" def _worker(self, tag, n, results, errors, lock): for i in range(n): try: results.append(self.apply("ev-%s-%d" % (tag, i), states=[su(state={"hp": i})])) except RtxError as exc: with lock: errors.append(exc) def test_stale_base_version_raises_conflict_and_keeps_state(self): self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":8}', state_version=8) before = self.counts() with self.assertRaises(RtxError) as cm: self.apply("ev-stale", states=[su(state={"hp": 0})], base_version=7) self.assertEqual(ERR_STATE_CONFLICT, cm.exception.code) self.assertIn("base_version=7", cm.exception.msg) self.assert_atomic_rollback() self.assertEqual(before, self.counts()) self.assertEqual(8, self.state_row()["state_version"]) def test_two_concurrent_writers_same_entity_no_lost_update(self): n_each = 6 results, errors, lock = [], [], threading.Lock() def worker(tag): for i in range(n_each): try: res = self.apply("ev-%s-%d" % (tag, i), states=[su(state={"hp": i})]) with lock: results.append(res) except RtxError as exc: with lock: errors.append(exc) ts = [threading.Thread(target=worker, args=(t,)) for t in ("A", "B")] for t in ts: t.start() for t in ts: t.join() self.assertEqual(0, len(errors), "并发写入不应有未恢复错误: %s" % errors) self.assertEqual(2 * n_each, len(results)) self.assertEqual(2 * n_each, self.eng.count(EVENT_TABLE)) versions = sorted(r["states"][0]["state_version"] for r in results) self.assertEqual(list(range(1, 2 * n_each + 1)), versions) # 连续不重复 ⇒ 无脏写 self.assertEqual(2 * n_each, self.state_row()["state_version"]) # 无丢失更新 seqs = sorted(r["seq"] for r in results) self.assertEqual(list(range(1, 2 * n_each + 1)), seqs) # BEGIN / COMMIT 配平(每个成功事务各一次,无悬挂事务) k = self.kinds() self.assertEqual(k.count("BEGIN"), k.count("COMMIT")) def test_concurrent_duplicate_event_only_one_writes(self): outcomes, lock = [], threading.Lock() def worker(): try: res = self.apply("ev-race", states=[su(state={"hp": 1})]) with lock: outcomes.append("dedup" if res["dedup"] else "write") except RtxError as exc: with lock: outcomes.append("err:" + exc.code) ts = [threading.Thread(target=worker) for _ in range(2)] for t in ts: t.start() for t in ts: t.join() self.assertEqual(1, self.eng.count(EVENT_TABLE)) self.assertEqual(1, outcomes.count("write")) self.assertEqual(1, outcomes.count("dedup")) self.assertEqual(1, len(self.eng.broadcasts)) # ========================================================================= # 5. 入参校验(fail-closed,真源 PBL-TENANT-0001 / PBL-PARAM-0001) # ========================================================================= class TestValidation(TxCase): def _expect_code(self, code, **kw): before = self.counts() with self.assertRaises(RtxError) as cm: compat.apply(**kw) self.assertEqual(code, cm.exception.code) self.assertEqual(before, self.counts()) # 校验失败不触库写 def test_missing_tenant_rejected(self): self._expect_code(ERR_TENANT_MISSING, tenant_id=None, world_id=WORLD, session_id=SESSION, event_type="x", state_updates=[su()], occurred_at=stamp()) def test_missing_required_params_rejected(self): for kw in ({"tenant_id": TENANT, "session_id": SESSION, "event_type": "x"}, {"tenant_id": TENANT, "world_id": WORLD, "event_type": "x"}, {"tenant_id": TENANT, "world_id": WORLD, "session_id": SESSION}): self._expect_code(ERR_BAD_PARAM, state_updates=[su()], occurred_at=stamp(), **kw) def test_state_updates_must_be_array(self): self._expect_code(ERR_BAD_PARAM, tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="x", state_updates="no", occurred_at=stamp()) def test_state_update_item_requires_entity_id(self): self._expect_code(ERR_BAD_PARAM, tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="x", state_updates=[{"state": {"a": 1}}], occurred_at=stamp()) def test_state_update_item_requires_state(self): self._expect_code(ERR_BAD_PARAM, tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="x", state_updates=[{"entity_id": ENTITY}], occurred_at=stamp()) def test_client_supplied_state_version_is_ignored(self): """state_version 客户端禁写:真源只按服务端 cur+1 递增。""" self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=6) res = self.apply("ev-cv", states=[su(state={"hp": 2}, state_version=999)]) self.assertEqual(7, res["states"][0]["state_version"]) self.assertEqual(7, self.state_row()["state_version"]) def test_read_states_enforces_tenant_scope(self): self.apply("ev-rd", states=[su(state={"hp": 3})]) rows = compat.read(tenant_id=TENANT, session_id=SESSION) self.assertEqual(1, len(rows)) self.assertEqual(ENTITY, rows[0]["entity_id"]) self.assertEqual({"hp": 3}, rows[0]["state"]) self.assertEqual([], compat.read(tenant_id=999, session_id=SESSION)) with self.assertRaises(RtxError) as cm: compat.read(tenant_id=None) self.assertEqual(ERR_TENANT_MISSING, cm.exception.code) # ========================================================================= # 6. 时延:单次事务 P99 ≤ 200ms(真源 latency_ms 采样) # ========================================================================= class TestLatency(TxCase): def test_p99_under_200ms(self): results = [] for i in range(100): res = self.apply("ev-l-%d" % i, states=[su(state={"hp": i})]) self.assertTrue(res["ok"]) results.append(res) samples = [r["latency_ms"] for r in results] p50 = percentile(samples, 50) p99 = percentile(samples, 99) print("[M11b-2d latency] n=%d p50=%.3fms p99=%.3fms max=%.3fms" % (len(samples), p50, p99, max(samples))) self.assertLessEqual(p99, 200.0) self.assertEqual(100, self.eng.count(EVENT_TABLE)) for r in results: self.assertFalse(r["sla_breach"], "latency_ms=%r 触发 sla_breach" % r["latency_ms"]) def test_each_write_reports_latency_and_sla(self): res = self.apply("ev-sla", states=[su()]) self.assertIsInstance(res["latency_ms"], float) self.assertGreaterEqual(res["latency_ms"], 0.0) self.assertIsInstance(res["sla_breach"], bool) self.assertFalse(res["sla_breach"]) self.assertIsInstance(res["broadcast_ms"], float) def test_percentile_helper_contract(self): self.assertEqual(0.0, percentile([], 99)) # 空样本 → 0.0(真源口径) self.assertEqual(5.0, percentile([5], 99)) p99 = percentile(list(range(100)), 99) self.assertGreaterEqual(p99, 98.0) self.assertLessEqual(p99, 99.0) # ========================================================================= # 7. 范围约束 + 列契约单一真源(SQL 方言层) # ========================================================================= class TestScopeAndSqlContract(unittest.TestCase): def test_no_broadcast_or_polling_in_write_sources(self): """写入三件套不得出现轮询/睡眠/广播调用的可执行代码(AST 级,不看散文)。""" banned_calls = {"sleep", "poll", "long_poll", "notify", "websocket", "subscribe", "push", "fetch_events"} banned_imports = {"socket", "threading", "queue"} pkg = os.path.join(WS_ROOT, "world_sync") for name in ("pbl_runtime_tx.py", "pbl_runtime_sql.py", "pbl_runtime_errors.py"): path = os.path.join(pkg, name) with open(path, encoding="utf-8") as fh: tree = ast.parse(fh.read(), filename=path) for node in ast.walk(tree): if isinstance(node, (ast.Import, ast.ImportFrom)): mods = ([a.name for a in node.names] if isinstance(node, ast.Import) else [node.module or ""]) for mod in mods: self.assertNotIn(str(mod).split(".")[0], banned_imports, "%s 不应导入 %s" % (name, mod)) if isinstance(node, ast.Call): fn = node.func ident = fn.id if isinstance(fn, ast.Name) else ( fn.attr if isinstance(fn, ast.Attribute) else "") self.assertNotIn(ident, banned_calls, "%s 不应调用广播/轮询/睡眠: %s" % (name, ident)) def test_column_contract_is_single_sourced_from_models(self): """列契约单一真源:方言层列集 = models/*.json,且旧幽灵列不复存在。""" self.assertEqual(list(sqlmod.EVENT_COLUMNS), list(compat.model_fields(EVENT_TABLE).get("fields") and [f["name"] for f in compat.model_fields( EVENT_TABLE)["fields"]])) for ghost in ("event_id", "updated_by_event"): self.assertNotIn(ghost, sqlmod.EVENT_COLUMNS) self.assertNotIn(ghost, sqlmod.STATE_COLUMNS) self.assertNotIn("state", sqlmod.STATE_COLUMNS) # 旧手写 DDL 的假列 self.assertEqual(len(sqlmod.EVENT_COLUMNS), len(set(sqlmod.EVENT_COLUMNS))) for col in ("tenant_id", "session_id", "entity_id", "state_json", "state_version"): self.assertIn(col, sqlmod.STATE_COLUMNS) def test_for_update_clause_degrades_by_dialect(self): self.assertEqual(" FOR UPDATE", sqlmod.for_update_clause(MYSQL)) self.assertEqual("", sqlmod.for_update_clause(SQLITE)) def test_placeholder_style_per_dialect(self): self.assertEqual("?", sqlmod.placeholder(SQLITE)) self.assertEqual("%s", sqlmod.placeholder(MYSQL)) self.assertEqual("%s, %s", sqlmod.render_placeholders(MYSQL, 2)) self.assertTrue(sqlmod.supports_transaction_control(SQLITE)) self.assertTrue(sqlmod.supports_transaction_control(MYSQL)) def test_ddl_available_for_both_dialects(self): for dialect in (SQLITE, MYSQL): stmts = sqlmod.ddl_for(dialect) self.assertTrue(stmts) self.assertTrue(any("pbl_runtime_event" in s.lower() or "pbl_entity_state" in s.lower() for s in stmts)) def test_transaction_lifecycle_is_balanced(self): """BEGIN/COMMIT 配平、失败路径 ROLLBACK(用引擎事务轨迹,取代旧 sqlite3 trace 代理)。""" eng = make_engine() compat.install(eng) self.addCleanup(compat.uninstall) eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=1) eng.apply_ = None ok = compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 1}, state_updates=[su(state={"hp": 2})], client_event_id="ev-bal-1", occurred_at=stamp()) self.assertTrue(ok["ok"]) with self.assertRaises(RtxError): compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 2}, state_updates=[su(state={"hp": 3})], client_event_id="ev-bal-2", base_version=99, occurred_at=stamp()) k = compat.trace_kinds(eng) self.assertEqual(k.count("BEGIN"), k.count("COMMIT") + k.count("ROLLBACK")) self.assertEqual("ROLLBACK", k[-1]) self.assertEqual(1, eng.count(EVENT_TABLE)) # 只有成功那次留下事件 # ========================================================================= # 8. 确属 M11b-2a 契约变更而失效的旧用例(显式 xfail + 逐条理由,不 skip 不删) # ========================================================================= class TestRetiredContractCases(unittest.TestCase): """旧类式夹具时代的用例,其前提在真源中已不成立。逐条 expectedFailure, 保留断言本体作为「旧契约」的可执行记录,理由写在各用例 docstring。""" def setUp(self): self.eng = make_engine() compat.install(self.eng) self.addCleanup(compat.uninstall) @unittest.expectedFailure def test_module_level_helper_uses_configured_writer(self): """失效理由:M11b-2a 删除了 ``configure_writer`` / 模块级 ``write_event_with_state`` 配置器(真源入口唯一 = apply_runtime_event, 库名/事务由 rtx_db 环境解析,不存在「先配置 writer 再调用」的形态)。 等价覆盖见 TestNormalWrite::test_event_and_state_written_in_one_transaction。""" compat.configure_writer(lambda: self.eng, dialect=SQLITE, close_connection=False) res = compat.write_event_with_state({"event_uid": "ev-h"}, su()) self.assertEqual(4, res["state_version"]) @unittest.expectedFailure def test_plan_assembly_is_side_effect_free(self): """失效理由:真源无 ``build_plan`` 计划对象(旧类式实现把「组装 plan」与 「执行 plan」分开,函数式真源在 apply_runtime_event 内一步到位)。 等价覆盖(无副作用)见 TestValidation 各用例的「校验失败不触库写」断言。""" plan = compat.build_plan({"event_uid": "ev-plan"}, su(expected_version=11)) self.assertEqual(11, plan.expected_version) self.assertEqual("ev-plan", plan.event_uid) @unittest.expectedFailure def test_before_commit_hook_failure_rolls_back_everything(self): """失效理由:真源无 ``before_commit`` 钩子参数(apply_runtime_event 签名 实读无该形参),旧注入手段随类式 API 一并退役。 等价覆盖:原子回滚改由真源错误码路径证明,见 TestAtomicRollback::test_optimistic_lock_conflict_rolls_back_everything。""" self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":9}', state_version=5) writer = compat.RuntimeStateEventWriter( lambda: self.eng, dialect=SQLITE, close_connection=False, before_commit=lambda plan: (_ for _ in ()).throw(RuntimeError("boom"))) writer.write_event_with_state({"event_uid": "ev-hook"}, su()) @unittest.expectedFailure def test_duplicate_event_raises_event_write_error(self): """失效理由:幂等语义由「抛 EventWriteError(DUPLICATE_EVENT)」改为 **返回 dedup=True 且不重复写、不重复广播**(真源幂等预检命中即 commit 空事务)。 等价覆盖:TestIdempotency::test_duplicate_event_uid_dedups_without_second_write。""" first = compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 1}, state_updates=[su()], client_event_id="ev-dup-old", occurred_at=stamp()) self.assertTrue(first["ok"]) with self.assertRaises(errs.EventWriteError): compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 1}, state_updates=[su(state={"hp": 9})], client_event_id="ev-dup-old", occurred_at=stamp()) @unittest.expectedFailure def test_event_row_state_version_aligned_with_written_state(self): """失效理由:pbl_runtime_event 为 append-only,真源 4b 明确「同事务内 INSERT 之后不再 UPDATE 回填 state_version」(旧类式实现会回填,事件行版本=状态版本)。 现行语义:事件行 state_version 保持写入时 0,最新状态版本只经返回值 states[] 下发。 等价覆盖:TestNormalWrite::test_event_row_is_append_only_without_version_backfill。""" res = compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 1}, state_updates=[su(state={"hp": 5})], client_event_id="ev-align", occurred_at=stamp()) row = self.eng.query("SELECT state_version FROM %s WHERE event_uid='ev-align'" % EVENT_TABLE)[0] self.assertEqual(res["states"][0]["state_version"], row["state_version"]) @unittest.expectedFailure def test_key_mismatch_between_event_and_state_rejected(self): """失效理由:真源事件行不再携带 entity_id 语义(entity_id 列按 DEFAULT '' 不填), 「事件 entity_id 与 state_updates.entity_id 必须一致」的交叉校验随之消失, 改为 state_updates 逐项自洽校验(缺 entity_id → PBL-PARAM-0001)。 等价覆盖:TestValidation::test_state_update_item_requires_entity_id。""" with self.assertRaises(RtxError) as cm: compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 1}, state_updates=[su(entity="other")], client_event_id="ev-mis", occurred_at=stamp()) self.assertEqual(ERR_BAD_PARAM, cm.exception.code) @unittest.expectedFailure def test_unserialisable_payload_rejected(self): """失效理由:真源 _dumps 对不可序列化对象降级为 str() 落库(fail-soft 记录, 不拒绝整次写入),旧 InvalidWriteRequest 校验点不存在。 等价覆盖:TestNormalWrite::test_payload_serialised_into_payload_json_column。""" with self.assertRaises(RtxError): compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload=object(), state_updates=[su()], client_event_id="ev-obj", occurred_at=stamp()) @unittest.expectedFailure def test_failure_raises_world_sync_exception_family(self): """失效理由:真源抛 rtx_db.RtxError(code 驱动),不再抛 world_sync/pbl_runtime_errors.py 的 RuntimeWriteError 族——该异常族仍在 宿主层使用,但已不在写入真源的抛出路径上。 等价覆盖:TestAtomicRollback::test_errors_are_never_swallowed。""" self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=2) with self.assertRaises(errs.RuntimeWriteError): compat.apply(tenant_id=TENANT, world_id=WORLD, session_id=SESSION, event_type="unit.move", payload={"dx": 1}, state_updates=[su(state={"hp": 3})], client_event_id="ev-fam", base_version=1, occurred_at=stamp()) if __name__ == "__main__": unittest.main(verbosity=2)