session-mechanism:闸④ 不再被周期排期卡死 + --report 漏 id 当场拒收

① 闸④ 收窄 recurring(用户选 A 案)
   改 ws_pending_schedules():schedule_type='recurring' 的行一律不算「待执行」,
   once 的过期排除与拦截照旧。
   症状=周期排期永远有下一次 ⇒ 拿「在册 + 没跑过」判它恒真 ⇒ 闸④ 恒拦
   ⇒ 目标检查会话一条都建不出来(aigc-idea-impression 被「AI变现日报 每日 06:00」
   挡了一整天,日志每 20 秒重复「还有 1 条待执行排期 ⇒ 不建」)。
   变异对照:同一工作区、同一时刻,旧版返回 ['AI变现日报 · 每日自动生成']、新版返回 []。

② --report 拒收闸(用户选 B 案)
   --report 后面必须跟真的需求 id;id 为空或以 -- 开头 ⇒ 打用法行 + 退出码 2,台账一字不写。
   症状=漏 id 时 _arg("--report") 取到的是下一个 token(--state)
   ⇒ 台账多一条键名写坏的记录,还被算进队列计数(本区 queue_n 2→3;vibe-product 也有一条)。
   变异对照:把闸删掉 ⇒ 同一条命令 rc=0 且真写出 "--state";加回去 ⇒ rc=2 且台账为空。

自检:闸④ 用例 5 → 6 项;新增「拒收闸」用例 6 项(含正例对照,防「未找到部署配置 ⇒ 拒跑」
把 rc=2 混成假绿);全量 PASS 110 / FAIL 0。

文档:references/pitfalls.md —— P0-65 补 recurring 口径(含实测症状与反例库);
新增 P0-96(漏写 --report id 的静默污染 + 判据自身两处假绿坑)。
This commit is contained in:
admin committed 2026-10-12 09:50:10 +08:00
1 parent e12e985808
commit 24040ee171
3 files changed
+156 -3

No files matched your search

+86 -3
View File
@@ -3891,6 +3891,79 @@ def t_check_prompt_no_wild_hunt():
@case("🔴 `--report` 拒收闸:漏写需求 id ⇒ 报错退出 + ⛔ 不写台账(2026-10-12 用户选 B 案)")
def t_report_requires_id():
"""🔴 2026-10-12 实测缺陷(本区 + `vibe-product` 都中过)。
🔴 **现象**:把收报写成 `--report --state done --artifact …`(**漏了 id**)⇒
`_arg("--report")` 取到的是**下一个 token**(实为 `--state`)
⇒ 台账里多出一条**键名写坏**的记录,而且它**被算进队列计数**
(本区实测 `queue_n` 2 → 3;`vibe-product` 的 `q_sig` 里也有一条 `--state=done`)。
🔴 **为什么必须拦**:这类污染是**静默**的 —— 命令看着像成功、台账看着像有记录;
更糟的是它可能落在 `running`/`blocked` 态 ⇒ 闸① 误判「有活在跑」⇒ 去建结果检查会话。
✅ **正解**=`--report` 分支加拒收闸:id 为空或**以 `--` 开头** ⇒ 打用法行 + 退出码 2,
台账一个字都不写。
⚠️ 判据自身的坑(两个都得堵,否则**假绿**):
① **正例探针⛔绝不能指向真工作区** —— 我 2026-10-12 09:43 就是这么往真台账里
敲进一条 `ZZZ-假id`(`running`)的,只能手工撤回。⇒ 一律用临时工作区。
② **临时工作区必须自带 `collabd.config.json`** —— 不然 `collabd.py` 会先
「未找到部署配置 ⇒ 拒跑」并返回 **rc=2** ⇒ 负例断言**因为错的理由**变绿。
⇒ 夹具里现造一份最小配置,并加一条**正例**(形态正确的 id ⇒ rc=0)当对照。
"""
import json as _j
import os as _os
import shutil as _sh
import subprocess as _sp
import tempfile as _tf
import pathlib as _pl
m = imp()
_src = (HERE / "collabd.py").read_text(encoding="utf-8")
_own = (HERE / "selftest.py").read_text(encoding="utf-8")
has_guard = ('if (not _rid) or _rid.startswith("--"):' in _src
and '✗ `--report` 后面必须跟' in _src)
uses_tmp = 'env["COLLABD_WORKSPACE"] = str(tb)' in _own
tb = _pl.Path(_tf.mkdtemp(prefix="dsh-report-id-"))
try:
cfg = tb / ".workbuddy" / "collab" / "collabd.config.json"
cfg.parent.mkdir(parents=True, exist_ok=True)
cfg.write_text(_j.dumps({"workspace": str(tb).replace("\\", "/"),
"inbox": "tmp/supervise-inbox",
"live": "交付物/实时状态.md",
"singleton_port": 20999}, ensure_ascii=False),
encoding="utf-8")
(tb / "y.md").write_text("夹具产物", encoding="utf-8") # ⛔ 别让 artifact 缺失来抢答
env = dict(_os.environ)
env["COLLABD_WORKSPACE"] = str(tb)
env["COLLABD_CONFIG"] = str(cfg)
env["COLLABD_NO_ENSURE"] = "1"
_cmd = [sys.executable, str(HERE / "collabd.py"), "--report"]
# 正例(对照):形态正确的 id ⇒ 必须被受理,证明配置可用、确实走到了 --report 分支
r_ok = _sp.run(_cmd + ["T-正常id", "--state", "running", "--by", "b"],
cwd=str(tb), env=env, capture_output=True, text=True, timeout=120)
# 负例:漏 id(=本轮真出事的写法)
r_bad = _sp.run(_cmd + ["--state", "done", "--by", "b", "--artifact", "y.md"],
cwd=str(tb), env=env, capture_output=True, text=True, timeout=120)
rc_ok, rc = r_ok.returncode, r_bad.returncode
out = (r_bad.stdout or "") + (r_bad.stderr or "")
said = ("必须跟" in out) and ("需求 id" in out)
led = list(tb.rglob("tasks.json"))
blob = "".join(p.read_text(encoding="utf-8", errors="replace") for p in led)
no_bad_key = '"--state"' not in blob
wrote_ok = '"T-正常id"' in blob
finally:
_sh.rmtree(str(tb), ignore_errors=True)
return [
("① 源码里有拒收闸(id 为空或以 `--` 开头 ⇒ 拒收)", has_guard),
("② 正例对照:形态正确的 id ⇒ 受理(rc=%s;⚠️ 拿掉它,③ 就可能因"
"「未找到部署配置」而假绿)" % rc_ok, rc_ok == 0 and wrote_ok),
("③ 漏写 id ⇒ **退出码 2**(实得 %s)" % rc, rc == 2),
("④ 报错说清缺什么(含「需求 id」与正确用法)", said),
("⑤ ⛔ 一个字都没写进台账(无坏键 `\"--state\"`)", no_bad_key),
("⑥ 判据本身用**临时工作区**起子进程(变异:拿掉那行 ⇒ 本项变红)", uses_tmp),
]
@case("台账纪律:`done` 必须带产物文档 + `blocked` 必须带原因(否则检查会话只能去翻)")
def t_done_requires_artifact():
"""🔴 2026-10-03 10:5x(用户要求逐字):
@@ -4355,11 +4428,14 @@ def t_gate4_stale_once():
⇒ ⛔ 靠 `last_run_at` 收口**永远收不了**。
✅ **正解**=排除「**已过期的一次性**排期」,宽限 **15 分钟**
(宿主扫描周期 ≤30 s、实测到点延迟 20~35 s ⇒ 15 min 是安全余量)。
⚠️ 四条边界(缺一条就误杀或漏杀):
⚠️ 五条边界(缺一条就误杀或漏杀):
① 刚过期 ⇒ **仍算待执行**(马上要开 ⇒ 闸④ 该拦)
② 过期 >15 min ⇒ **排除**
③ `next_run_at` 为 NULL ⇒ **仍算待执行**(⛔ 别误杀)
④ `recurring` 过期 ⇒ **仍算待执行**(它会再触发)
④ `recurring` ⇒ **一律排除**(2026-10-12 用户选 A 案)
🔴 **2026-10-12 新增边界 ④(用户选 A 案)**:`recurring` 周期排期**不算"待执行"**。
症状:周期排期永远有下一次 ⇒ 老口径下闸④ **恒拦** ⇒ 本区目标检查会话**一条都建不出来**
(实测被「AI变现日报 每日 06:00」挡了一整天,日志每 20 秒重复「还有 1 条待执行排期 ⇒ 不建」)。
"""
import re as _re
import time as _tm
@@ -4383,6 +4459,12 @@ def t_gate4_stale_once():
# ⇒ 我上一版写"NULL ⇒ 仍算待执行(别误杀)"**是错的** ⇒ 那样它照样永��卡闸④。
# (第五种情形:(a) 已过宽限 / (b) 已被消费 ⇒ 两者都排除。)
null_excluded = "if str(_st or \"\").lower() == \"once\":" in _c and "_gone = (not _na)" in _c
# ⑥ 🔴 2026-10-12(用户选 A 案):`recurring` 一律排除 —— 周期排期永远有下一次,
# 拿"在册+没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ 目标检查会话一条都建不出来。
recurring_excluded = (
'if str(_st or "").lower() == "recurring":' in _c
and "_rec.append(str(_nm))" in _c
and "continue" in _c)
# ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产)
live = m.ws_pending_schedules()
return [
@@ -4391,7 +4473,8 @@ def t_gate4_stale_once():
% limit_min, 10 <= limit_min <= 30),
("③ 只排 `once`(⛔ `recurring` 会再触发,永不按过期排除)", only_once),
("④ `once` 且 `next_run_at` 为 NULL(已被宿主消费)⇒ **也排除**", null_excluded),
("⑤ 判据现读不抛(ws_pending_schedules() = %s)" % (live if live else "[]"), True),
("⑤ `recurring`(周期排期)**一律排除** ⇒ 闸④ 不再恒拦", recurring_excluded),
("⑥ 判据现读不抛(ws_pending_schedules() = %s)" % (live if live else "[]"), True),
]