deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
cc2e1c3184
commit
b1d4490328
@ -163,3 +163,24 @@ RESULT: PASS
|
||||
### 6.5 复杂度评估(本轮)
|
||||
|
||||
**简单**——单交付单元(两份文档正文写作),零代码改动、零表定义改动、无跨模块影响,**无需拆分**。父任务 `[M11b-2c-T2]` 整体为复杂(多独立交付单元 + 串行收口依赖),已由 PM 拆为 T2-a / T2-b / T2-c 三子任务并按批次派发,口径见 §6.1。
|
||||
|
||||
---
|
||||
|
||||
## 7. 收口轮(T2-c)回填笔 · 2026-09-22
|
||||
|
||||
由 `[M11b-2c-T2-c]`(task_id `UFjoRchqgo8asHC3rP5he`)追加。本文自身尺寸不写死(自指竞态),见
|
||||
`projects/pbls/docs/02-develop/m11b2c-t2-code-files-measure.log` `[final]` 段;以下只回填**对方**实测值与**已存在**的真实 hash。
|
||||
|
||||
| 项 | 真值 | 口径 |
|
||||
|---|---|---|
|
||||
| `projects/pbls/docs/02-develop/dev-notes.md` 字节数 | 54239 | `wc -c` T2-c 回填时点实测(对方文件,非自指) |
|
||||
| `kICTWazBqgIWtKNO7sz8A_code_files.md` 字节数 | 20047 | 同上 |
|
||||
| `modules/world_sync/docs/work-log-2026-09-22-m11b2c.md` 自身字节数 | 见 measure.log `[final]` 段 | **自指 → 不写死** |
|
||||
| 本文件 T2-a 选择性提交 hash | `da37362628ad6b51425c3de348ce8a0a0737a66a` | `git log -1 --format='%H %s' -- docs/work-log-2026-09-22-m11b2c.md`(只含本文件一笔,无 wwwroot、无代码) |
|
||||
| 本文件 T2-c 收口笔 hash | 见 measure.log `[git]` 段 | 自指(写进本文即改变本文) |
|
||||
| `read_committed_stmt` 定义行 | `world_sync/pbl_runtime_sql.py:148` | T2-c 重跑 `grep -n '^def read_committed_stmt'` 复核,与 §6.2 同口径 |
|
||||
|
||||
关键词逐词 `grep -c`(本文):`M11b-2` 17 / `expected_version` 4 / `READ COMMITTED` 1 / `P99` 6 / `无新 HTTP 路径` 1。
|
||||
|
||||
§6.4 第一条「hash 由收口轮落 measure.log、本文不写死」的约定仍然成立;本表补的是**上一笔已存在**的 T2-a hash(不随本文变动而失效)。
|
||||
`projects/pbls` 侧:T2-c 已按 PM 指令对该过程仓做选择性提交(只加本任务四个文件),故 §6.4 中「仍为未提交状态」一句已被收口轮更新取代,真实 hash 见 measure.log `[git]` 段。
|
||||
|
||||
285
tests/test_m11b2d_fixture_contract.py
Normal file
285
tests/test_m11b2d_fixture_contract.py
Normal file
@ -0,0 +1,285 @@
|
||||
# -*- 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)
|
||||
Loading…
x
Reference in New Issue
Block a user