world_sync/tests/test_m11b2d_fixture_contract.py
2026-09-22 21:00:10 +08:00

286 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""M11b-2d-A3 契约守卫用例:函数式夹具契约 + 真源返回键齐全性。
来源
====
本文件由一次性调试探针 ``modules/world_sync/.m11b2d_probe.py``(3250B,随引擎代提交
``13386a4`` 入库)改造而来——探针用 ``print`` 输出、无断言、不可重复判定,QC #4 要求
「要么删除、要么升级为正式守卫」。本文件即升级结果,探针本体已 ``git rm``。
守卫什么(三件事)
==================
1. **旧类式符号不再存在**:``RuntimeStateEventWriter`` / ``configure_writer`` /
``build_plan`` / 类式 ``write_event_with_state`` 不得再出现在夹具层
``tests/_m11b2_compat`` 上(它们曾构成第二条夹具路径,把测试钉在已删除的调用形态上,
实测 22 failed)。
2. **新函数式符号可用**:夹具只保留一条路径——``install/uninstall/apply/read/
trace_kinds/update_statements/insert_statements`` + ``RuntimeEngine``,且真源本体
``apply_runtime_event`` / ``read_states`` 经夹具直连(不是副本)。
3. **真源返回键齐全**:``apply`` 的返回值键集合**逐键断言**,不做任何增补(夹具
``apply`` 只是 ``asyncio.run(apply_runtime_event(...))`` 的同步入口)。分支覆盖
成功写 / 幂等命中 / 乐观锁冲突 / 参数门禁 / 快照 / 提交后广播。
边界(遵守 QC #3 允许路径与本任务禁改清单)
==========================================
* 不改业务实现、不改 ``pbl_runtime_ext/models/*.json``、不改 ``load_path.py``;
* 不改 ``tests/_m11b2_compat.py`` 任何既有符号(只读引用);
* 不改 ``test_m11b2_single_tx.py`` / ``test_m11b2_wiring.py``(并行任务独占);
* 权威契约定义在 ``tests/_m11b2_compat.py`` 顶部 docstring 与
``docs/work-log/``… 不,登记文档为 ``skill/SKILL.md`` 指向的
``docs/work-log/m11b2d-fixture-contract.md``,本文件只做机械断言,不重复搬运说明。
夹具的 ``_default_clause`` 把 models 里 ``default: ""``(空串)当「无默认值」跳过,
于是 ``event_code`` 等「NOT NULL DEFAULT ''」列在 sqlite 侧成为无默认值的 NOT NULL 列,
真源 INSERT(按语义不填这些列)会撞约束。本文件与 ``test_m11b2_single_tx.py`` 同口径:
**仅在构造期**临时补正空串默认值渲染,建完表立即还原,不改 compat 既有定义与行为语义。
"""
import inspect
import os
import sys
import unittest
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
sys.path.insert(0, os.path.dirname(HERE))
import _m11b2_compat as compat # noqa: E402
apply_runtime_event = compat.apply_runtime_event # pbl_runtime_ext.tx_write 真源本体
read_states = compat.read_states
RtxError = compat.RtxError
EVENT_TABLE = compat.EVENT_TABLE
STATE_TABLE = compat.STATE_TABLE
SNAPSHOT_TABLE = compat.SNAPSHOT_TABLE
ERR_TENANT_MISSING = compat.ERR_TENANT_MISSING
ERR_BAD_PARAM = compat.ERR_BAD_PARAM
ERR_STATE_CONFLICT = compat.ERR_STATE_CONFLICT
# 真源 tx_write.apply_runtime_event 成功分支的返回键(源码 tx_write.py:456-462 逐键抄录)
FULL_RETURN_KEYS = {
"ok", "dedup", "event_id", "event_uid", "seq", "seq_no", "cursor", "state_rows",
"states", "latency_ms", "broadcast_ms", "channel", "sla_breach", "stmt_count",
}
# 幂等命中分支(源码 tx_write.py:383-389):无 states/broadcast_ms/sla_breach/seq_no
DEDUP_RETURN_KEYS = {
"ok", "dedup", "event_id", "event_uid", "seq", "cursor", "state_rows",
"latency_ms", "channel", "stmt_count",
}
# 真源 apply_runtime_event 形参(源码 tx_write.py:344-348),全部默认 None
TRUE_SOURCE_PARAMS = [
"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",
]
# 夹具层禁止复活的旧类式符号
DEAD_CLASS_STYLE_SYMBOLS = [
"RuntimeStateEventWriter", "configure_writer", "build_plan", "write_event_with_state",
]
# 夹具层必须存在的函数式入口
LIVE_FIXTURE_SYMBOLS = [
"RuntimeEngine", "install", "uninstall", "apply", "read", "trace_kinds",
"update_statements", "insert_statements", "apply_runtime_event", "read_states",
"build_row", "assert_append_only", "RtxError", "percentile",
"EVENT_TABLE", "STATE_TABLE", "SNAPSHOT_TABLE",
]
_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
def su(entity="e1", state=None):
return {"entity_id": entity, "state": {"hp": 2} if state is None else state}
class FixtureSurfaceCase(unittest.TestCase):
"""夹具面:旧符号已死、新符号可直连真源。"""
def test_dead_class_style_symbols_absent(self):
for name in DEAD_CLASS_STYLE_SYMBOLS:
self.assertFalse(
hasattr(compat, name),
"夹具层不得复活旧类式符号 %s(第二条夹具路径 = M11b-2c 22 failed 根因)" % name)
def test_live_function_style_symbols_present(self):
for name in LIVE_FIXTURE_SYMBOLS:
self.assertTrue(hasattr(compat, name), "夹具缺少函数式符号 %s" % name)
self.assertTrue(callable(compat.install))
self.assertTrue(callable(compat.uninstall))
self.assertTrue(callable(compat.apply))
self.assertTrue(callable(compat.read))
def test_fixture_apply_is_thin_wrapper_on_true_source(self):
"""夹具 apply/read 不得自带实现——必须是真源的同步入口(零返回键增补)。"""
self.assertIs(compat.apply_runtime_event, apply_runtime_event)
src = inspect.getsource(compat.apply)
self.assertIn("apply_runtime_event", src)
self.assertNotIn("update(", src) # 不补键
self.assertNotIn("setdefault", src)
self.assertNotIn("_augment", src)
def test_true_source_signature(self):
"""真源逐参数签名钉死:形参名/顺序/默认值(文档 m11b2d-fixture-contract.md 同源)。"""
spec = inspect.signature(apply_runtime_event)
self.assertEqual(list(spec.parameters), TRUE_SOURCE_PARAMS)
for name in TRUE_SOURCE_PARAMS:
self.assertIsNone(spec.parameters[name].default,
"形参 %s 默认值应为 None" % name)
def test_read_states_signature(self):
spec = inspect.signature(read_states)
self.assertEqual(list(spec.parameters),
["tenant_id", "session_id", "entity_ids", "world_id", "env"])
class ReturnKeysCase(unittest.TestCase):
"""真源返回键齐全性(走夹具函数式路径,断言真值,不接受 print)。"""
def setUp(self):
self.eng = make_engine()
compat.install(self.eng)
self.addCleanup(compat.uninstall)
def _apply(self, **kw):
base = {"tenant_id": 7, "world_id": 1, "session_id": 11,
"event_type": "unit.move", "payload": {"dx": 1}}
base.update(kw)
return compat.apply(**base)
def test_success_return_keys_exact(self):
r = self._apply(state_updates=[su()], client_event_id="ev-1", actor_id="u1")
self.assertEqual(set(r.keys()), FULL_RETURN_KEYS)
self.assertIs(r["ok"], True)
self.assertIs(r["dedup"], False)
self.assertIsNotNone(r["event_id"])
self.assertEqual(r["seq"], 1)
self.assertEqual(r["seq_no"], r["seq"]) # 对外别名一致
self.assertEqual(r["state_rows"], 1)
self.assertEqual(r["channel"], "pbl:rtx:w1:t7")
self.assertIsInstance(r["stmt_count"], int)
self.assertGreaterEqual(r["stmt_count"], 2) # 事件 INSERT + 状态 UPSERT
self.assertIsInstance(r["states"], list)
self.assertEqual(set(r["states"][0].keys()),
{"entity_id", "state_version", "action", "state_key",
"state_value", "seq"})
self.assertEqual(r["states"][0]["action"], "insert")
self.assertEqual(r["states"][0]["state_version"], 1)
def test_dedup_return_keys_exact_and_no_second_write(self):
r1 = self._apply(state_updates=[su()], client_event_id="ev-1")
r2 = self._apply(state_updates=[su(state={"hp": 9})], client_event_id="ev-1")
self.assertEqual(set(r2.keys()), DEDUP_RETURN_KEYS)
self.assertIs(r2["dedup"], True)
self.assertEqual(r2["event_id"], r1["event_id"]) # 复用既有事件,不新写
self.assertEqual(r2["seq"], r1["seq"])
self.assertEqual(r2["state_rows"], 0)
self.assertIsNone(r2["latency_ms"])
self.assertEqual(self.eng.count(EVENT_TABLE), 1)
self.assertEqual(self.eng.count(STATE_TABLE), 1)
self.assertEqual(len(self.eng.broadcasts), 1) # 幂等命中不重复广播
def test_read_states_rows_shape(self):
self._apply(state_updates=[su()], client_event_id="ev-1")
rows = compat.read(tenant_id=7, session_id=11)
self.assertIsInstance(rows, list)
self.assertEqual(len(rows), 1)
row = rows[0]
self.assertEqual(row["entity_id"], "e1")
self.assertEqual(row["state"], {"hp": 2})
self.assertEqual(row["state_version"], 1)
self.assertIn("checksum", row)
self.assertIn("updated_at", row)
def test_snapshot_writes_snapshot_table(self):
r = self._apply(state_updates=[su(state={"hp": 5})], client_event_id="ev-4",
snapshot={"reason": "join", "hp": 5})
self.assertEqual(set(r.keys()), FULL_RETURN_KEYS)
self.assertEqual(self.eng.count(SNAPSHOT_TABLE), 1)
def test_broadcast_strictly_after_commit(self):
self._apply(state_updates=[su()], client_event_id="ev-1")
kinds = compat.trace_kinds(self.eng)
self.assertEqual(kinds[0], "BEGIN")
self.assertIn("COMMIT", kinds)
self.assertIn("BROADCAST", kinds)
self.assertGreater(kinds.index("BROADCAST"), kinds.index("COMMIT"),
"广播必须在事务提交之后(M11b-3 契约)")
self.assertEqual(self.eng.broadcasts[0]["channel"], "pbl:rtx:w1:t7")
self.assertEqual(self.eng.broadcasts[0]["event"]["event_type"], "unit.move")
def test_event_table_is_append_only(self):
"""事件表只允许 INSERT:UPDATE 语句里不得出现事件表名。"""
self._apply(state_updates=[su()], client_event_id="ev-1")
self._apply(state_updates=[su(state={"hp": 3})], client_event_id="ev-2")
for sql, _p in compat.update_statements(self.eng):
self.assertNotIn(EVENT_TABLE.upper(), sql.upper(),
"事件表出现 UPDATE 语句,违反 append-only:%s" % sql)
inserts = compat.insert_statements(self.eng, EVENT_TABLE)
self.assertEqual(len(inserts), 2)
for sql, _p in inserts:
self.assertIn(EVENT_TABLE.upper(), sql.upper())
class GateErrorCase(unittest.TestCase):
"""参数门禁与乐观锁冲突:必须抛 RtxError 且 code 精确、事务回滚不留脏行。"""
def setUp(self):
self.eng = make_engine()
compat.install(self.eng)
self.addCleanup(compat.uninstall)
def _expect(self, code, **kw):
base = {"tenant_id": 7, "world_id": 1, "session_id": 11,
"event_type": "x", "payload": {}}
base.update(kw)
with self.assertRaises(RtxError) as ctx:
compat.apply(**base)
self.assertEqual(ctx.exception.code, code)
return ctx.exception
def test_missing_tenant_is_fail_closed(self):
self._expect(ERR_TENANT_MISSING, tenant_id=None)
self.assertEqual(self.eng.count(EVENT_TABLE), 0)
def test_missing_required_params(self):
for key in ("world_id", "session_id", "event_type"):
self._expect(ERR_BAD_PARAM, **{key: None})
self.assertEqual(self.eng.count(EVENT_TABLE), 0)
def test_bad_state_updates(self):
self._expect(ERR_BAD_PARAM, state_updates="notalist")
self._expect(ERR_BAD_PARAM, state_updates=[{"state": {"a": 1}}]) # 缺 entity_id
self._expect(ERR_BAD_PARAM, state_updates=[{"entity_id": "e1"}]) # 缺 state
self.assertEqual(self.eng.count(STATE_TABLE), 0)
def test_state_conflict_rolls_back(self):
compat.apply(tenant_id=7, world_id=1, session_id=11, event_type="unit.move",
payload={}, state_updates=[su()], client_event_id="ev-1")
before_evt = self.eng.count(EVENT_TABLE)
before_state = self.eng.count(STATE_TABLE)
self._expect(ERR_STATE_CONFLICT, event_type="x",
state_updates=[su(state={"hp": 0})],
client_event_id="ev-3", base_version=999)
kinds = compat.trace_kinds(self.eng)
self.assertEqual(kinds[-1], "ROLLBACK", "冲突分支必须回滚,不得 COMMIT")
self.assertEqual(self.eng.count(EVENT_TABLE), before_evt)
self.assertEqual(self.eng.count(STATE_TABLE), before_state)
if __name__ == "__main__":
unittest.main(verbosity=2)