真 bug 合集:向量库探活误报 + edit 降级漏查能力位 + 群聊沉默窗口不撤销 + pytest 配置节名错 #93

Closed
opened 2026-07-29 08:21:47 +00:00 by KumaAgent · 0 comments
Member

来源:2026-07-29 全 ADR 查漏(31 份 ADR + CONTEXT.md + design.md 对照 origin/main)+ 内部结构实测。本票是第一批(先 bug → 再文档 → 再结构)里的「小修合集」,五处彼此独立、全是一到几行的修复,一个 PR 完成。

1. storage.py 向量库探活把 property 当函数调用 → /diagnose 恒误报

src/nonebot_plugin_arise/storage.py:1763

await self._qdrant().get_collections()

_qdrant@propertystorage.py:690),返回 AsyncQdrantClient,加括号即 TypeError: 'AsyncQdrantClient' object is not callable

决定性证据:全文件 self._qdrant 共 11 处,10 处是 self._qdrant.upsert(...) 式属性访问,唯独 1763 行多了括号955/1009/1585/1596/1600/1622/1632/1892/1917/1934 全部无括号)。

后果/diagnose_probe("向量库", ...),而 _probe 宽捕 Exception 只回类名 → 真实部署上向量库永远报 down,理由是 TypeError,一个纯误报且理由具误导性。

为什么没被发现:唯一相关测试 tests/test_admin_commands.py:625 用的是自定义假 storage,PersistentStorage.ping_vector_store 从未被执行过

  • 去掉括号。
  • 补一条真的走 PersistentStorage.ping_vector_store 的测试(否则同类手误还会再来一次)。

2. _handle_edit 的降级路径不查 capabilities.self_recall

runtime_loop.py::_handle_editif self._capabilities.edit: 走原生 edit;else 分支直接 await self._egress.recall(chat_id, entry.message_id),全程不查 capabilities.self_recall

而同文件 _handle_self_recall 有这个 fail-closed 检查——issue #20 修过一模一样的 bug,这次从 edit 那扇门原样漏出来。CapabilitySet 全字段默认 Falseedit=False & self_recall=False 就是缺省组合,而 edit 工具对模型恒可见。

违反 ADR-0004「core 拥降级策略」+ ADR-0012「无 self_recall → post-send 撤回不可用」。

  • 降级路径补 capabilities.self_recall 检查,为假即 fail-closed 不产生任何平台调用。
  • 必须同时修测试替身tests/fake_egress.py:49recall() 是唯一一个不检查能力位就记账的方法(其余七个都 raise NotImplementedError),所以就算有人写了这条组合的用例也不会红。

3. 沉默窗口在群聊里不撤销 → 恰好在抢话

_silence_window.interrupt() 全仓唯一调用点__init__.py:263,位于 _handle_reactive_message 内,而该 handler 注册在 reactive_matcher = on_message(rule=to_me())

后果:群聊里第三方正常发言(没 @ 机器人)不撤销待发消息——恰恰是这个机制要治的「抢话」。私聊因 to_me() 恒真而碰巧正确,这也是它一直没被发现的原因。

CONTEXT.md「沉默窗口」条与 silence_window.py:3 模块文档都断言「窗口内任何人发言即撤销(补位而非抢话)」,与接线不符。

  • 新挂一个 @event_postprocessor,只调 _silence_window.interrupt(chat_id)纯撤销、不产生任何 LLM 调用,所以不构成「新增消息接收面/成本面」的架构决定(那是 gating.py:28 记录过的既有顾虑,本改动不触及)。仓里已有两个 @event_postprocessor 先例(_handle_environment_signal/_handle_presence_events)。
  • 补群聊场景回归测试:第三方发言应撤销待发。

4. [tool.pytest] 节名错,整节配置静默失效

pyproject.toml:93[tool.pytest],而 pytest 只读 [tool.pytest.ini_options]。全仓无 ini_options、无 pytest.ini/setup.cfg/tox.ini(已核实)。

于是以下全部未生效--import-mode=prepend--strict-markers--tb=short-ra-s-vpythonpath = ["src"]asyncio_mode = "auto"asyncio_default_fixture_loop_scope = "session"

注意实际影响没有听起来大,别当成"测试都白跑了"

  • pythonpath 冗余(包是 editable 安装);

  • asyncio_mode/loop_scopetests/conftest.pypytest_collection_modifyitems 钩子独立补偿(用 is_async_test 给异步测试补 pytest.mark.asyncio(loop_scope="session")),且 69 个测试文件本身带显式 marker——测试是真在跑,1436 绿不是假的

  • 真实损失是 --strict-markers(打错的 marker 被静默忽略)和几条纯观感项。

  • 改成 [tool.pytest.ini_options]

  • 改完跑一次全量确认没有因 --strict-markers 生效而暴露出既有的错拼 marker;有就一并修。

5. ports.py:172 错误文案

查漏顺带发现的文案问题(低价值,搭车修)。实施时读一下当前文本再决定怎么改。

Not in scope

  • 本票只做上述五处,不夹带任何结构调整(__init__.py 拆分、StoragePort 抽出等在结构批)。
  • 不碰 Delta 压缩容错与 valence 产出方(另一张票,且那两件事有先后依赖)。
  • 不碰 send 失败兜底(另一张票,改动面中等)。
> 来源:2026-07-29 全 ADR 查漏(31 份 ADR + CONTEXT.md + design.md 对照 `origin/main`)+ 内部结构实测。本票是**第一批**(先 bug → 再文档 → 再结构)里的「小修合集」,五处彼此独立、全是一到几行的修复,一个 PR 完成。 ## 1. `storage.py` 向量库探活把 property 当函数调用 → `/diagnose` 恒误报 `src/nonebot_plugin_arise/storage.py:1763`: ```python await self._qdrant().get_collections() ``` `_qdrant` 是 `@property`(`storage.py:690`),返回 `AsyncQdrantClient`,加括号即 `TypeError: 'AsyncQdrantClient' object is not callable`。 **决定性证据**:全文件 `self._qdrant` 共 11 处,**10 处是 `self._qdrant.upsert(...)` 式属性访问,唯独 1763 行多了括号**(`955/1009/1585/1596/1600/1622/1632/1892/1917/1934` 全部无括号)。 **后果**:`/diagnose` 走 `_probe("向量库", ...)`,而 `_probe` 宽捕 `Exception` 只回类名 → 真实部署上**向量库永远报 `down`,理由是 `TypeError`**,一个纯误报且理由具误导性。 **为什么没被发现**:唯一相关测试 `tests/test_admin_commands.py:625` 用的是自定义假 storage,`PersistentStorage.ping_vector_store` **从未被执行过**。 - [ ] 去掉括号。 - [ ] 补一条真的走 `PersistentStorage.ping_vector_store` 的测试(否则同类手误还会再来一次)。 ## 2. `_handle_edit` 的降级路径不查 `capabilities.self_recall` `runtime_loop.py::_handle_edit`:`if self._capabilities.edit:` 走原生 edit;**else 分支直接 `await self._egress.recall(chat_id, entry.message_id)`,全程不查 `capabilities.self_recall`**。 而同文件 `_handle_self_recall` 有这个 fail-closed 检查——issue #20 修过**一模一样**的 bug,这次从 `edit` 那扇门原样漏出来。`CapabilitySet` 全字段默认 `False`,`edit=False & self_recall=False` 就是缺省组合,而 `edit` 工具对模型恒可见。 违反 ADR-0004「core 拥降级策略」+ ADR-0012「无 `self_recall` → post-send 撤回不可用」。 - [ ] 降级路径补 `capabilities.self_recall` 检查,为假即 fail-closed 不产生任何平台调用。 - [ ] **必须同时修测试替身**:`tests/fake_egress.py:49` 的 `recall()` 是唯一一个**不检查能力位就记账**的方法(其余七个都 `raise NotImplementedError`),所以就算有人写了这条组合的用例也不会红。 ## 3. 沉默窗口在群聊里不撤销 → 恰好在抢话 `_silence_window.interrupt()` **全仓唯一调用点**在 `__init__.py:263`,位于 `_handle_reactive_message` 内,而该 handler 注册在 `reactive_matcher = on_message(rule=to_me())`。 **后果**:群聊里第三方正常发言(没 @ 机器人)**不撤销**待发消息——恰恰是这个机制要治的「抢话」。私聊因 `to_me()` 恒真而碰巧正确,这也是它一直没被发现的原因。 CONTEXT.md「沉默窗口」条与 `silence_window.py:3` 模块文档都断言「窗口内**任何人**发言即撤销(补位而非抢话)」,与接线不符。 - [ ] 新挂一个 `@event_postprocessor`,只调 `_silence_window.interrupt(chat_id)`。**纯撤销、不产生任何 LLM 调用**,所以不构成「新增消息接收面/成本面」的架构决定(那是 `gating.py:28` 记录过的既有顾虑,本改动不触及)。仓里已有两个 `@event_postprocessor` 先例(`_handle_environment_signal`/`_handle_presence_events`)。 - [ ] 补群聊场景回归测试:第三方发言应撤销待发。 ## 4. `[tool.pytest]` 节名错,整节配置静默失效 `pyproject.toml:93` 是 `[tool.pytest]`,而 pytest 只读 **`[tool.pytest.ini_options]`**。全仓无 `ini_options`、无 `pytest.ini`/`setup.cfg`/`tox.ini`(已核实)。 于是以下**全部未生效**:`--import-mode=prepend`、`--strict-markers`、`--tb=short`、`-ra`、`-s`、`-v`、`pythonpath = ["src"]`、`asyncio_mode = "auto"`、`asyncio_default_fixture_loop_scope = "session"`。 **注意实际影响没有听起来大,别当成"测试都白跑了"**: - `pythonpath` 冗余(包是 editable 安装); - `asyncio_mode`/`loop_scope` 被 `tests/conftest.py` 的 `pytest_collection_modifyitems` 钩子独立补偿(用 `is_async_test` 给异步测试补 `pytest.mark.asyncio(loop_scope="session")`),且 69 个测试文件本身带显式 marker——**测试是真在跑,1436 绿不是假的**。 - 真实损失是 **`--strict-markers`**(打错的 marker 被静默忽略)和几条纯观感项。 - [ ] 改成 `[tool.pytest.ini_options]`。 - [ ] 改完**跑一次全量**确认没有因 `--strict-markers` 生效而暴露出既有的错拼 marker;有就一并修。 ## 5. `ports.py:172` 错误文案 查漏顺带发现的文案问题(低价值,搭车修)。实施时读一下当前文本再决定怎么改。 ## Not in scope - 本票只做上述五处,不夹带任何结构调整(`__init__.py` 拆分、`StoragePort` 抽出等在结构批)。 - 不碰 Delta 压缩容错与 valence 产出方(另一张票,且那两件事有先后依赖)。 - 不碰 `send` 失败兜底(另一张票,改动面中等)。
Yushu closed this issue 2026-07-29 10:05:13 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ProjectKuma/arise#93
No description provided.