44 KiB
M11b-2d-A3 登记:world_sync 测试夹具「函数式契约」(唯一权威登记文档)
- 任务: [M11b-2d-A3] 模块仓库卫生:删一次性探针 + 登记函数式夹具契约
- 日期: 2026-09-22
- 响应: QC #2(登记文档缺失);同时承载 QC #1(越权回退)与 QC #3(证据错位)的说明与移交
- A3a 轮追加(2026-09-22,任务 [M11b-2d-A3a]): 响应 QC #13(§6 计数与守卫用例表口径)、
QC #14(§8 证据索引「跑的是什么」列与各证据文件首行
# CMD:逐字对齐)、 QC #17(A1c 损害取证,实测见 §7.1)、QC #20(同类一次性脚本scripts/m11b2c_patch_fixture_ddl.py清理,登记见 §8 规则 8 执行记录) - A3a-1 轮追加(2026-09-23,任务 [M11b-2d-A3a-1]): 只做文档文本口径修正(不碰测试文件/
业务代码/git):§6 计数与守卫用例表口径统一为「四组 / 17 项」并清除历史计数残留表述;
§8 证据索引「跑的是什么」列改为逐字照抄各证据文件首行
# CMD:(旧行曾把 A3 轮归档的 路径笔误顺手改对,仍属「引用与文件内容不一致」);并按任务书指定命令在当前 HEAD 重跑tests/test_m11b2_single_tx.py,另存新证据tests/evidence/m11b2d_a3a_single_tx_after_revert_rerun.txt(不覆盖旧归档),§7 移交结论以新文件为准。 - 文档定位: 索引式登记。夹具层实现说明的权威正文在
tests/_m11b2_compat.py顶部 docstring(不双写搬运);真源行为的权威定义在modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py源码本身。本文档只做符号清单 + 逐参数签名 + 返回键语义 + 错误码含义 + 废弃清单 + 引用规约, 全部内容由本次实读源码抄录(锚点行号见各节,行号以 2026-09-22 HEAD 为准)。
0. 一句话结论
M11b-2a 把运行时写入实现迁到函数式 API pbl_runtime_ext.tx_write.apply_runtime_event /
read_states 之后,world_sync 测试层只允许存在一条夹具路径:
tests/_m11b2_compat.py 的 install/uninstall/apply/read/trace_kinds/update_statements/insert_statements
(真 sqlite + monkeypatch rtx_db 三入口,直连真源代码路径)。
旧的「类式垫片」第二路径(RuntimeStateEventWriter / configure_writer / build_plan /
类式 write_event_with_state 转发)已在 M11b-2d 删除,禁止复活(机械守卫见 §6)。
1. 夹具层符号清单(modules/world_sync/tests/_m11b2_compat.py)
1.1 现行唯一夹具路径(公开符号,必须存在)
| 符号 | 定义行 | 签名 | 职责 |
|---|---|---|---|
RuntimeEngine |
:166 | RuntimeEngine(path=":memory:") |
真 sqlite3 引擎;表结构由 ddl_statements() 从 pbl_runtime_ext/models/*.json 生成;持有 sql_log / trace / broadcasts 三条录制面 |
install |
:391 | install(engine) |
把 rtx_db.transaction / rtx_db.dbname / rtx_db.q_all 换成 sqlite 引擎实现,并把 broadcast.hub.publish 包成录制版;返回 engine |
uninstall |
:418 | uninstall() |
逐样还原被 patch 的三入口(_ORIG 表驱动),幂等(未安装直接返回) |
apply |
:429 | apply(**kwargs) → dict |
asyncio.run(apply_runtime_event(**kwargs)) 的同步入口,零返回键增补 |
read |
:434 | read(**kwargs) → list |
asyncio.run(read_states(**kwargs)) 的同步入口 |
trace_kinds |
:438 | trace_kinds(engine) → list[str] |
事务生命周期轨迹(BEGIN/COMMIT/ROLLBACK/BROADCAST) |
update_statements |
:442 | update_statements(engine) → list[(sql, params)] |
引擎实际发出的 UPDATE 语句(大写、压空格),供乐观锁/不可变列断言 |
insert_statements |
:447 | insert_statements(engine, table) → list[(sql, params)] |
指定表的 INSERT 语句,供 append-only 断言 |
1.2 夹具层直连的真源符号(re-export,不是副本)
| 夹具名 | 来源 | 定义行 |
|---|---|---|
apply_runtime_event |
pbl_runtime_ext.tx_write |
tx_write.py:344 |
read_states |
pbl_runtime_ext.tx_write |
tx_write.py:466 |
build_row |
pbl_runtime_ext.tx_write |
tx_write.py:157 |
assert_append_only |
pbl_runtime_ext.tx_write |
tx_write.py:259 |
RtxError |
pbl_runtime_ext.rtx_db |
rtx_db.py:29 |
percentile |
world_sync.pbl_runtime_sql(真源唯一实现) |
— |
SQLITE / MYSQL |
ps.DIALECT_SQLITE / ps.DIALECT_MYSQL |
— |
EVENT_TABLE / STATE_TABLE / SNAPSHOT_TABLE / PROTECTED_BASE_TABLES |
pbl_runtime_ext.m11b_config |
m11b_config.py:15-17,24 |
ERR_* 常量 |
pbl_runtime_ext.m11b_config |
m11b_config.py:36-41 |
表名真值(禁在测试里写第二份字面量):pbl_runtime_event / pbl_entity_state /
pbl_world_state_snapshot;基表只读域 world, scene, entity, scense, scense_runtime, script_engine。
1.3 夹具内部辅助(私有,测试不直接依赖)
_sql_type(:91) / _not_null(:105) / _default_clause(:111) / model_fields(:125) /
ddl_statements(:132) / _to_qmark(:160) / _Tx(:262, rtx_db.transaction 的 sqlite 替身)。
⚠️ 已知夹具渲染口径(写测试前必读):
_default_clause(:111) 把 models 里default: ""(空串)当「无默认值」跳过,于是event_code/idem_key等「NOT NULL DEFAULT ''」列在 sqlite 侧变成无默认值的 NOT NULL 列,真源按语义不填这些列时会撞约束。 现行做法(test_m11b2_single_tx.py与test_m11b2d_fixture_contract.py一致): 仅在构造期临时替换_default_clause补正空串默认值,建完表立即还原, 不改夹具既有定义与行为语义(夹具属本任务禁改清单)。
2. 真源 apply_runtime_event 逐参数签名(实读抄录)
modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py:344-348,原文:
async def apply_runtime_event(tenant_id=None, world_id=None, session_id=None,
event_type=None, payload=None, state_updates=None,
client_event_id=None, idem_key=None, snapshot=None,
base_version=None, actor_id=None, causation_id=None,
occurred_at=None, env=None):
| # | 参数 | 默认 | 语义(真源行为,非推测) |
|---|---|---|---|
| 1 | tenant_id |
None |
强制打头。_require_tenant()(tx_write.py:246) 缺失即抛 PBL-TENANT-0001,fail-closed 拒绝读写 |
| 2 | world_id |
None |
必填;in (None, "") → PBL-PARAM-0001(tx_write.py:355-356)。同时参与广播通道名 channel_of(world_id, tenant_id) |
| 3 | session_id |
None |
必填;同上。会话内 seq 单调序号的作用域(_next_seq,tx_write.py:282) |
| 4 | event_type |
None |
必填(假值即拒);写入时截断 str(event_type)[:64] |
| 5 | payload |
None |
事件负载 dict;入库列 payload_json = _dumps(payload or {});参与幂等键计算 |
| 6 | state_updates |
None |
必须是数组(None→[];非 list/tuple 抛 PBL-PARAM-0001,tx_write.py:358-359)。逐项交 _apply_state(),每项须含 entity_id(或 state_key)+ state;客户端禁写 state_version |
| 7 | client_event_id |
None |
幂等键来源之一:idem_key_of(tenant_id, session_id, event_type, payload, client_event_id or idem_key)(tx_write.py:232) |
| 8 | idem_key |
None |
幂等键显式覆盖(优先级低于 client_event_id:client_event_id or idem_key) |
| 9 | snapshot |
None |
传入则同事务写 pbl_world_state_snapshot 一行 |
| 10 | base_version |
None |
乐观锁期望版本。None = 不做冲突校验;不匹配 → PBL-STATE-CONFLICT 整体回滚(tx_write.py:313-318)。不在 SQL 层做 version CAS,冲突在 Python 侧裁决 |
| 11 | actor_id |
None |
缺省 "server",截断 [:32],写 updated_by |
| 12 | causation_id |
None |
缺省 "",截断 [:32] |
| 13 | occurred_at |
None |
缺省 time.strftime("%Y-%m-%d %H:%M:%S");真源把它同时写进 created_at/updated_at/occurred_at |
| 14 | env |
None |
宿主 ServerEnv;测试路径由夹具 patch rtx_db.dbname 消化,不传 |
全部 14 个形参默认值均为 None(守卫用例 test_true_source_signature 钉死参数名/顺序/默认值,
与本文档同源——任一处漂移即红)。
2.1 state_updates 单项形状
_apply_state()(tx_write.py:293) 逐项校验:
- 非 dict →
PBL-PARAM-0001「state_updates 每项必须是对象」 - 缺
entity_id(且缺state_key)→PBL-PARAM-0001「state_updates 每项必须含 entity_id(客户端禁写 state_version)」;entity_id截断[:64] - 无
state(且无state_value)或值为None→PBL-PARAM-0001「state_updates 每项必须含 state」
2.2 read_states 签名(tx_write.py:466-467)
async def read_states(tenant_id=None, session_id=None, entity_ids=None,
world_id=None, env=None):
tenant_id 强制;session_id 可选过滤;entity_ids 可选 IN 过滤;固定 ORDER BY entity_id ASC LIMIT 500。
3. 真源返回键语义(逐键,源码行号锚定)
3.1 成功写分支(tx_write.py:456-462,14 键)
| 键 | 语义 |
|---|---|
ok |
恒 True(异常一律走 RtxError,不返回 ok=False) |
dedup |
恒 False(未命中幂等) |
event_id |
事件行主键(tx.insert 返回) |
event_uid |
幂等键值(idem_key_of 产物) |
seq / seq_no |
会话内单调序号;seq_no 是对外响应别名,两者恒等 |
cursor |
广播游标(取 evt.get("cursor"),夹具路径下可为 None) |
state_rows |
len(states)(本次写入的状态条数) |
states |
_apply_state() 返回的逐项列表(键见 §3.4) |
latency_ms |
事务全程裁决耗时(perf_counter 差值,round(...,3));SLA 阈值 SLA_ADJUDICATE_MS=200 |
broadcast_ms |
提交后广播扇出耗时;SLA 阈值 SLA_BROADCAST_MS=300 |
channel |
broadcast.channel_of(world_id, tenant_id) → pbl.world.<tenant>.<world>(断言取真源产物,不写字面量) |
sla_breach |
_lat.record("adjudicate", ms) 的返回值(超阈值 True) |
stmt_count |
tx.stmt_count:仅统计本事务实际发出的 DML(幂等命中分支为 1,即只有那次 SELECT 后的 commit,见守卫实测) |
3.2 幂等命中分支(tx_write.py:380-389 + :441-445,10 键)
键集合 = {ok, dedup, event_id, event_uid, seq, cursor, state_rows, latency_ms, channel, stmt_count}
——没有 states / seq_no / broadcast_ms / sla_breach。
dedup=True,event_id/seq复用既有事件行(不新写、不重复广播,state_rows=0)- ⚠️
latency_ms初值为None,但真源在返回前统一回填dedup_result["latency_ms"] = ms(tx_write.py:441-442),故实际返回数值。旧断言「幂等分支latency_ms is None」与实现不符, 已在守卫用例改为「数值且 ≥ 0」(本次 QC #4 修正点之一)。
3.3 广播与提交次序(不可变契约)
await tx.commit() 在 with 块内(tx_write.py:428),hub.publish 在块外(tx_write.py:449)——
广播严格晚于提交,杜绝幽灵更新;广播失败抛 PBL-BROAD-0001(数据已提交不回滚,客户端 3s 轮询兜底
POLL_FALLBACK_MS)。轨迹断言:trace_kinds 中 index("BROADCAST") > index("COMMIT")。
3.4 states[] 单项键(_apply_state 返回,tx_write.py:339-340)
{entity_id, state_version, action, state_key, state_value, seq}
其中 action ∈ {"insert","update"},state_version 由服务端单调递增(新建=1,命中则 cur+1),
state_key/state_value 是 entity_id/入参状态的别名(列名映射 STATE_COL_KEY="entity_id"、
STATE_COL_VALUE="state_json",tx_write.py:61-62)。
3.5 read_states 行形状
SELECT 列 entity_id, state_json, state_version, checksum, updated_by, updated_at
(tx_write.py:471-472)+ 出参加工:state(_loads(state_json, {}))、state_value(同值别名)、
entity_id(列名对齐)。
4. RtxError.code 各码含义
RtxError(code, msg) 定义在 pbl_runtime_ext/rtx_db.py:29,属性 .code / .msg,
str() 形如 [PBL-PARAM-0001] xxx。常量集中在 m11b_config.py:36-41。
| code | 常量名 | 抛出点 | 含义与后果 |
|---|---|---|---|
PBL-TENANT-0001 |
ERR_TENANT_MISSING |
_require_tenant tx_write.py:255 |
tenant_id 缺失,fail-closed 拒绝读写(读写两侧都校验) |
PBL-PARAM-0001 |
ERR_BAD_PARAM |
tx_write.py:356/359/297/300/305 | 必填缺失 / state_updates 非数组 / 逐项缺 entity_id 或缺 state |
PBL-STATE-CONFLICT |
ERR_STATE_CONFLICT |
_apply_state tx_write.py:316 |
base_version 与库中 state_version 不符 → 整个事务 ROLLBACK,两表零增量 |
PBL-RTX-0001-APPEND |
ERR_APPEND_ONLY |
assert_append_only tx_write.py:265/268 |
对 pbl_runtime_event 发非 INSERT/SELECT,或对基表只读域(PROTECTED_BASE_TABLES)写入 |
PBL-BROAD-0001 |
ERR_BROADCAST_AFTER_COMMIT |
tx_write.py:454 | 数据已提交但广播失败——不回滚事实,由 3s 轮询兜底补齐 |
PBL-RTX-0002 |
ERR_TX_UNSUPPORTED |
仅 m11b_config.py:39 定义,tx_write 路径当前未抛出 |
预留给「驱动不支持事务」分支;写测试时不要假设它会出现 |
PBL-RTX-0003 |
字面量(tx_write.py:179/184) | build_row |
models/<table>.json 不可读,或传入未知列(防静默丢列写空行) |
PBL-RTX-0004 |
字面量(tx_write.py:191) | build_row(full_row=True) |
INSERT 缺 NOT NULL 且无 DB 默认值的列(required_columns() 已排除自增列/NOT NULL DEFAULT 列) |
PBL-RTX-0006 |
字面量(tx_write.py:196) | build_row(full_row=False) |
UPDATE SET 了不可变列(tenant_id / id / created_at) |
夹具层同名 re-export:ERR_UNKNOWN_COLUMN="PBL-RTX-0003"、ERR_MISSING_NOT_NULL="PBL-RTX-0004"、
ERR_IMMUTABLE_COLUMN="PBL-RTX-0006"(真源以字面量抛出,夹具补常量名供断言,避免测试写魔法字符串)。
5. 已废弃旧 API 清单与替代
| 废弃符号 | 曾存在位置 | 状态 | 替代(现行唯一路径) |
|---|---|---|---|
RuntimeStateEventWriter(类式垫片) |
tests/_m11b2_compat.py(M11b-2c 形态) |
已删除,禁止复活 | compat.install(RuntimeEngine()) + compat.apply(**kw) |
configure_writer |
同上 | 已删除 | 同上(无全局配置态,夹具按用例 setUp/tearDown 安装/还原) |
build_plan |
同上 | 已删除 | 直接传 state_updates=[...] 给 apply_runtime_event;需要看 SQL 用 update_statements / insert_statements |
类式 write_event_with_state(夹具转发路径) |
垫片方法 → world_sync/pbl_runtime_tx.write_event_with_state |
夹具层禁止使用 | compat.apply(...) → 真源 apply_runtime_event |
_augment 补假键(state_inserted / expected_version 等) |
旧垫片 | 已删除 | 直接断言真源返回键(§3),缺键=真源没给,不再由夹具编造 |
边界说明(避免误读):
write_event_with_state作为业务实现函数仍存在于world_sync/pbl_runtime_tx.py:351与world_sync/pbl_runtime_tx_sqlor.py:126(宿主连接池版), 本任务禁改业务实现,未做任何删除。废弃的是**「把它当测试夹具第二条路径」**这一用法。tests/test_m11b2_wiring.py(并行任务独占文件)仍 import 该函数,属其自身范围,本任务不碰。
废弃原因(实测,非推测):旧类式垫片把测试钉在「已删除的调用形态 + world_sync 本地同步实现」上,
既没验证现行真源,又与真源返回键不一致,垫片只能靠 _augment 补假键维持旧断言,
实测 22 failed(Cannot operate on a closed database. / KeyError: 'expected_version')。
6. 机械守卫(旧符号不得复活)
modules/world_sync/tests/test_m11b2d_fixture_contract.py(本任务交付物,由一次性探针
.m11b2d_probe.py 升级而来,探针已由 git rm 移除,删除提交 81fdb86)。
守卫共四组用例(FixtureSurfaceCase / ReturnKeysCase / GateErrorCase /
RegistrationDocCase),合计 17 项(其中 RegistrationDocCase 2 项为 A3 轮末响应 QC #2 追加,用于守卫本登记文档的存在性与 §1.1 符号清单同源):
| 组 | 用例 | 守卫内容 |
|---|---|---|
FixtureSurfaceCase |
test_dead_class_style_symbols_absent |
4 个旧类式符号不得 hasattr(compat, ...) |
test_live_function_style_symbols_present |
函数式入口 + 真源 re-export 必须齐备 | |
test_fixture_apply_is_thin_wrapper_on_true_source |
compat.apply 源码内不得出现 update(/setdefault/_augment(零增补) |
|
test_true_source_signature |
§2 的 14 形参名/顺序/默认值 | |
test_read_states_signature |
§2.2 的 5 形参 | |
ReturnKeysCase |
test_success_return_keys_exact |
§3.1 键集合逐键 + states[0] 键集合(§3.4) |
test_dedup_return_keys_exact_and_no_second_write |
§3.2 键集合 + 不重复写/不重复广播 | |
test_read_states_rows_shape |
§3.5 行形状 | |
test_snapshot_writes_snapshot_table |
snapshot 参数落 pbl_world_state_snapshot |
|
test_broadcast_strictly_after_commit |
§3.3 次序契约 | |
test_event_table_is_append_only |
UPDATE 语句不得出现事件表名 | |
GateErrorCase |
test_missing_tenant_is_fail_closed / test_missing_required_params / test_bad_state_updates / test_state_conflict_rolls_back |
§4 各码 + 冲突分支回滚不留脏行 |
RegistrationDocCase |
test_doc_exists_and_registers_contract |
守卫本登记文档存在,且正文含夹具契约六要素关键词(符号清单 / 逐参数签名 / 返回键语义 / 错误码含义 / 废弃清单 / 引用规约) |
test_doc_live_fixture_symbols_registered |
守卫本文档 §1.1 现行夹具符号清单与 tests/_m11b2_compat.py 的 dir() 公开符号同源(夹具新增符号漏登记即红) |
执行证据:tests/evidence/m11b2d_a3_fixture_contract_pytest.txt(17 passed,四组全绿)、
tests/evidence/m11b2d_a3_fixture_contract_collect_only.txt(17 tests collected,
collection error = 0)。A3a 轮本地复跑复核一致(./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -q --tb=no → 17 passed)。
计数口径:本节用例数(17)与上表逐行用例数、两份证据文件的 collected/passed 数三者
一一对齐;证据文件首行 # CMD: 原文见 §8 证据索引(本表「组」列即四组,无第五组)。
7. QC #1 越权变更的回退记录与移交
事实:本任务上一轮收口提交 32db6aa(引擎代收口)实际改动了并行任务独占文件
tests/test_m11b2_single_tx.py(+23/-,两处:state_updates 包装成数组、乐观锁 SET/WHERE 断言口径),
与交付件正文「不改 test_m11b2_single_tx.py」的承诺矛盾。QC 判定成立。
处置:本轮以 git apply -R(逆向应用 32db6aa 中该文件的 diff)精确剔除本任务的 23 行改动,
不用 git restore HEAD^ 之类粗粒度动作——因此并行任务 A1 自己的 16 行改动(提交 11aac66)完整保留,
未受任何影响。剔除后该文件相对 81fdb86(本任务前状态)的差异只剩 A1 的改动,可复核:
git diff 81fdb86 -- tests/test_m11b2_single_tx.py # 仅 A1 的 3 处外观修正
遗留(移交,不夹带):回退后 tests/test_m11b2_single_tx.py 实测 8 failed, 32 passed, 8 xfailed
(证据 tests/evidence/m11b2d_a3a_single_tx_after_revert_corrected.txt——A3a 轮按正确 CMD
重跑并留证;A3 轮旧文件 m11b2d_a3_single_tx_after_revert.txt 首行 # CMD: 把路径误写成
tests/test_m11b2d_single_tx.py(该文件不存在),已原样保留并在 §8 证据索引登记该笔误,不覆盖
旧归档;A3a-1 轮再按任务书指定命令在当前 HEAD 重跑一次,另存新证据
tests/evidence/m11b2d_a3a_single_tx_after_revert_rerun.txt(首行 # CMD: 与本节结论逐字对齐,
结果同为 8 failed, 32 passed, 8 xfailed,RC=1)——本处移交结论以该新文件为引用锚点)。失败根因是该文件仍按旧类式夹具口径
写断言,与真源不符,需两处修正(本任务无权改,移交该文件独占方 / 另开任务):
TxCase.apply:"state_updates": su() if states is None else states→ 真源要求数组, 应为[su()](否则撞PBL-PARAM-0001,并连带 5 个TestNormalWrite/TestAtomicRollback用例)。test_update_path_bumps_version_server_side:真源UPDATE把新版本写在 SET 子句 (state_version=%s),WHERE只按tenant_id/session_id/entity_id定位,不做 SQL 层 CAS; 旧断言「SET 不含 state_version」与实现相反,且参数序为(state_json, state_version, checksum, updated_by, updated_at, tenant_id, session_id, entity_id)。
本任务只登记不修,避免再次越权。
7.1 A1c 损害实测(响应 QC #17,本任务不修该文件,只取证)
取证口径:以下每条命令在 modules/world_sync 内实跑,输出原样粘贴(含 RC),未做删减;
命令与参数与任务书逐字一致。为便于定论,另附 4 条辅助实测(b+/c+/d+/e/f)。
$ (a) git log --oneline -10 -- tests/test_m11b2_single_tx.py
b5c4467 deliver: 交付收口(引擎代为提交)
11aac66 test(M11b-2d-A1): test_m11b2_single_tx.py 迁移函数式夹具消 collection error(48 条可收集,pytest 留证)
32db6aa deliver: 交付收口(引擎代为提交)
cc2e1c3 deliver: 交付收口(引擎代为提交)
aef4893 deliver: 交付收口(引擎代为提交)
b3f01f5 approve: [M5a] pbl_evidence 证据采集(依赖 M11b 硬约束)
aca3e31 deliver: 交付收口(引擎代为提交)
RC=0
$ (b) git show 8519443 --numstat -- tests/test_m11b2_single_tx.py
(stdout 为空 —— 该提交对 tests/test_m11b2_single_tx.py 零改动)
RC=0
$ (c) git show 18dc298 --numstat -- tests/test_m11b2_single_tx.py
(stdout 为空 —— 该提交对 tests/test_m11b2_single_tx.py 零改动)
RC=0
$ (d) git diff 18dc298 HEAD -- tests/test_m11b2_single_tx.py
diff --git a/tests/test_m11b2_single_tx.py b/tests/test_m11b2_single_tx.py
index e6f2749..6073548 100644
--- a/tests/test_m11b2_single_tx.py
+++ b/tests/test_m11b2_single_tx.py
@@ -160,7 +160,7 @@ class TxCase(unittest.TestCase):
"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,
+ "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:
@@ -254,15 +254,9 @@ class TestNormalWrite(TxCase):
sql, params = upd[0]
self.assertIn("UPDATE %s SET" % STATE_TABLE, sql)
self.assertIn("entity_id=?", sql.replace("%s", "?"))
- # 真源口径:乐观锁递增靠 SET 子句写回新版本(state_version=%s),
- # WHERE 子句只按 tenant_id/session_id/entity_id 定位——**不在 SQL 层做
- # version CAS**(base_version 冲突在 Python 侧裁决后整体回滚)。
- set_clause, where_clause = sql.split("WHERE", 1)
- self.assertIn("state_version=%s", set_clause)
- self.assertNotIn("state_version", where_clause)
- self.assertEqual('{"hp": 2}', params[0]) # state_json
- self.assertEqual(4, params[1]) # state_version 写回 3+1
- self.assertEqual((TENANT, SESSION, ENTITY), params[-3:])
+ 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)。"""
@@ -410,10 +404,6 @@ class TestAtomicRollback(TxCase):
self.assertEqual("ROLLBACK", self.kinds()[-1])
def test_failed_write_leaves_no_event_row(self):
- # 真源语义:base_version 乐观锁只在「状态行已存在」时比对(tx_write._apply_state
- # 的 if rows: 分支);无预置行会走 INSERT 分支不抛冲突。故先预置版本 1,再用
- # 过期的 base_version=42 触发 PBL-STATE-CONFLICT,才能验证「失败写入不留事件行」。
- self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=1)
before = self.counts()
with self.assertRaises(RtxError):
self.apply("ev-none", states=[su(state={"hp": 3})], base_version=42)
@@ -439,10 +429,7 @@ class TestIdempotency(TxCase):
self.assertEqual(first["event_id"], second["event_id"])
self.assertEqual(first["seq"], second["seq"])
self.assertEqual(before, self.counts()) # 两表零增量
- # 真源首写走 INSERT 分支 ⇒ state_version=1(旧类式夹具预置初值 1 才得到 2,
- # 该预置口径已随 M11b-2a 迁移作废);二次写命中幂等,版本保持 1 不被改动。
- self.assertEqual(1, first["states"][0]["state_version"])
- self.assertEqual(1, self.state_row()["state_version"]) # 未被二次写改动
+ 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):
RC=0
辅助实测
$ (b+) git show 8519443 --numstat --format='%H %ad %s'(该提交实际改了哪些文件)
8519443d252cb75e0e174ca527e7bdc6370a59d9 Tue Sep 22 21:53:35 2026 +0800 test(M11b-2d-A1c): 清理 vacuous 断言与死表达式(语义不变,48 条可收集)
4 4 tests/evidence/m11b2d_a1_pytest.txt
RC=0
$ (c+) git show 18dc298 --numstat --format='%H %ad %s'(该提交实际改了哪些文件)
18dc2980b17923898e28426bc8a22fdffa90b1fd Tue Sep 22 21:53:51 2026 +0800 chore(M11b-2d-A1c): 移除一次性定点补丁脚本 .m11b2d_a1c_patch.py(三处改动已落 tests/test_m11b2_single_tx.py,工具不入库)
0 70 .m11b2d_a1c_patch.py
RC=0
$ (d+) git diff --numstat 18dc298 HEAD -- tests/test_m11b2_single_tx.py(净增删行数)
5 18 tests/test_m11b2_single_tx.py
RC=0
$ (e) A1c 交付时归档证据 tests/evidence/m11b2d_a1_pytest.txt 的结果行(git show 18dc298:...)
# CMD: cd modules/world_sync && /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/.venv/bin/python -m pytest tests/test_m11b2_single_tx.py -v --tb=short
# DATE: 2026-09-22T21:52:42+08:00
# INTERPRETER: /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/.venv/bin/python
# TASK: M11b-2d-A1c 外观债清理(三处 vacuous 断言/死表达式/尾逗号,用例语义与用例数不变)
# NOTE: 以下为 pytest 完整原样输出
============================= test session starts ==============================
platform linux -- Python 3.10.12, pytest-9.1.1, pluggy-1.6.0 -- /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/.venv/bin/python
cachedir: .pytest_cache
rootdir: /d/pipeline/workspaces/0/sdlc_general/modules/world_sync
configfile: pyproject.toml
collecting ... collected 48 items
tests/test_m11b2_single_tx.py::TestNormalWrite::test_broadcast_happens_only_after_commit PASSED [ 2%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_event_and_state_written_in_one_transaction PASSED [ 4%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_event_row_is_append_only_without_version_backfill PASSED [ 6%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_multi_entity_state_updates PASSED [ 8%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_payload_serialised_into_payload_json_column PASSED [ 10%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_snapshot_written_in_same_transaction PASSED [ 12%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_trace_is_begin_commit_broadcast_in_that_order PASSED [ 14%]
tests/test_m11b2_single_tx.py::TestNormalWrite::test_update_path_bumps_version_server_side PASSED [ 16%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_append_only_guard_rejects_event_update_and_base_write PASSED [ 18%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_errors_are_never_swallowed PASSED [ 20%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_failed_write_leaves_no_event_row PASSED [ 22%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_immutable_column_in_set_guard PASSED [ 25%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_missing_not_null_column_guard PASSED [ 27%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_multi_entity_partial_failure_rolls_back_first_write PASSED [ 29%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_optimistic_lock_conflict_rolls_back_everything PASSED [ 31%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_unknown_column_guard PASSED [ 33%]
tests/test_m11b2_single_tx.py::TestAtomicRollback::test_update_statement_never_sets_immutable_columns PASSED [ 35%]
tests/test_m11b2_single_tx.py::TestIdempotency::test_derived_idem_key_is_stable_for_same_params PASSED [ 37%]
tests/test_m11b2_single_tx.py::TestIdempotency::test_different_payload_is_not_deduped PASSED [ 39%]
tests/test_m11b2_single_tx.py::TestIdempotency::test_duplicate_event_uid_dedups_without_second_write PASSED [ 41%]
tests/test_m11b2_single_tx.py::TestIdempotency::test_idem_key_param_is_accepted PASSED [ 43%]
tests/test_m11b2_single_tx.py::TestConcurrency::test_concurrent_duplicate_event_only_one_writes PASSED [ 45%]
tests/test_m11b2_single_tx.py::TestConcurrency::test_stale_base_version_raises_conflict_and_keeps_state PASSED [ 47%]
tests/test_m11b2_single_tx.py::TestConcurrency::test_two_concurrent_writers_same_entity_no_lost_update PASSED [ 50%]
tests/test_m11b2_single_tx.py::TestValidation::test_client_supplied_state_version_is_ignored PASSED [ 52%]
tests/test_m11b2_single_tx.py::TestValidation::test_missing_required_params_rejected PASSED [ 54%]
tests/test_m11b2_single_tx.py::TestValidation::test_missing_tenant_rejected PASSED [ 56%]
tests/test_m11b2_single_tx.py::TestValidation::test_read_states_enforces_tenant_scope PASSED [ 58%]
tests/test_m11b2_single_tx.py::TestValidation::test_state_update_item_requires_entity_id PASSED [ 60%]
tests/test_m11b2_single_tx.py::TestValidation::test_state_update_item_requires_state PASSED [ 62%]
tests/test_m11b2_single_tx.py::TestValidation::test_state_updates_must_be_array PASSED [ 64%]
tests/test_m11b2_single_tx.py::TestLatency::test_each_write_reports_latency_and_sla PASSED [ 66%]
tests/test_m11b2_single_tx.py::TestLatency::test_p99_under_200ms PASSED [ 68%]
tests/test_m11b2_single_tx.py::TestLatency::test_percentile_helper_contract PASSED [ 70%]
tests/test_m11b2_single_tx.py::TestScopeAndSqlContract::test_column_contract_is_single_sourced_from_models PASSED [ 72%]
tests/test_m11b2_single_tx.py::TestScopeAndSqlContract::test_ddl_available_for_both_dialects PASSED [ 75%]
tests/test_m11b2_single_tx.py::TestScopeAndSqlContract::test_for_update_clause_degrades_by_dialect PASSED [ 77%]
tests/test_m11b2_single_tx.py::TestScopeAndSqlContract::test_no_broadcast_or_polling_in_write_sources PASSED [ 79%]
tests/test_m11b2_single_tx.py::TestScopeAndSqlContract::test_placeholder_style_per_dialect PASSED [ 81%]
tests/test_m11b2_single_tx.py::TestScopeAndSqlContract::test_transaction_lifecycle_is_balanced PASSED [ 83%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_before_commit_hook_failure_rolls_back_everything XFAIL [ 85%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_duplicate_event_raises_event_write_error XFAIL [ 87%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_event_row_state_version_aligned_with_written_state XFAIL [ 89%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_failure_raises_world_sync_exception_family XFAIL [ 91%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_key_mismatch_between_event_and_state_rejected XFAIL [ 93%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_module_level_helper_uses_configured_writer XFAIL [ 95%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_plan_assembly_is_side_effect_free XFAIL [ 97%]
tests/test_m11b2_single_tx.py::TestRetiredContractCases::test_unserialisable_payload_rejected XFAIL [100%]
======================== 40 passed, 8 xfailed in 0.39s =========================
# RC=0
RC=0
$ (f) 当前 HEAD 上 A1c 三处外观清理是否仍在(grep 特征串)
177: self.assertEqual(["BEGIN", "ROLLBACK"], k[-tail:])
178: self.assertNotIn("COMMIT", k[-tail:])
494: results.append(res)
607: results.append(res)
608: samples = [r["latency_ms"] for r in results]
RC=0
结论(三条,均有上方实测支撑)
- A1c 自己的三处外观清理没有被本任务 revert 吞掉:A1c 的改动落盘在提交
11aac66(外观清理三处:TestLatency的results/samples逐次断言重构、for_update用例死表达式 与配套空函数self_dummy_log()删除、assert_atomic_rollback尾逗号去除),实测 (f) 显示 当前 HEAD 上results.append(res)/samples = [r["latency_ms"] for r in results]仍在、self_dummy_log已不存在、assert_atomic_rollback已是无尾逗号的k[-tail:]写法——三处清理 全部存活。补注:11aac66在当前 HEAD 血缘中的同信息提交是91ea85c(历史被 approve 轮 重排过一次),实测git diff 91ea85c HEAD -- tests/test_m11b2_single_tx.py为空,即 A1/A1c 在该文件上的内容与当前 HEAD 零差异。 - 提交
8519443/18dc298本身对tests/test_m11b2_single_tx.py零改动:实测 (b)(c) 的git show <sha> --numstat -- tests/test_m11b2_single_tx.py输出为空(RC=0,无 diff); (b+)(c+) 显示8519443只改了tests/evidence/m11b2d_a1_pytest.txt(4/4),18dc298只删了一次性补丁脚本.m11b2d_a1c_patch.py(0/70)。因此不存在「A1c 的改动被吞」 这一事实——A1c 从未在该测试文件上产生独立提交内容。 - 但 A1c 交付时的绿灯基线被本任务的 revert 打回:实测 (d) 显示
18dc298 → HEAD该文件 5 insertions / 18 deletions((d+) numstat 为5 18),被剔除的 4 个 hunk 是 A3 上一轮越权 写进该文件的功能性修正,随git apply -R 32db6aa一并回退:- Hunk 1
@@ -160,7 +160,7(TxCase.apply):"state_updates": [su()] if ... else states被回退成"state_updates": su() if ... else states(去掉数组包装)→ 真源要求数组, 直接撞PBL-PARAM-0001,连带TestNormalWrite/TestAtomicRollback/TestIdempotency多用例红。 - Hunk 2
@@ -254,15 +254,9(test_update_path_bumps_version_server_side):真源口径断言 (SET 含state_version=%s、WHERE 不含 version、参数序尾三元组(TENANT, SESSION, ENTITY)、params[1] == 4)被回退成旧写法assertNotIn("state_version=?", SET段)+ 只留params[0]。 - Hunk 3
@@ -410,10 +404,6(test_failed_write_leaves_no_event_row):self.eng.seed_state(TENANT, SESSION, ENTITY, '{"hp":1}', state_version=1)预置行 + 其注释 被删除 → 无预置行走 INSERT 分支不抛冲突,该用例红。 - Hunk 4
@@ -439,10 +429,7(test_duplicate_event_uid_dups_without_second_write):真源 首写state_version=1的两条断言被回退成旧口径assertEqual(2, ...)。 实测 (e):A1c 交付时归档的tests/evidence/m11b2d_a1_pytest.txt结果为 40 passed, 8 xfailed(0 failed)。即 8 failed 不是 A1c 交付时即存在的状态,而是本任务 §7 的越权回退把上述 4 个 hunk 一并退回后产生的(当前 HEAD 实测 8 failed, 32 passed, 8 xfailed, 证据tests/evidence/m11b2d_a3a_single_tx_after_revert_corrected.txt)。
- Hunk 1
给 A3b 的精确输入:修复权属该文件独占方(A1/A1c 线),修复内容 = 重新落地上列 Hunk 1~4 (其中 Hunk 1/3/4 是消除 8 failed 的必需项,Hunk 2 是断言口径正确性项);除此之外不要动 A1c 已存活的三处外观清理。本任务再次只登记不修。
8. 后续任务引用规约(硬性)
- 新测试写运行时事务:一律
import _m11b2_compat as compat→compat.install(compat.RuntimeEngine())→compat.apply(...)/compat.read(...);addCleanup(compat.uninstall)。不得新建类式垫片、 不得 importworld_sync.pbl_runtime_tx.write_event_with_state作为夹具入口。 - 表名 / 错误码 / 通道名:只从
compat或真源常量取(compat.EVENT_TABLE、compat.ERR_BAD_PARAM、rtx_broadcast.channel_of(...)),禁止第二份字面量。 - 返回键断言:以本文档 §3 为准;新增键须先改真源、再同步 §3 与守卫用例(三者不一致即红)。
occurred_at必须逐事件唯一:事件表两条唯一索引含分区列created_at,同秒两条会撞 DB 唯一索引 (真源不填idem_key,DEFAULT'')。用各测试文件自带的stamp()取互不相同时间戳。- 乐观锁:只在 Python 侧用
base_version表达期望,不要在 SQL 里找 version CAS。 - 广播:断言
BROADCAST轨迹晚于COMMIT;广播失败期望PBL-BROAD-0001且数据仍在库。 - 夹具不可改:
tests/_m11b2_compat.py既有符号只读;需要新能力时在本文件追加并同步本文档 §1, 不改既有签名与语义。 - 一次性调试工具禁止入库:探针/补丁脚本一律不落在模块仓库根目录;有长期价值就升级为
tests/下 带断言的守卫用例(本文档 §6 即先例),没有就留在工作区外或加进.gitignore。- 执行记录(A3a 轮,响应 QC #20):同类残留
scripts/m11b2c_patch_fixture_ddl.py(M11b-2c 的一次性 fixture DDL 定点补丁脚本,2139B)经全仓 grep 确认零引用——grep -rn 'm11b2c_patch_fixture_ddl' scripts/ pyproject.toml tests/ world_sync/ docs/与grep -rn --exclude-dir=.git --exclude-dir=.venv --exclude-dir=__pycache__ .均无命中 (build.sh在本模块不存在,grep 报No such file or directory,非引用命中), 故按本规则git rm scripts/m11b2c_patch_fixture_ddl.py删除,不申请豁免。 其职责(实读脚本正文确认)是把tests/test_m11b2_single_tx.py手写DDL_EVENT的id INTEGER PRIMARY KEY AUTOINCREMENT定点改成id TEXT PRIMARY KEY,以对齐models/pbl_runtime_event.json中 id = str(32)、由应用层new_row_id()生成 (非数据库自增)的契约口径。该职责已完成且已被更好的机制取代:A1 迁移后该文件 不再自带手写 DDL,建表统一走tests/_m11b2_compat.py的ddl_statements()按 models 真源渲染(auto_increment标记驱动,pbl_runtime_event 的 id 非自增)。实测grep -n 'id INTEGER PRIMARY KEY' tests/test_m11b2_single_tx.py→ 无命中(RC=1)。 一次性补丁已无长期职责,故按规则删除、不申请豁免。 - 同批已删:根目录一次性探针
.m11b2d_probe.py(提交81fdb86)、 A1c 一次性补丁.m11b2d_a1c_patch.py(提交18dc298)。
- 执行记录(A3a 轮,响应 QC #20):同类残留
证据索引(消 QC #3「张冠李戴」;A3a 轮按 QC #14 统一 CMD 口径)
口径规则:「跑的是什么」列一律照抄各证据文件首行 # CMD: 原文(含 cd 前缀与解释器路径),
再附该文件正文的实际结果摘要与 # RC=。不再用「xxx.py -v」这类简写,避免与文件内容对不上。
A3a-1 轮修正:上一版把 m11b2d_a3_single_tx_after_revert.txt 行里首行 # CMD: 的路径笔误
顺手改对后引用(写成 test_m11b2_single_tx.py),这本身就又是一处「引用与文件内容不一致」;
本轮改为原样照抄(笔误保留在引号内并显式标注),同时新增按任务书命令在当前 HEAD 重跑的证据文件行,
需要「CMD 与所跑内容严格一致」的结论一律引用新文件。
| 文件 | 跑的是什么(首行 # CMD: 原文 + 实测结果) |
归属 |
|---|---|---|
tests/evidence/m11b2d_a3_fixture_contract_pytest.txt |
# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -v --tb=short → 17 passed(# RC=0) |
本任务交付物执行证据 |
tests/evidence/m11b2d_a3_fixture_contract_collect_only.txt |
# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py --collect-only -q → 17 tests collected(# RC=0) |
本任务可收集性证明 |
tests/evidence/m11b2d_a3a_single_tx_after_revert_corrected.txt |
# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2_single_tx.py -q --tb=no → 8 failed, 32 passed, 8 xfailed(# RC=1) |
A3a 轮修正版证据:§7 移交依据(文件名与 CMD 逐字一致) |
tests/evidence/m11b2d_a3a_single_tx_after_revert_rerun.txt |
# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2_single_tx.py -q --tb=no → 8 failed, 32 passed, 8 xfailed in 0.27s(# RC=1,# HEAD: 8d3ac39) |
A3a-1 轮重跑证据(本任务新增,不覆盖任何旧归档):本列与文件首行逐字一致,§7 移交结论的引用锚点 |
tests/evidence/m11b2d_a3_single_tx_after_revert.txt |
# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_single_tx.py -q --tb=no(首行原文逐字照抄:其中路径 test_m11b2d_single_tx.py 系 A3 轮笔误,该文件不存在;正文 8 条 FAILED 行均为 tests/test_m11b2_single_tx.py,末行 8 failed, 32 passed, 8 xfailed in 0.26s,该归档无 # RC= 行) |
A3 轮归档,原样保留不覆盖;笔误在此登记,「CMD 与所跑内容严格一致」的版本见上一行 |
tests/evidence/m11b2d_a1_pytest.txt |
# CMD: cd modules/world_sync && /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/.venv/bin/python -m pytest tests/test_m11b2_single_tx.py -v --tb=short → 48 items collected,40 passed, 8 xfailed |
并行任务 M11b-2d-A1/A1c 的产物证据,非本任务交付物;本任务未改名(改名会二次触碰 A1 归档),在此登记归属。注意该结果对应提交 18dc298 时点,已被 §7.1 所述回退打回 |
tests/evidence/m11b2d_a1_collect_only.txt |
# CMD: cd modules/world_sync && /d/pipeline/workspaces/0/sdlc_general/modules/world_sync/.venv/bin/python -m pytest tests/test_m11b2_single_tx.py --collect-only -q → 48 tests collected |
同上(A1 线归档) |
tests/evidence/m11b2d_a3a_a1c_damage_forensics.txt |
首行原文为 # FILE: tests/evidence/m11b2d_a3a_a1c_damage_forensics.txt(取证转录件,非单命令输出:正文以 $ (a) … 逐条列出实跑命令与原样输出含 RC,命令清单见 §7.1) |
A3a 轮取证归档(响应 QC #8/#17) |
tests/evidence/m11b2d_a3a_script_cleanup_grep.txt |
# CMD: cd modules/world_sync && grep -r 'm11b2c_patch_fixture_ddl' scripts/ pyproject.toml build.sh tests/ world_sync/ ; echo RC=$? → 4 段检索原样输出(build.sh: No such file or directory;命中仅本文档自身,代码/配置零引用) |
A3a 轮一次性脚本清理取证(§8 规则 8 执行记录) |