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

383 lines
18 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 契约守卫用例:函数式夹具契约 + 真源返回键齐全性。
来源(响应 QC #4「探针要么删要么升级」)
==========================================
本文件由一次性调试探针 ``modules/world_sync/.m11b2d_probe.py``(3250B,随引擎代提交
``13386a4`` 入库)改造而来——探针用 ``print`` 输出、无断言、不可重复判定。探针本体已
由 ``git rm`` 从工作区与 git 索引移除,删除提交为 ``81fdb86``(chore(M11b-2d-A3):
移除一次性调试探针),本文件即升级结果。
守卫什么(三件事)
==================
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(...))`` 的同步入口)。分支覆盖
成功写 / 幂等命中 / 乐观锁冲突 / 参数门禁 / 快照 / 提交后广播。
权威契约定义(本文档不搬运说明)
================================
* 夹具层契约:``tests/_m11b2_compat.py`` 顶部 docstring(唯一权威实现说明);
* 登记文档(索引式,含真源逐参数签名与返回键语义):
``docs/work-log-2026-09-22-m11b2d-fixture-contract.md``;
* 真源本体:``modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py``
(``apply_runtime_event`` / ``read_states``)。
本文件只做机械断言,断言值一律**以真源实现为准**(如广播通道名取
``broadcast.channel_of(world_id, tenant_id)`` 的真实产物,不写理想化字面量)。
边界(遵守 QC #3 允许路径与本任务禁改清单)
==========================================
* 不改业务实现、不改 ``pbl_runtime_ext/models/*.json``、不改 ``load_path.py``;
* 不改 ``tests/_m11b2_compat.py`` 任何既有符号(只读引用);
* 不改 ``test_m11b2_single_tx.py`` / ``test_m11b2_wiring.py``(并行任务独占)。
created_at 说明(与 ``test_m11b2_single_tx.py`` 同一口径)
=========================================================
真源事件表两条唯一索引(``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),与并行文件的做法一致,属测试侧用法修正,不改真源与夹具语义。
夹具的 ``_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 threading
import time
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 (导入时已把 modules/pbl_runtime_ext 加入 sys.path)
# 真源本体(被测对象,不是副本)
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
# 广播通道名的真源实现(pbl_runtime_ext/broadcast.py:22 channel_of,前缀 CHANNEL_PREFIX="pbl.world")
from pbl_runtime_ext import broadcast as rtx_broadcast # noqa: E402
TENANT = 7
WORLD = 1
SESSION = 11
# 真源 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/seq_no/broadcast_ms/sla_breach
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
_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 expected_channel(world_id=WORLD, tenant_id=TENANT):
"""通道名以真源实现为准:pbl.world.<tenant>.<world>(租户隔离在通道层生效)。"""
return rtx_broadcast.channel_of(world_id, tenant_id)
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):
"""真源逐参数签名钉死:形参名/顺序/默认值(与登记文档同源)。"""
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": TENANT, "world_id": WORLD, "session_id": SESSION,
"event_type": "unit.move", "payload": {"dx": 1}}
base.update(kw)
if "occurred_at" not in kw:
base["occurred_at"] = stamp()
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)
# 通道名以真源实现为准:pbl.world.<tenant>.<world>
self.assertEqual(r["channel"], expected_channel())
self.assertEqual(r["channel"], "pbl.world.7.1")
self.assertIsInstance(r["stmt_count"], int)
self.assertGreaterEqual(r["stmt_count"], 2) # 事件 INSERT + 状态 UPSERT
self.assertIsInstance(r["latency_ms"], (int, float))
self.assertGreaterEqual(r["latency_ms"], 0)
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):
ts = stamp()
r1 = self._apply(state_updates=[su()], client_event_id="ev-1", occurred_at=ts)
# 幂等命中:同 client_event_id → 同 event_uid,走「已提交事务 + 不重复写」分支
r2 = self._apply(state_updates=[su(state={"hp": 9})], client_event_id="ev-1",
occurred_at=ts)
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)
# 真源语义:幂等命中**仍然计时**(tx_write.py:442 在返回前回填 latency_ms=ms),
# 故断言「是数值且 >= 0」,不断言 None(旧断言与真源实现不符,QC #4)。
self.assertIsInstance(r2["latency_ms"], (int, float))
self.assertGreaterEqual(r2["latency_ms"], 0)
self.assertEqual(r2["channel"], expected_channel())
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=TENANT, session_id=SESSION)
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"], expected_channel())
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": TENANT, "world_id": WORLD, "session_id": SESSION,
"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=TENANT, world_id=WORLD, session_id=SESSION,
event_type="unit.move", payload={}, state_updates=[su()],
client_event_id="ev-1", occurred_at=stamp())
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, occurred_at=stamp())
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)
class RegistrationDocCase(unittest.TestCase):
"""登记文档守卫(响应 QC #2:交付件正文引用的文档必须真实存在,不留悬空引用)。
文档只做索引式登记,权威实现仍在 ``tests/_m11b2_compat.py`` docstring 与真源源码;
本用例钉的是「登记与真源同源」:形参名、返回键、错误码、废弃符号四类清单
必须在文档正文中逐条出现——任一处漏登记即红。
"""
DOC = os.path.join(os.path.dirname(HERE), "docs",
"work-log-2026-09-22-m11b2d-fixture-contract.md")
def _text(self):
self.assertTrue(os.path.isfile(self.DOC),
"登记文档缺失(交付件引用悬空):%s" % self.DOC)
with open(self.DOC, encoding="utf-8") as f:
return f.read()
def test_doc_exists_and_registers_contract(self):
text = self._text()
for p in TRUE_SOURCE_PARAMS:
self.assertIn("`" + p + "`", text, "登记文档缺少真源形参 " + p)
for key in sorted(FULL_RETURN_KEYS):
self.assertIn("`" + key + "`", text, "登记文档缺少返回键 " + key)
for code in ("PBL-TENANT-0001", "PBL-PARAM-0001", "PBL-STATE-CONFLICT",
"PBL-RTX-0001-APPEND", "PBL-BROAD-0001", "PBL-RTX-0002",
"PBL-RTX-0003", "PBL-RTX-0004", "PBL-RTX-0006"):
self.assertIn(code, text, "登记文档缺少错误码 " + code + " 的含义说明")
for dead in DEAD_CLASS_STYLE_SYMBOLS:
self.assertIn(dead, text, "登记文档缺少废弃符号 " + dead + " 与替代说明")
def test_doc_live_fixture_symbols_registered(self):
"""夹具层公开符号(现行唯一路径)必须逐个登记在文档表格里。"""
text = self._text()
for name in ("RuntimeEngine", "install", "uninstall", "apply", "read",
"trace_kinds", "update_statements", "insert_statements"):
self.assertIn("`" + name + "`", text,
"登记文档缺少夹具符号 " + name + "(§1.1 清单)")
if __name__ == "__main__":
unittest.main(verbosity=2)