[A3a-1b] 恢复登记文档全文(撤销 5b56b27 代收口用三处摘录整体覆盖导致的 594 行删除):以 1a4002e 的 49,966B/609 行为基线,§0-§8 全章节回归(响应 QC #1 硬门禁·真实性)
This commit is contained in:
parent
5b56b2725c
commit
6a1cce6400
@ -1,21 +1,609 @@
|
|||||||
【本轮(A3a-1b)对该既有文档的三处定点改动原文,非文档全文;全文 49,966B / 610 行见 HEAD 1a4002e】
|
# 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 移交结论以新文件为准。
|
||||||
|
- **A3a-1 轮重做说明(2026-09-23,任务 [M11b-2d-A3a-1] 第二次交付,响应 QC #1/#2/#3/#4/#5)**:
|
||||||
|
上一版交付把本登记文档 543 行正文整体删除、只留改动段落摘录(实测 4939B / 39 行 vs
|
||||||
|
改动前 44796B / 572 行),属「声称定点修正、实为破坏性截断」,本轮已用
|
||||||
|
`git checkout 616ed5f -- docs/work-log-2026-09-22-m11b2d-fixture-contract.md` **恢复全文**
|
||||||
|
后再做定点替换(本轮 diff 仅涉及本节、§6 执行证据段、§8 证据索引表新增一行,
|
||||||
|
未删除任何既有章节)。同时纠正上一版虚假自测声明:文档全文被截断时
|
||||||
|
`RegistrationDocCase` 两项守卫用例即红(缺 §1.1 符号清单 / `tenant_id` 真源形参),
|
||||||
|
恢复全文后复跑为 `17 passed`、`# RC=0`,真实输出另存新证据
|
||||||
|
`tests/evidence/m11b2d_a3a1_fixture_contract_rerun.txt`(不覆盖旧归档),索引见 §8。
|
||||||
|
- **A3a-1a 轮恢复说明(2026-09-23,任务 [A3a-1a])**: 上一轮交付(提交 `807e72f`,引擎代收口)
|
||||||
|
把本文档正文压成改动段落摘录(实测 2,540B / 20 行,§0–§8 章节标题全部丢失),属「以 diff
|
||||||
|
片段替代文档本体」的破坏性覆盖。本轮按任务书用
|
||||||
|
`git checkout 4ae2e20 -- modules/world_sync/docs/work-log-2026-09-22-m11b2d-fixture-contract.md`
|
||||||
|
**恢复完整正文**(实测 46,554B,§0–§8 章节标题全部重新命中),并在正文内就地核对 §6「四组 /
|
||||||
|
17 项」口径与 `RegistrationDocCase` 两行表格行、§8 证据索引各行「跑的是什么」列与归档件首行
|
||||||
|
`# CMD:` 逐字一致(含 `m11b2d_a3_single_tx_after_revert.txt` 首行路径笔误原样照抄并显式标注,
|
||||||
|
顺手改对反而造成「引用与文件内容不一致」)。恢复后本地复跑
|
||||||
|
`./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -q --tb=no` → **17 passed**
|
||||||
|
(`# RC=0`),与 §6 声明的计数口径一致。本节以下正文与提交 `4ae2e20` 逐字相同,未新增或删除
|
||||||
|
任何既有章节。
|
||||||
|
|
||||||
(1) 文首新增段(第 37-41 行):
|
|
||||||
- **A3a-1b 轮复跑说明(2026-09-23,任务 [A3a-1b],响应 QC #6/#7 交付摘要与实测一致硬门禁)**: 本轮不改任何测试文件与业务实现,只在恢复后的真实状态(HEAD `d6514bf`,本文档 47,693B)上**实跑**守卫用例并生成真实证据:
|
- **A3a-1b 轮复跑说明(2026-09-23,任务 [A3a-1b],响应 QC #6/#7 交付摘要与实测一致硬门禁)**: 本轮不改任何测试文件与业务实现,只在恢复后的真实状态(HEAD `d6514bf`,本文档 47,693B)上**实跑**守卫用例并生成真实证据:
|
||||||
`./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -q --tb=no` → **17 passed in 0.07s**(`# RC=0`)、
|
`./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -q --tb=no` → **17 passed in 0.07s**(`# RC=0`)、
|
||||||
`./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py --collect-only -q` → **17 tests collected in 0.03s**(`# RC=0`),
|
`./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py --collect-only -q` → **17 tests collected in 0.03s**(`# RC=0`),
|
||||||
`RegistrationDocCase` 两项均绿。真实输出已归档:名为 `m11b2d_a3a1_fixture_contract_rerun.txt` 的 A3a-1 轮旧件(`# HEAD: 1d1fd3d`,`17 passed in 0.09s`)按本任务书指定路径**被本次输出覆盖**,另新增同轮 collect-only 留证
|
`RegistrationDocCase` 两项均绿。真实输出已归档:名为 `m11b2d_a3a1_fixture_contract_rerun.txt` 的 A3a-1 轮旧件(`# HEAD: 1d1fd3d`,`17 passed in 0.09s`)按本任务书指定路径**被本次输出覆盖**,另新增同轮 collect-only 留证
|
||||||
`m11b2d_a3a1_fixture_contract_collect_only_rerun.txt`;`m11b2d_a3_*`、`m11b2d_a1_*` 等历史归档一律未覆盖。
|
`m11b2d_a3a1_fixture_contract_collect_only_rerun.txt`;`m11b2d_a3_*`、`m11b2d_a1_*` 等历史归档一律未覆盖。
|
||||||
本轮文档改动为本节、§6 执行证据段与 §8 证据索引表(更新 1 行 + 新增 1 行),未删除任何既有章节;§8 各行「跑的是什么」与对应文件首行 `# CMD:` 逐字一致。
|
本轮文档改动为本节、§6 执行证据段与 §8 证据索引表(更新 1 行 + 新增 1 行),未删除任何既有章节;§8 各行「跑的是什么」与对应文件首行 `# CMD:` 逐字一致。
|
||||||
|
- **文档定位**: **索引式登记**。夹具层实现说明的权威正文在 `tests/_m11b2_compat.py` 顶部
|
||||||
|
docstring(不双写搬运);真源行为的权威定义在 `modules/pbl_runtime_ext/pbl_runtime_ext/tx_write.py`
|
||||||
|
源码本身。本文档只做**符号清单 + 逐参数签名 + 返回键语义 + 错误码含义 + 废弃清单 + 引用规约**,
|
||||||
|
全部内容由本次实读源码抄录(锚点行号见各节,行号以 2026-09-22 HEAD 为准)。
|
||||||
|
|
||||||
(2) §6 执行证据段新增句(紧接「恢复后 `2 failed` 已消除)。」之后):
|
---
|
||||||
|
|
||||||
|
## 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`,原文:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
|
||||||
|
```python
|
||||||
|
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`)。
|
||||||
|
A3a-1 轮在恢复文档全文后再次复跑复核一致,并把真实输出(含 `# RC=0`)归档为
|
||||||
|
`tests/evidence/m11b2d_a3a1_fixture_contract_rerun.txt`(首行 `# CMD:` 原文见 §8 索引;
|
||||||
|
上一版因截断本文档导致 `RegistrationDocCase` 两项红,恢复后 `2 failed` 已消除)。
|
||||||
A3a-1b 轮在恢复提交 `d6514bf` 上再次复跑(`-q --tb=no` → `17 passed in 0.07s`、
|
A3a-1b 轮在恢复提交 `d6514bf` 上再次复跑(`-q --tb=no` → `17 passed in 0.07s`、
|
||||||
`--collect-only -q` → `17 tests collected in 0.03s`,两者均 `# RC=0`),真实输出按任务书覆盖同名件
|
`--collect-only -q` → `17 tests collected in 0.03s`,两者均 `# RC=0`),真实输出按任务书覆盖同名件
|
||||||
`tests/evidence/m11b2d_a3a1_fixture_contract_rerun.txt`,并另存新件
|
`tests/evidence/m11b2d_a3a1_fixture_contract_rerun.txt`,并另存新件
|
||||||
`tests/evidence/m11b2d_a3a1_fixture_contract_collect_only_rerun.txt`;`m11b2d_a3_*` 等历史归档未覆盖,
|
`tests/evidence/m11b2d_a3a1_fixture_contract_collect_only_rerun.txt`;`m11b2d_a3_*` 等历史归档未覆盖,
|
||||||
`RegistrationDocCase` 两项在恢复后的文档上恒绿。
|
`RegistrationDocCase` 两项在恢复后的文档上恒绿。
|
||||||
并把 §6 计数口径句改为:各归档证据文件的 passed 数四者一一对齐(A3 轮两份 + A3a/A3a-1/A3a-1b 轮复跑件,见 §8 索引)
|
**计数口径**:本节用例数(17)与上表逐行用例数、`--collect-only` 的 **17 tests collected**、
|
||||||
|
各归档证据文件的 passed 数四者一一对齐(A3 轮两份 + A3a/A3a-1/A3a-1b 轮复跑件,见 §8 索引);证据文件首行 `# CMD:` 原文见 §8 证据索引(本表「组」列即四组,无第五组)。
|
||||||
|
|
||||||
(3) §8 证据索引表:更新 1 行 + 新增 1 行(第 601-602 行):
|
---
|
||||||
|
|
||||||
|
## 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)——本处移交结论以该新文件为引用锚点)。失败根因是该文件仍按**旧类式夹具口径**
|
||||||
|
写断言,与真源不符,需两处修正(本任务无权改,移交该文件独占方 / 另开任务):
|
||||||
|
|
||||||
|
1. `TxCase.apply`:`"state_updates": su() if states is None else states` → 真源要求数组,
|
||||||
|
应为 `[su()]`(否则撞 `PBL-PARAM-0001`,并连带 5 个 `TestNormalWrite`/`TestAtomicRollback` 用例)。
|
||||||
|
2. `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
|
||||||
|
```text
|
||||||
|
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
|
||||||
|
```text
|
||||||
|
(stdout 为空 —— 该提交对 tests/test_m11b2_single_tx.py 零改动)
|
||||||
|
```
|
||||||
|
RC=0
|
||||||
|
|
||||||
|
$ (c) git show 18dc298 --numstat -- tests/test_m11b2_single_tx.py
|
||||||
|
```text
|
||||||
|
(stdout 为空 —— 该提交对 tests/test_m11b2_single_tx.py 零改动)
|
||||||
|
```
|
||||||
|
RC=0
|
||||||
|
|
||||||
|
$ (d) git diff 18dc298 HEAD -- tests/test_m11b2_single_tx.py
|
||||||
|
```text
|
||||||
|
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'(该提交实际改了哪些文件)
|
||||||
|
```text
|
||||||
|
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'(该提交实际改了哪些文件)
|
||||||
|
```text
|
||||||
|
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(净增删行数)
|
||||||
|
```text
|
||||||
|
5 18 tests/test_m11b2_single_tx.py
|
||||||
|
```
|
||||||
|
RC=0
|
||||||
|
|
||||||
|
$ (e) A1c 交付时归档证据 tests/evidence/m11b2d_a1_pytest.txt 的结果行(git show 18dc298:...)
|
||||||
|
```text
|
||||||
|
# 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 特征串)
|
||||||
|
```text
|
||||||
|
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
|
||||||
|
|
||||||
|
#### 结论(三条,均有上方实测支撑)
|
||||||
|
|
||||||
|
1. **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 零差异。
|
||||||
|
2. **提交 `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 从未在该测试文件上产生独立提交内容。
|
||||||
|
3. **但 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`)。
|
||||||
|
|
||||||
|
**给 A3b 的精确输入**:修复权属该文件独占方(A1/A1c 线),修复内容 = 重新落地上列 Hunk 1~4
|
||||||
|
(其中 Hunk 1/3/4 是消除 8 failed 的必需项,Hunk 2 是断言口径正确性项);除此之外不要动 A1c
|
||||||
|
已存活的三处外观清理。本任务再次**只登记不修**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 后续任务引用规约(硬性)
|
||||||
|
|
||||||
|
1. **新测试写运行时事务**:一律 `import _m11b2_compat as compat` → `compat.install(compat.RuntimeEngine())`
|
||||||
|
→ `compat.apply(...)` / `compat.read(...)`;`addCleanup(compat.uninstall)`。**不得**新建类式垫片、
|
||||||
|
不得 import `world_sync.pbl_runtime_tx.write_event_with_state` 作为夹具入口。
|
||||||
|
2. **表名 / 错误码 / 通道名**:只从 `compat` 或真源常量取(`compat.EVENT_TABLE`、`compat.ERR_BAD_PARAM`、
|
||||||
|
`rtx_broadcast.channel_of(...)`),**禁止第二份字面量**。
|
||||||
|
3. **返回键断言**:以本文档 §3 为准;新增键须先改真源、再同步 §3 与守卫用例(三者不一致即红)。
|
||||||
|
4. **`occurred_at` 必须逐事件唯一**:事件表两条唯一索引含分区列 `created_at`,同秒两条会撞 DB 唯一索引
|
||||||
|
(真源不填 `idem_key`,DEFAULT `''`)。用各测试文件自带的 `stamp()` 取互不相同时间戳。
|
||||||
|
5. **乐观锁**:只在 Python 侧用 `base_version` 表达期望,**不要**在 SQL 里找 version CAS。
|
||||||
|
6. **广播**:断言 `BROADCAST` 轨迹晚于 `COMMIT`;广播失败期望 `PBL-BROAD-0001` 且**数据仍在库**。
|
||||||
|
7. **夹具不可改**:`tests/_m11b2_compat.py` 既有符号只读;需要新能力时在本文件**追加**并同步本文档 §1,
|
||||||
|
不改既有签名与语义。
|
||||||
|
8. **一次性调试工具禁止入库**:探针/补丁脚本一律不落在模块仓库根目录;有长期价值就升级为 `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`)。
|
||||||
|
|
||||||
|
### 证据索引(消 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_a3a1_fixture_contract_rerun.txt` | `# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -q --tb=no` → **17 passed in 0.07s**(`# RC=0`,`# HEAD: d6514bf`) | **A3a-1b 轮复跑证据**(A3a-1 轮同名件在本轮按任务书被本次真实输出覆盖,旧内容 `# HEAD: 1d1fd3d`;`m11b2d_a3_*` 等历史归档未触碰):响应 QC #6/#7,文档全文恢复后(HEAD `d6514bf`)在当前工作区的复跑留证,本列与文件首行 `# CMD:` 及正文输出逐字一致 |
|
| `tests/evidence/m11b2d_a3a1_fixture_contract_rerun.txt` | `# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py -q --tb=no` → **17 passed in 0.07s**(`# RC=0`,`# HEAD: d6514bf`) | **A3a-1b 轮复跑证据**(A3a-1 轮同名件在本轮按任务书被本次真实输出覆盖,旧内容 `# HEAD: 1d1fd3d`;`m11b2d_a3_*` 等历史归档未触碰):响应 QC #6/#7,文档全文恢复后(HEAD `d6514bf`)在当前工作区的复跑留证,本列与文件首行 `# CMD:` 及正文输出逐字一致 |
|
||||||
| `tests/evidence/m11b2d_a3a1_fixture_contract_collect_only_rerun.txt` | `# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py --collect-only -q` → **17 tests collected in 0.03s**(`# RC=0`,`# HEAD: d6514bf`) | **A3a-1b 轮新增 collect-only 复跑证据**(未覆盖 `m11b2d_a3_fixture_contract_collect_only.txt` 旧归档):四组 17 项可收集性在当前 HEAD 的复证,本列与文件首行 `# CMD:` 及正文输出逐字一致 |
|
| `tests/evidence/m11b2d_a3a1_fixture_contract_collect_only_rerun.txt` | `# CMD: cd modules/world_sync && ./.venv/bin/python -m pytest tests/test_m11b2d_fixture_contract.py --collect-only -q` → **17 tests collected in 0.03s**(`# RC=0`,`# HEAD: d6514bf`) | **A3a-1b 轮新增 collect-only 复跑证据**(未覆盖 `m11b2d_a3_fixture_contract_collect_only.txt` 旧归档):四组 17 项可收集性在当前 HEAD 的复证,本列与文件首行 `# CMD:` 及正文输出逐字一致 |
|
||||||
|
| `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 执行记录) |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user