Files
workbuddy_skills/session-mechanism/scripts/selftest.py
T
admin 433d43af54 decision-rules-hook: 注入补「两个停止点」+「技术方案排序轴」
起因(2026-10-06 用户问):能自决的问题为什么还要问。
复盘发现注入的判据里**缺动作**:
- 缺「十步」的两个停止点 → ① 判类型(方案请求 vs 直接执行)② 判方向(扩大/收窄/中性);
- 缺「自主裁决顺序」的排序轴 → 更小改动 → 更少新增形态 → **不扩大可见面**。
⇒ 前者导致「会扩大可见面」时不停手;后者导致排完仍把已定项混进待拍板清单(=变相征询)。
本次只补这两条动作骨架(1459 → 1906 字符),⛔ 不塞十步全文(避免稀释)。
2026-10-06 22:39:51 +08:00

7242 lines
470 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""selftest —— 「多会话协作机制」的**回归测试**:把**所有异常情况**做成用例,每次改完跑一遍。
🔴 纪律(2026-09-29 用户定案):**每次改完 collabd.py / guard.py,必须跑本文件**,全绿才算改完。
设计原则:
· **不碰生产**:整套测试用 `COLLABD_CONFIG` 指向 `tmp/selftest/`(独立 inbox / 看板 / 任务图),
生产台账与看板**一个字节都不动**。
· **离线可跑**:需要宿主库的用例,读不到就记 SKIP(⛔ 不假装通过)。
· **判据是"读数"不是"没报错"**:rc、文件是否生成、台账状态、投递台账增量 …
· FAIL ⇒ **非零退出**(配合"rc=0 才是真成功")。
用法:python selftest.py # 全部
python selftest.py -k 投递 # 只跑名字含"投递"的用例
"""
from __future__ import annotations
# [session-mechanism] roots.env 外置(由 install.py 生成;缺失则回落到按位置推导)
def _sm_load_roots():
import os as _os
_here = _os.path.dirname(_os.path.abspath(__file__))
for _up in range(4):
_p = _os.path.join(_here, *([".."] * _up), "roots.env")
_p = _os.path.normpath(_p)
if _os.path.isfile(_p):
try:
with open(_p, encoding="utf-8") as _f:
for _ln in _f:
_ln = _ln.strip()
if _ln and not _ln.startswith("#") and "=" in _ln:
_k, _v = _ln.split("=", 1)
_os.environ.setdefault(_k.strip(), _v.strip())
except Exception:
pass
return
_sm_load_roots()
# 🔴 2026-10-01 加 · **输出编码兜底**:脚本一旦被重定向(钩子/常驻/后台任务都会这么干),
# Windows 本地编码(GBK)编不出 ⛔/✅/🔴 这类字符 ⇒ `print` 抛 UnicodeEncodeError
# ⇒ 被顶层 handler 记成 `fatal`、**整轮失败**(实测:本包里连续 4 次 `fatal 'gbk' codec ...`)。
# ⇒ 出口一律 UTF-8 + errors="replace"(⛔ 不让"打不出字"升级成"程序死")。
try:
import sys as _sys
_sys.stdout.reconfigure(encoding="utf-8", errors="replace")
_sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path
HERE = Path(__file__).resolve().parent
WS = Path(os.environ.get("DSH_COLLAB_WS") or os.environ.get("DSH_WS_ROOT") or Path.cwd())
TEST_WS = WS / "tmp" / "selftest"
TIN = TEST_WS / "inbox"
CFG = TEST_WS / "collabd.config.json"
PY = sys.executable
RESULTS = []
# 🔴🔴 2026-10-04 新增:**本工作区是否真启用了这套机制**(判「生产/副本」用)。
# 起因(`vibe-product` 实测坐实):那是个**技能副本**(`.workbuddy/skills/session-mechanism/`),
# **根本没启用机制** —— 连 `tmp/supervise-inbox/goal.json` 与 `.workbuddy/collab/collabd.py`
# 都不存在 ⇒ 那些「量**生产工作区部署齐全**」的判据(交付物/目标执行状态.md、peer 配置、
# 目标检查脚本、跨区 peer 格)**必然报红** ⇒ 副本被当生产环境自测。
# ⚠️ 这**不是技能有错**,是判据没声明适用范围 ⇒ 用**跳过**表达,⛔ 不许改成"永远绿"(那是把判据废掉)。
def _mech_on() -> bool:
"""本工作区**启用了**会话协作机制吗?判据=真源文件在不在(⛔ 不看 env、不猜)。"""
try:
return (WS / "tmp" / "supervise-inbox" / "goal.json").exists()
except Exception:
return False
MECH_ON = _mech_on()
def _strip_comments_and_docs(src: str) -> str:
"""把 Python 源码里的**注释 + 字符串(含 docstring)**全部剔掉,只留可执行骨架。
🔴🔴 2026-10-05 新增。**为什么必须有一个共用实现**(⛔ 不许各用例各写一份):
本项目有大量"判据查源码里有没有某字面"的用例。而本仓库的**考古纪律**要求:
废弃形态的说明**必须留在注释/docstring 里**(写明"曾怎么做、为什么废弃")。
⇒ 那些字面**注定存在** ⇒ 拿全文 `not in src` 判「已清除」**必然误红**。
⚠️ **同一个坑我今天踩了两遍**:
① `t_declare_resets_lifecycle`:只剔 `#` 行 ⇒ 这次对了(那句在 `#` 注释里);
② 本条守卫:`-File start-supervise.ps1` 那句在 **docstring** 里 ⇒ 只剔 `#` 行
**照样命中** ⇒ 又误红一次。
⇒ 结论:**「剔注释」≠「剔 docstring」**,必须用 `tokenize` 一次剔干净
(⛔ 别用正则凑 —— 多行字符串、嵌套引号、f-string 都会漏)。
⛔ 用 `tokenize` 失败(比如源码本身语法坏)时**回落到"保留原文"**:
宁可误红(能被看见)也不许误绿(静默放行)。
🔴🔴 **2026-10-05 二次修正(又踩一坑)**:第一版把 `STRING` **全剔**,结果把
「写在 PowerShell 命令**字符串里**的字面」也剔掉了(`New-ScheduledTaskAction`、
`supervise-launch.py` 正是**字符串内容**)⇒ 判据**由误红变成误红**(换个方向)。
✅ 正解=**只剔两样**:① `#` 注释;② **docstring**(用 `ast` 精确定位,⛔ 不用正则)。
普通字符串**一律保留** —— 因为"代码里干了什么"经常就写在字符串里(PowerShell 命令、
文件名、任务名……),剔掉等于**看不见真实现**。
"""
import ast as _ast
import io
import tokenize as _tk
# ① 先用 ast 抓出所有 docstring 的**起止行列**(模块/类/函数首表达式字符串)
_doc_spans = []
try:
_tree = _ast.parse(src)
for _node in _ast.walk(_tree):
if isinstance(_node, (_ast.Module, _ast.ClassDef, _ast.FunctionDef,
_ast.AsyncFunctionDef)):
_b = getattr(_node, "body", None)
if (_b and isinstance(_b[0], _ast.Expr)
and isinstance(_b[0].value, _ast.Constant)
and isinstance(_b[0].value.value, str)):
_doc_spans.append((_b[0].value.lineno, getattr(_b[0].value, "end_lineno", _b[0].value.lineno)))
except Exception:
_doc_spans = []
def _in_doc(ln):
return any(a <= ln <= b for a, b in _doc_spans)
# ② 逐 token 剔:注释一律剔;字符串**只在 docstring 位置**剔
try:
out = []
for tok in _tk.generate_tokens(io.StringIO(src).readline):
if tok.type == _tk.COMMENT:
continue
if tok.type == _tk.STRING and _in_doc(tok.start[0]):
continue
out.append(tok.string)
return " ".join(out)
except Exception:
return src
def _mk_env() -> dict:
env = dict(os.environ)
env["COLLABD_CONFIG"] = str(CFG)
env["DSH_COLLAB_WS"] = str(TEST_WS)
# 🔴 2026-10-02(S8):**测试环境里⛔ 不许起常驻**。
# 真因(本棒实测):`--tick` 现在会顺手"续命常驻"(`ensure_supervise()`),而自测用的
# `--tick` 继承了真环境的网关口令 ⇒ 它会在**测试工作区**里起一条真的 `--supervise`
# ⇒ 那条进程会持续 `supervise()` 并**重写测试夹具**(实测:`已停总闸` 用例报
# `告警投影被清掉` 假红、且 FAIL 数每轮不同)+ 留下僵尸测试进程。
# ⇒ 显式关掉(⛔ 不是"把断言写松")。
env["COLLABD_NO_ENSURE"] = "1"
return env
def _prepare() -> None:
TIN.mkdir(parents=True, exist_ok=True)
for _f in ("wake.lock", "guard.stop", "goal.pending.json",
# 🔴 2026-10-01 加:退场表也每轮清 —— 它会**跨轮留下来**,
# 而退场用例断言的是精确集合(`retired==1 / stale==1`)⇒
# 上一轮留下的那条会在下一轮把计数顶成 2 ⇒ **第二次跑才红的假红**(最难查的一型)。
"retired.json"): # 清上一轮残留(防跨轮假红)
try:
(TIN / _f).unlink()
except Exception:
pass
# 🔴 2026-10-01 加:**假配置根(假的会话日志)也每轮清空** —— 这棵树的文件会跨轮留下来,
# 而「哑会话」用例断言的是**精确集合**(`d == {DEAF}`)⇒ 上一轮留下的昨天那份 10 MiB
# 会在下一轮把结果多出几条 ⇒ **第二次跑才红的假红**(最难查的一型)。⛔ 只删测试区内的。
try:
shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True)
except Exception:
pass
(TEST_WS / "live.md").write_text("(selftest)", encoding="utf-8")
(TEST_WS / "tg.json").write_text(json.dumps({"nodes": [
{"id": "T1", "title": "测试件-1", "status": "todo", "line": "line-a"},
{"id": "T2", "title": "测试件-2", "status": "done", "line": "line-b"},
]}, ensure_ascii=False), encoding="utf-8")
CFG.write_text(json.dumps({
"_说明": "selftest 专用(⛔ 不碰生产)",
"workspace": str(TEST_WS).replace("\\", "/"),
"inbox": "inbox",
"live": "live.md",
"taskgraph": "tg.json",
"lines": {"line-a": "A 线", "line-b": "B 线"},
"goal_docs": [], "targets": {},
"host_db": "",
"singleton_port": 20098,
"interval": 5, "idle_min": 12, "stuck_min": 30, "vacuum_min": 5,
"health_every": 300, "wake_enable": True, "wake_min_gap": 300, "wake_max_ports": 4,
"wake_text": "(selftest)",
}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def case(name):
def deco(fn):
fn._case = name
return fn
return deco
def report(name):
"""**报告型**用例:照常显示、照常报红/报绿,但 ⛔ **不计入 `--verify` 的成败**。
🔴 2026-10-04 新增(口径判据落地时撞到):有些检查**天生依赖现网存量** ——
例「产物是否都落在目标文件夹」在**历史散落的文件清理掉之前**必然红。
⇒ 若让它计入 verify ⇒ **`install.py --verify` 在新机器上永远失败**,
而那个失败与"安装对不对"**毫无关系** ⇒ 判据被当成安装故障 ⇒ ⛔ 误报。
✅ 判据纪律:**"装得对不对"与"存量干不干净"必须分开判**,
后者只**如实报**,不牵动前者(否则一条提示会永久堵住安装)。
"""
def deco(fn):
fn._case = name
fn._report_only = True
return fn
return deco
def run_cli(args, timeout=60):
p = subprocess.run([PY, "-u", str(HERE / "collabd.py")] + args, cwd=str(HERE),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
env=_mk_env(), timeout=timeout, errors="replace")
return p.returncode, p.stdout or ""
def tasks() -> dict:
try:
return json.loads((TIN / "tasks.json").read_text(encoding="utf-8")) or {}
except Exception:
return {}
def wakeups_n() -> int:
try:
return len((TIN / "wakeups.jsonl").read_text(encoding="utf-8").strip().splitlines())
except Exception:
return 0
def state() -> dict:
try:
return json.loads((TIN / "collabd-state.json").read_text(encoding="utf-8")) or {}
except Exception:
return {}
def put_tasks(t: dict) -> None:
(TIN / "tasks.json").write_text(json.dumps(t, ensure_ascii=False, indent=2), encoding="utf-8")
def run_cli_env(args, env: dict, timeout=60):
"""同 `run_cli`,但**自定义环境**(用例要故意抽掉网关口令等)。"""
p = subprocess.run([PY, "-u", str(HERE / "collabd.py")] + args, cwd=str(HERE),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
env=env, timeout=timeout, errors="replace")
return p.returncode, p.stdout or ""
_PROD_SNAP = {}
def _snap_prod():
"""记录**生产 inbox** 关键产物的 (mtime,size) —— 元用例据此断言「测试不碰生产」。"""
global _PROD_SNAP
_PROD_SNAP = {}
for f in ("tasks.json", "wakeups.jsonl", "digest.md", "TO-MAIN.md", "wake.lock"):
p = WS / "tmp" / "supervise-inbox" / f
try:
st = p.stat()
_PROD_SNAP[f] = (st.st_mtime, st.st_size)
except Exception:
_PROD_SNAP[f] = None
def imp():
# 🔴 先设环境,**再加载模块** —— 否则模块会去读**生产配置**(本用例曾因此"碰生产"+结果飘)
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("cb_selftest", HERE / "collabd.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
# ── 用例(每条都对应一个真实踩过的坑) ─────────────────────────────
@case("命名:两级前缀解析(合规 / 缺级 / 接续会话 / 主控前缀)")
def t_name():
m = imp()
a = m.parse_session_name("[协作]-[手机接入]-N9 复测")
b = m.parse_session_name("[主]-[手机接入]-机制线")
c = m.parse_session_name("[协作]N9-2156") # 只一级 ⇒ 不合规
d = m.parse_session_name("接续 · 机制线(旧式)")
# 🆕 2026-10-01:**接续会话**(会话自己建出来的"下一棒",标题由建它的那条会话写)
e = m.parse_session_name("[唤醒机制] 接续 · 日志事前叫停钩子落地(第 2 棒)")
f = m.parse_session_name("接续棒:日志增长治理(任务 0 → 任务 A)")
# 🆕 **主控前缀**(实测现役主会话就这么写:中点分隔、无方括号)
g = m.parse_session_name("主控 · 协作机制 · 阈值交接收尾")
return [("合规二级=worker/手机接入", (a["role"], a["topic"], a["ok"]) == ("worker", "手机接入", True)),
("合规二级=main", (b["role"], b["topic"], b["ok"]) == ("main", "手机接入", True)),
("只一级 ⇒ 判不合规", c["ok"] is False),
("🔴 只一级 ⇒ 角色仍判 worker(旧写法剥错方括号 ⇒ 靠 `role_of` 兜底才没出事)",
c["role"] == "worker"),
("旧式接续 ⇒ 判不合规", d["ok"] is False),
("🆕 `[<类别>] 接续 · …` ⇒ **worker**,类别取方括号里那一个(⛔ 方括号里不是角色)",
(e["role"], e["topic"], e["form"]) == ("worker", "唤醒机制", "continuation")),
("🆕 无方括号 `接续棒:…` ⇒ **worker**(⛔ 不是主会话)", f["role"] == "worker"),
("🆕 `主控 · …`(无方括号)⇒ **main**(否则真主会话被判「角色未知」)",
(g["role"], g["form"]) == ("main", "main-prefix")),
("🆕 `form` 标出形态;老调用方只看 `ok` ⇒ 兼容(`ok=False` 语义未变)", a["form"] == "prefix")]
@case("命名:看板与常驻程序**同一套角色判据**(⛔ 防两处漂移 —— 漂了就没人能发现换主会话)")
def t_role_drift():
"""角色判据在 `collabd.py::parse_session_name()` 与 `board.py::_role_of_title()` **两处**
(2026-09-30 起就是"改一处必须改两处")⇒ 这里**真的把两边拉出来逐样本对账**,⛔ 不靠人盯。"""
m = imp()
import importlib.util
spec = importlib.util.spec_from_file_location("bd_selftest", HERE / "board.py")
bd = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bd)
samples = ["[协作]-[唤醒机制]-日志事前叫停棒", "[唤醒]-[唤醒机制]-脉冲", "[主]-[唤醒机制]-机制线",
"[跟进]-[会话协作自检]-队列上报跟进棒",
"[唤醒机制] 接续 · 第 2 棒", "接续棒:日志增长治理(任务 0 → 任务 A)",
"主控 · 协作机制 · 收尾", "接续 · 机制线(旧式)", "[协作]N9-2156",
# 🔴 2026-10-01 补(**第三处漂移**,扫线上标题时发现的,不是推理出来的):
# `[接续] …` 这种写法**方括号里是「接续」**(⛔ 不是类别)——
# `collabd.is_continuation()` 判 `"接续" in name`(整串)⇒ worker;
# 而 `board._role_of_title()` 原来只查**方括号后面那段** ⇒ 返回**空**。
# 后果:看板的「主会话」位上坐着一条**接续棒**(线上实测 `3aa35bae`,355 分钟没动)。
"[接续] 会话机制合并技能包(第 1 棒)",
"下载 open design", ""]
bad = [t for t in samples if m.parse_session_name(t)["role"] != bd._role_of_title(t)]
sa = (HERE / "collabd.py").read_text(encoding="utf-8")
sb = (HERE / "board.py").read_text(encoding="utf-8")
return [
("两边角色判据逐样本一致(%d 条;不一致:%s)" % (len(samples), bad or "无"), not bad),
("🔴🔴 2026-10-02 两类改造:`[唤醒]`/`[跟进]` 两边**都判空**(已退役)—— ⛔ 判成活角色 ⇒ "
"那些历史标题会被当**主会话候选**(带 `[<类别>]` 时更会变成「该类别的主会话」)⇒ 自指死结",
m.parse_session_name("[唤醒]-[X]-脉冲").get("role") == ""
and bd._role_of_title("[唤醒]-[X]-脉冲") == ""
and m.parse_session_name("[跟进]-[X]-队列上报").get("role") == ""
and bd._role_of_title("[跟进]-[X]-队列上报") == ""),
("🔴🔴 `[接续] …`(**方括号里是「接续」**)两边都判 `worker` —— ⛔ 判空 ⇒ 接续棒"
"会被当**主会话候选** ⇒ 看板「主会话」位上坐着一条干活的棒(线上实测 `3aa35bae`)",
m.parse_session_name("[接续] 会话机制合并技能包(第 1 棒)").get("role") == "worker"
and bd._role_of_title("[接续] 会话机制合并技能包(第 1 棒)") == "worker"),
("🔴 collabd 的主会话候选判据=**只排 `worker`**(2026-10-02 两类改造,`waker`/`follow` 已退役)"
"+ **退役角色闸**(2026-10-04,见下条)",
'_role not in ("worker",)' in sa and 'is_retired_role_title' in sa),
("🔴 board 同款", '_role_of_title(t) not in ("worker",)' in sb
and '_is_retired_role_title' in sb),
# 🔴🔴 2026-10-04 21:5x 新增(**用户拍板「候选一」**):退役角色**不许当主会话候选**。
# 病根:`[跟进]`/`[唤醒]` 在 2026-10-02 从映射表摘掉后 ⇒ `role=""` ⇒
# 而排除元组是 `not in ("worker",)` ⇒ **`""` 恰好放行** ⇒ 这两类**已退役的干活的棒**
# 被收进主会话候选 ⇒ 看板「主会话」位上坐着它。**实测现网**:
# `main_by_topic = {"机制排查与修复": "6ab1463e", "会话协作自检": "57f58ecf"}`
# —— 两个任务类别**全都指向 `[跟进]` 退役会话**。
# ⚠️ 修法⛔ 不是"排除空串"(空串里还有真主会话 `a80f300d`「复盘失败并避免重犯」)。
# ⚠️ 也⛔ 不是"恢复 `[跟进]`/`[唤醒]` 映射"(那等于让退役类别复活,与 10-02 口径相反)。
("🔴🔴 2026-10-04 **退役角色不许当主会话候选**(用户拍板「候选一」)—— 判据="
"「一级方括号是退役角色词」(⛔ 不是『角色解析为空』,空串里还有真主会话)",
m.is_retired_role_title("[跟进]-机制排查与修复-队列跟进")
and m.is_retired_role_title("[唤醒]-会话协作自检-脉冲")
and not m.is_retired_role_title("复盘失败并避免重犯")
and not m.is_retired_role_title("主控 · 协作机制 · 收尾")),
("🔴 退役角色闸**只认一级方括号里的整词** —— ⛔ 不许误杀「类别名里恰好含『唤醒』」的"
"在役会话(`[协作]-[唤醒机制]-…` 一级是「协作」⇒ 是 worker)",
not m.is_retired_role_title("[协作]-[唤醒机制]-日志事前叫停棒")
and not m.is_retired_role_title("[主]-[唤醒机制]-机制线")),
("🔴 两侧**共用同一实现**:`board.py` 从 `collabd.py` 取该判据(⛔ 不各写一份字面量 —— "
"这两侧已漂过三次)",
# ⚠️ ⛔ 不能用 `bd._is_retired_role_title is m.is_retired_role_title` 来判:
# 本用例把两个文件**各自按路径 exec 一遍**(各自 `importlib` 一次 collabd)
# ⇒ 拿到的是**两个模块对象** ⇒ 函数身份比较**恒 False**(实测踩到:判据没错、是写法不成立)。
# ✅ 正解①**行为逐样本一致**(真正要守的东西);②`board.py` 里是**取**不是**重定义**
# (`__module__` 指向 board 加载的那份 collabd ⇒ 证明"从模块取"这条路走通了)。
all(bd._is_retired_role_title(s) == m.is_retired_role_title(s) for s in
("[跟进]-X-队列上报", "[唤醒]-X-脉冲", "[协作]-[唤醒机制]-棒",
"复盘失败并避免重犯", "主控 · A · B", ""))
and bd._is_retired_role_title.__module__ == "_cb_shared"),
]
@case("目标:无目标 ⇒ 拒绝启动 rc=3")
def t_goal_missing():
g = TIN / "goal.json"
bak = g.read_text(encoding="utf-8") if g.exists() else None
if g.exists():
g.unlink()
p = subprocess.run([PY, "-u", str(HERE / "guard.py")], cwd=str(HERE),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
env=_mk_env(), timeout=60, errors="replace")
if bak is not None:
g.write_text(bak, encoding="utf-8")
return [("rc==3", p.returncode == 3), ("提示含『没有任务目标』", "没有任务目标" in (p.stdout or ""))]
@case("目标:有待确认 ⇒ 拒绝启动 rc=4;--propose/--confirm 能走通")
def t_goal_pending():
g, gp = TIN / "goal.json", TIN / "goal.pending.json"
bak = g.read_text(encoding="utf-8") if g.exists() else None
if g.exists():
g.unlink()
subprocess.run([PY, "-u", str(HERE / "guard.py"), "--propose", "自测目标"],
cwd=str(HERE), env=_mk_env(), timeout=60,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace",
creationflags=0x08000000)
p = subprocess.run([PY, "-u", str(HERE / "guard.py")], cwd=str(HERE),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True,
env=_mk_env(), timeout=60, errors="replace")
rc_pending = p.returncode
subprocess.run([PY, "-u", str(HERE / "guard.py"), "--confirm"], cwd=str(HERE), env=_mk_env(),
timeout=60, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, errors="replace",
creationflags=0x08000000)
ok_confirmed = g.exists() and not gp.exists()
if bak is not None:
g.write_text(bak, encoding="utf-8")
return [("有 pending ⇒ rc==4", rc_pending == 4), ("--confirm 后 goal.json 生成且 pending 清除", ok_confirmed)]
@case("🔴 `goalctl` 的**配置落点**必须跟工作区走(⛔ 不许写死技能目录)")
def t_goalctl_cfg_location():
"""🔴🔴 2026-10-06 立(vibe-product 实测踩到)。
**病根**:`goalctl.py` 原为 `CFG = HERE / "collabd.config.json"`(`HERE` = **技能包目录**)。
而定则「**技能就是技能、程序就是程序,谁用产生的文件放在他自己那里**」⇒
**技能目录里⛔ 不放生产配置** ⇒ 该文件**恒不存在** ⇒ `_load(CFG,{})` 恒 `{}`。
**症状(不报错、不崩溃,只是那一路永远读不到)**:
· `taskgraph` 取默认 `INBOX/taskgraph.json` ⇒ **改了配置不生效**
(实测:vibe-product 配置里已改指 proto-board,`goals_open()` 仍报
`bad=['任务图读不到(taskgraph.json)']` ⇒ 目标**判不出来**);
· `wake_enable`/`targets`/写回配置等**全部作用在一个不存在的文件上**。
✅ **判据**:`goalctl.CFG` 必须命中
①`COLLABD_CONFIG`(若有)或 ②`<工作区>/.workbuddy/collab/collabd.config.json`,
⛔ **不得**落在技能包目录下。
"""
import importlib.util as _ilu
rows = []
# ① 显式 env(钩子/启动器路径)
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
_s1 = _ilu.spec_from_file_location("gc_cfg1", HERE / "goalctl.py")
_m1 = _ilu.module_from_spec(_s1)
_s1.loader.exec_module(_m1)
rows.append(("给 `COLLABD_CONFIG` ⇒ 用它(实得 %s)" % _m1.CFG,
_m1.CFG == CFG))
# ② 不给 env ⇒ 必须是**工作区标准落点**(⛔ 不是技能目录)
# ⚠️ 先**真造**一份工作区标准落点的配置 —— 否则该路径不存在 ⇒ 按设计回落到技能目录
# (那是"三路都不中"的告警分支,⛔ 不是本用例要测的形状)。
_hold = os.environ.pop("COLLABD_CONFIG", None)
_std = TEST_WS / ".workbuddy" / "collab" / "collabd.config.json"
_std_bak = _std.read_text(encoding="utf-8") if _std.exists() else None
_std.parent.mkdir(parents=True, exist_ok=True)
try:
if not _std.exists():
_std.write_text(json.dumps({"workspace": str(TEST_WS)},
ensure_ascii=False), encoding="utf-8")
_s2 = _ilu.spec_from_file_location("gc_cfg2", HERE / "goalctl.py")
_m2 = _ilu.module_from_spec(_s2)
_s2.loader.exec_module(_m2)
rows.append(("不给 env ⇒ 工作区标准落点(实得 %s)" % _m2.CFG, _m2.CFG == _std))
rows.append(("🔬 反例:⛔ 不许落在技能包目录下(`HERE/collabd.config.json`)",
_m2.CFG != (HERE / "collabd.config.json")))
finally:
if _std_bak is None:
try:
_std.unlink()
except Exception:
pass
else:
_std.write_text(_std_bak, encoding="utf-8")
if _hold is not None:
os.environ["COLLABD_CONFIG"] = _hold
return rows
@case("🔴 `goalctl.goals_open()` 也必须认中文验收存值(⛔ 别只有 collabd/board 认)")
def t_goalctl_acc_chinese():
"""🔴🔴 2026-10-06 立(vibe-product 实测踩到,属"判据分叉"复发)。
**病根**:`board.py::acc_is_pass` 自称「**全库唯一实现**」,注释里点明三处
(`board.py` / `collabd.py::_acc_is_pass` / `board.html::accIsPass`)必须同款。
🔴 **实际是四处** —— `goalctl.py::goals_open()` 里那句 `str(acc[k]) != "pass"`
**被漏掉了**。而存值**真源就是中文** ⇒ 已通过的验收被计入 why ⇒ **恒判「未完成」**。
**症状**:`goalctl.py status` 报「未完成(验收 V1=过;V2=过;…)」—— **字面自相矛盾**,
而那正是判据与存值对不上的直接体现(不报错、不崩溃,只少说一句真话)。
✅ **本用例的判据能"改前报红"**:喂中文 `过`/`已过|…`/`达(…)`,
旧写法 `!= "pass"` ⇒ `goals_open()` 必为 `True`(红);修后为 `False`(绿)。
⛔ 不喂英文 `pass`(那种写法旧代码也能过 ⇒ 恒绿,等于没测)。
"""
m = imp()
# ⚠️ `imp()` 加载的是 **collabd**(它的 `goals_open()` 返回 **bool**)。
# 本用例要测的是 **goalctl** 的同名函数(返回 **dict**,含 `why`)⇒ 必须**单独加载**。
# (实测踩到:直接用 `m.goals_open()` ⇒ `TypeError: 'bool' object is not subscriptable`)
import importlib.util as _ilu
_spec = _ilu.spec_from_file_location("gc_selftest_acc", HERE / "goalctl.py")
_gc = _ilu.module_from_spec(_spec)
_spec.loader.exec_module(_gc)
# 🔴🔴 **图落点必须跟着 `CFG` 走,⛔ 不许猜**:
# · 测试配置(`imp()` 设的 `COLLABD_CONFIG`)里写的是 `"taskgraph": "tg.json"`
# ⇒ 相对落点 = `TEST_WS/tg.json`;
# · 而 `goal.json` 走 `goalctl.INBOX`(= `WS/tmp/supervise-inbox`,**硬编码**)。
# ⚠️ 两者**不是同一个目录**,实测各踩过一次:
# 写 `TEST_WS/inbox/` ⇒ `bad=['任务图读不到','无有效验收判据']`;
# 写 `TEST_WS/tg.json` 而旧版 `CFG` 读不到 ⇒ 回落到 `INBOX/taskgraph.json` 才"恰好"过。
# ⇒ 用**配置里那个值**算,⛔ 不硬编码任一侧。
_tg_rel = str((json.loads(CFG.read_text(encoding="utf-8")) if CFG.is_file() else {})
.get("taskgraph") or "")
tg = (TEST_WS / _tg_rel) if _tg_rel else (_gc.INBOX / "taskgraph.json")
tg.parent.mkdir(parents=True, exist_ok=True)
gj = _gc.INBOX / "goal.json"
gj.parent.mkdir(parents=True, exist_ok=True)
bak_g = gj.read_text(encoding="utf-8") if gj.exists() else None
tg_bak = tg.read_text(encoding="utf-8") if tg.exists() else None
tk_bak = tasks()
try:
# 前两路(台账/任务图)清成全 done ⇒ **只剩"验收那一路"能说话**
put_tasks({})
tg.write_text(json.dumps({"nodes": [
{"id": "T1", "title": "测试件-1", "status": "done", "line": "line-a"}]},
ensure_ascii=False), encoding="utf-8")
def _o(acc):
gj.write_text(json.dumps({"acceptance_state": acc}, ensure_ascii=False),
encoding="utf-8")
return _gc.goals_open()
# ── 真源的中文写法(全部应判"过" ⇒ goals_open 的 why 为空)
_cn = [("裸「过」", {"V1": "过"}),
("「过|…」带竖线后缀", {"V1": "过|pid 8024 活(13:30:55)"}),
("🆕 副词:「已过|…」", {"V1": "已过|四条全在"}),
("🆕 副词:「均已通过」", {"V1": "均已通过"}),
("🆕 「达(…)」", {"V1": "达(1440 与 390 两视口都有)"}),
("四条中文全过", {"V1": "过", "V2": "已过|x", "V3": "达(y)", "V4": "均完成"})]
# ── 必须仍判"不过"的(fail-closed 不破;⛔ 这是防"改成恒 True"的变异对照)
# 🔴 **emoji 前缀是刻意的 fail-closed**(2026-10-06 核实):本项目约定
# `🔴`/`⛔` = **表有问题**的标记,所以真源里**否定值才带 emoji**
# (例 `🔴 不过|…`),肯定值一律写 `过|…`/`pass`。
# ⇒ `🔴 已过` 判 False 是**当前三处一致的正确行为**,⛔ 不是缺陷;
# 本用例把它钉成 fail-closed 期望,防有人"顺手"改成 lstrip 掉任意非字母。
_no = [("「未过」", {"V1": "未过"}),
("「不过」", {"V1": "\U0001f534 不过|没做"}),
("「待重验」", {"V1": "待重验(等复跑)"}),
("「未达」", {"V1": "未达(缺两视口)"}),
("空值", {"V1": ""}),
("英文 fail", {"V1": "fail"}),
("🔴 emoji+肯定词 ⇒ ⛔ 仍判非过(约定:emoji=表有问题)",
{"V1": "\U0001f534 已过|四条全在"})]
rows, o_cn, o_no = [], [], []
for lab, acc in _cn:
o = _o(acc)
o_cn.append(o["open"])
rows.append(("中文「%s」⇒ ⛔ 不判未完成(why=%s)" % (lab, o["why"][:2]), o["open"] is False))
for lab, acc in _no:
o = _o(acc)
o_no.append(o["open"])
rows.append(("「%s」⇒ 仍判未完成(fail-closed)" % lab, o["open"] is True))
finally:
if tg_bak is not None:
tg.write_text(tg_bak, encoding="utf-8")
else:
try:
tg.unlink()
except Exception:
pass
put_tasks(tk_bak)
if bak_g is not None:
gj.write_text(bak_g, encoding="utf-8")
else:
try:
gj.unlink()
except Exception:
pass
# ── 变异对照:两边**必须同时出现**(全 True 或全 False ⇒ 判据是恒的,没测出东西)
rows.append(("🔬 变异对照:中文过 ⇒ 全 False,非过 ⇒ 全 True(⛔ 防恒绿/恒红)",
(not any(o_cn)) and all(o_no)))
return rows
@case("目标三态:『没有判据』⛔ 不等于『全过』(防『读不到被当成已完成』)")
def t_goal_undeclared():
"""🔴 2026-09-30 实测踩到(**跑整体验证时发现**):`acceptance_state` 里**只有 `_说明`/`_更新`
两个说明行**时,旧写法 `if not acc`(判**字典空不空**)**永不触发** ⇒ `goalctl status` 显示
「三路全过」。形状同族:**不崩溃,只是少说一句话**。
⇒ 判据改成「**有没有有效项**」(键不以 `_` 开头),并把状态分成**三态**:
`open`(有未过项)/`pass`(有判据且全过)/`undeclared`(一条有效判据都没有 ⇒ ⛔ 不算过)。
"""
m = imp()
gj, tg = TIN / "goal.json", TEST_WS / "tg.json"
bak_g = gj.read_text(encoding="utf-8") if gj.exists() else None
tg_bak, tk_bak = tg.read_text(encoding="utf-8"), tasks()
try:
# 把台账清空 + 任务图全 done ⇒ **只剩"验收那一路"能说话**(否则前两路恒为 open,测不出区别)
put_tasks({})
tg.write_text(json.dumps({"nodes": [
{"id": "T1", "title": "测试件-1", "status": "done", "line": "line-a"},
{"id": "T2", "title": "测试件-2", "status": "done", "line": "line-b"}]},
ensure_ascii=False), encoding="utf-8")
gj.write_text(json.dumps({"acceptance_state": {"_说明": "x", "_更新": "y"}},
ensure_ascii=False), encoding="utf-8")
s1, o1, r1 = m.goal_state(), m.goals_open(), m._acc_short()[0]
gj.write_text(json.dumps({"acceptance_state": {"V1": "pass"}}, ensure_ascii=False), encoding="utf-8")
s2, o2 = m.goal_state(), m.goals_open()
gj.write_text(json.dumps({"acceptance_state": {"V1": "fail"}}, ensure_ascii=False), encoding="utf-8")
s3, o3 = m.goal_state(), m.goals_open()
finally:
tg.write_text(tg_bak, encoding="utf-8")
put_tasks(tk_bak)
if bak_g is not None:
gj.write_text(bak_g, encoding="utf-8")
else:
try:
gj.unlink()
except Exception:
pass
return [("只有说明行 ⇒ `undeclared`(实得 %s)" % s1, s1 == "undeclared"),
("…且 `goals_open() is True`(⛔ 不判完成)", o1 is True),
("…且 `_acc_short` 明说『没有有效判据』(%s)" % r1[:38], "有效判据" in r1),
("有判据且全过 ⇒ `pass`(实得 %s)" % s2, s2 == "pass"),
("…且 `goals_open() is False`", o2 is False),
("有判据且未过 ⇒ `open`(实得 %s)" % s3, s3 == "open"),
("…且 `goals_open() is True`", o3 is True)]
@case("验收判据认**真源的中文写法**(⛔ 别只喂英文 `pass` —— 那等于判据恒绿)")
def t_acc_is_pass_chinese():
"""🔴🔴 2026-10-03 实测坐实(同族第三发:`pitfalls.md` P0-13/P0-20)。
**上一条用例(`t_goal_undeclared`)只喂英文 `{"V1": "pass"}`** ⇒ 判据**恒命中** ⇒ **永远绿**。
而**真源写的全是中文**:`goal.json::acceptance_state` 的值形如
`过|pid 8024 活(…)` / `🔴 不过|…` / `**复核:过(形态已治本…)**`。
⇒ `board.py` 10-02 已修(认中文)但 **`collabd.py` 的 `goal_state()`/`_acc_short()` 没跟修**
⇒ 现算坐实 **7 条有效判据里 5 条两判据相反** ⇒ `goal_state()` 恒判 `open`
⇒ `--once` 的 `if not goals_open()` 走 `paused_round(why="目标三路全过")`
⇒ **即使真完成也会被说成"已完成"**。
✅ **本用例的判据是"能改前报红"的那种**:先验**英文**(旧写法也能过 ⇒ 证明不是恒真),
再验**中文三种真源写法**(旧写法会红 ⇒ 证明真能抓)。
"""
m = imp()
rows = []
# ── 层 1:`_acc_is_pass()` 本身逐写法(⛔ 判据主体,⛔ 不依赖任何文件状态)
for label, val, want in [
("英文 pass", "pass", True),
("英文 PASS", "PASS", True),
("中文「过」", "过", True),
("中文「过|…」带竖线后缀", "过|pid 8024 活(13:30:55)", True),
("🔴 2026-10-03 实测补:竖线右边**带 json 串自带冒号**(V4 真实值)",
'过|board.py --serve 8788 起得来,body={"ok":true,"err":""};探针结束后端口已释放', True),
("中文「通过」", "通过", True),
("🆕 2026-10-04 实测补:中文「达」(vibe-product 真源 14 条主用写法!)",
"达(1440 与 390 两视口都有)", True),
("🆕 2026-10-04:「达」裸写法", "达", True),
("🆕 2026-10-04:「达标」", "达标(零残留)", True),
("🆕 2026-10-04:「合格」", "合格(复核通过)", True),
("🆕 2026-10-04:「完成」", "完成(已实测)", True),
# 🔴🔴 2026-10-05 **第 5 次复发**:判定词前带**表完成的副词**(`已`/`均`/`都`)。
# 实测(vibe-product 页签):9 条真值**全写 `已过|…`**,看板显示 **0 / 9 通过**
# —— 因为 `已过` 第一个字是 `已`、⛔ 不是 `过`。用户原话:
# 「完成情况 0 / 9 通过,但是我看到都显示已过呀」。
# 前四次都在**补词** ⇒ 这次改成**剥副词再比**(`ACC_ADV_PREFIX`)。
("🆕 2026-10-05:「已过」⇒ 必须判过(本轮报障真值)",
"已过|clipper 扩展已装入浏览器并可连 daemon", True),
("🆕 2026-10-05:「已过(本类别)|…」", "已过(本类别)|会话协作自检:唤醒 9", True),
("🆕 2026-10-05:「已通过」", "已通过|x", True),
("🆕 2026-10-05:「已完成」", "已完成|y", True),
("🆕 2026-10-05:「均已通过」⇒ **两级副词**都要剥", "均已通过(实测)", True),
("🆕 2026-10-05:「都已达标」⇒ 两级副词", "都已达标|z", True),
("🆕 2026-10-05:「已`*`达」⇒ 剥副词后再去星号", "已 * 达(两视口)", True),
("🔴 2026-10-05:`未过` ⇒ ⛔ 剥不动、必须判非过(fail-closed 不许破)",
"未过|还没测", False),
("🔴 2026-10-05:`已未过` ⇒ ⛔ 剥掉 `已` 还剩 `未过` ⇒ 仍非过", "已未过|x", False),
("🔴 2026-10-05:`已不过` ⇒ ⛔ 剥掉 `已` 还剩 `不过` ⇒ 仍非过", "已不过|x", False),
("🔴 2026-10-05:`非pass` ⇒ ⛔ 非过", "非pass|x", False),
("🆕 2026-10-04:白名单词前带 `*` 强调符", "* 达(两视口)", True),
("🆕 2026-10-04:「未达」⇒ ⛔ 不许因含『达』判过", "未达(还差 2 条)", False),
("🆕 2026-10-04:「待达标」⇒ ⛔ 不许判过", "待达标", False),
("「复核:过」判定词不在开头", "**复核:过(形态已治本)**", True),
("中文「不过|…」⇒ 未过", "🔴 不过|零条活着", False),
("「待重验」⇒ 未过", "待重验(判据随口径变更作废)", False),
("英文 fail", "fail", False),
("空值 ⇒ 判不出来 ⇒ 未过", "", False),
]:
got = m._acc_is_pass(val)
rows.append(("_acc_is_pass(%s) = %s(应 %s)" % (label, got, want), got is want))
# ── 层 2:端到端(喂中文进真源 ⇒ `goal_state()` 必须是 `pass`,⛔ 不是 `open`)
gj, tg = TIN / "goal.json", TEST_WS / "tg.json"
bak_g = gj.read_text(encoding="utf-8") if gj.exists() else None
tg_bak, tk_bak = tg.read_text(encoding="utf-8"), tasks()
try:
put_tasks({}) # 前两路清空 ⇒ 只剩验收那一路能说话
tg.write_text(json.dumps({"nodes": [
{"id": "T1", "title": "测试件-1", "status": "done", "line": "line-a"}]},
ensure_ascii=False), encoding="utf-8")
gj.write_text(json.dumps({"acceptance_state": {
"V1-中文写法": "过|pid 1234 活,心跳 8 秒前",
"V2-带括注": "过(复核:已实测)",
"_说明": "这行不参与判定"}}, ensure_ascii=False), encoding="utf-8")
s_cn, short_cn = m.goal_state(), m._acc_short()
gj.write_text(json.dumps({"acceptance_state": {
"V1-真不过": "🔴 不过|零条活着"}}, ensure_ascii=False), encoding="utf-8")
s_bad = m.goal_state()
finally:
tg.write_text(tg_bak, encoding="utf-8")
put_tasks(tk_bak)
if bak_g is not None:
gj.write_text(bak_g, encoding="utf-8")
else:
try:
gj.unlink()
except Exception:
pass
rows.append(("真源全中文且全过 ⇒ `goal_state()` = pass(实得 %s)" % s_cn, s_cn == "pass"))
rows.append(("…且 `_acc_short` 报『全部 pass』(%s)" % short_cn[0][:34],
"全部 pass" in short_cn[0]))
rows.append(("真源有一条中文「不过」⇒ `open`(实得 %s)" % s_bad, s_bad == "open"))
# ── 层 3:🔴🔴 **三处实现必须同款**(2026-10-05 加 —— 这是防"漂"的真判据)
# 技能文档明令:`board.py` / `collabd.py` / `board.html` 三处判据必须逐字同款
# (**已漂过 4 次**,每次都只修一处 ⇒ 前四次修完照样再栽)。
# ⚠️ 判据要**能改前报红**:撤掉任一处的新逻辑 ⇒ 本条必须红。
# ✅ 做法=**抽三处的"判定词表 + 副词表",逐字比对**(⛔ 不靠"我记得三处一样")。
_W_WANT = ("pass", "过", "通过", "达", "达标", "合格", "完成")
_A_WANT = ("已", "已经", "均", "都", "经复核", "复核后")
# ① board.py —— 🔴🔴 **唯一真身**(`collabd.py::_acc_is_pass` 是转发,⛔ 不再是第二份实现)。
# ⚠️ 2026-10-05 修:此处原写 `m.ACC_PASS_WORDS`,而 `m` = **collabd 模块**
# (`imp()` 把 collabd.py 装成 `cb_selftest`)⇒ `AttributeError` ⇒ **本用例整条红**。
# 真因=**拿错了"真身"**:判定词表住在 `board.py`,collabd 只是 import 它。
# ⇒ 判据必须**去 board.py 里读**(这才是"三处同款"要守的那个源)。
_rows_ok = []
_bp = HERE / "board.py"
if not _bp.is_file():
_bp = HERE / "scripts" / "board.py"
try:
_bs = _bp.read_text(encoding="utf-8")
_bw = re.search(r"^ACC_PASS_WORDS\s*=\s*\(([^)]*)\)", _bs, re.M)
_ba = re.search(r"^ACC_ADV_PREFIX\s*=\s*\(([^)]*)\)", _bs, re.M)
_tw = tuple(re.findall(r'"([^"]*)"', _bw.group(1))) if _bw else ()
_ta = tuple(re.findall(r'"([^"]*)"', _ba.group(1))) if _ba else ()
_rows_ok.append(("① `board.py::ACC_PASS_WORDS` 逐字同款", _tw == _W_WANT))
_rows_ok.append(("① `board.py::ACC_ADV_PREFIX` 逐字同款", _ta == _A_WANT))
# 🔴 `board.py` 也必须真含"剥副词再比"那段(⛔ 只对表不够)
_rows_ok.append(("① `board.py::acc_is_pass` 真含「剥副词再比」逻辑",
"ACC_ADV_PREFIX" in _bs and "len(_cut)" in _bs))
except Exception as _e:
_rows_ok.append(("① `board.py` 读取失败 %r" % (_e,), False))
# ② board.html 的 ACC_PASS_WORDS / ACC_ADV_PREFIX(抽源码)
# ⚠️ 2026-10-05 修:`HERE` = **本脚本所在目录(scripts/)**,
# 而 `board.html` 住在**技能根**下的 `assets/`(= `HERE.parent/assets`)
# ⇒ 原写 `HERE/"assets"` 必 `FileNotFoundError`(本轮实测就是这么红的)。
_html = HERE.parent / "assets" / "board.html"
if not _html.is_file():
_html = HERE / "assets" / "board.html"
try:
_hs = _html.read_text(encoding="utf-8")
_mw = re.search(r"var\s+ACC_PASS_WORDS\s*=\s*(\[[^\]]*\])", _hs)
_ma = re.search(r"var\s+ACC_ADV_PREFIX\s*=\s*(\[[^\]]*\])", _hs)
# ⚠️ JS 数组用**单引号**(`['pass','过']`)⇒ ⛔ `json.loads` 会 `JSONDecodeError`
# (本轮实测就是这么红的)⇒ 统一**按引号抽词**(双引号/单引号都认)。
_jw = tuple(re.findall(r"""['"]([^'"]*)['"]""", _mw.group(1))) if _mw else ()
_ja = tuple(re.findall(r"""['"]([^'"]*)['"]""", _ma.group(1))) if _ma else ()
_rows_ok.append(("② `board.html::ACC_PASS_WORDS` 逐字同款", _jw == _W_WANT))
_rows_ok.append(("② `board.html::ACC_ADV_PREFIX` 逐字同款", _ja == _A_WANT))
# 🔴 前端 HTML **必须**出现"剥副词"那段逻辑(⛔ 只对表不够:改了表但没改算法也白搭)
_rows_ok.append(("② `board.html::accIsPass` 真含「剥副词再比」逻辑",
"ACC_ADV_PREFIX" in _hs and "slice(cut.length)" in _hs))
except Exception as _e:
_rows_ok.append(("② `board.html` 读取失败 %r" % (_e,), False))
# ③ collabd.py 的**兜底白名单**(转发路径自动跟 board.py;兜底必须同款)
_cb = HERE / "scripts" / "collabd.py"
if not _cb.is_file():
_cb = HERE / "collabd.py"
try:
_cs = _cb.read_text(encoding="utf-8")
_i = _cs.index("def _acc_is_pass")
_j = _cs.index("\n# 🔴🔴 2026-10-04 加", _i)
_body = _cs[_i:_j]
_rows_ok.append(("③ `collabd.py::_acc_is_pass` 转发 `board.acc_is_pass`",
"board" in _body and "acc_is_pass(" in _body))
_rows_ok.append(("③ `collabd.py` 兜底含「剥副词再比」逻辑",
'"已"' in _body and '"已", "已经"' in _body))
except Exception as _e:
_rows_ok.append(("③ `collabd.py` 解析失败 %r" % (_e,), False))
rows.extend(_rows_ok)
return rows
@case("两类检查会话是**两件事**:prompt 必须不同 + 闸④「本项目无待执行排期」")
def t_two_kinds_of_check_agent():
"""🔴🔴 2026-10-03 08:0x 用户明令:「**这里面是两件事 提示词应该不一样**」。
用户原话两条:
1、会话全结束(**还要加上 定时任务中没有本项目待执行的任务**)且**队列不为空** ⇒
**结果检查会话**去跟进任务会话执行结果,判断是否创建对应任务会话继续完成目标;
2、会话全结束(同上)且**队列为空 >20 分钟** ⇒ **目标检查会话**去核对目标完成状态,
判断是建任务会话还是**修改目标状态**,并执行。
⇒ 改前**两套 prompt 完全共用一个模板** ⇒ 结果检查会话拿到了"改目标状态"的出口(越权),
目标检查会话拿到的是"看队列 head"的开场(串味)。本用例钉死"两套必须不同"。
✅ 判据**能改前报红**:把 `_check_prompt` 改成无视 `reason` 恒返回同一份 ⇒ 本条必红。
"""
import re as _re
m = imp()
rows = []
pr = m._check_prompt(1, "自检目标", "sessions-ended")
pg = m._check_prompt(1, "自检目标", "queue-empty")
rows.append(("两套 prompt 渲染都不炸(各 %d/%d 字符)" % (len(pr), len(pg)), True))
rows.append(("⛔ 两套 prompt **不是同一份**(实得 %s)" % (pr == pg), pr != pg))
rows.append(("结果检查:标题是「结果检查会话」(%s)", "【结果检查会话" in pr))
rows.append(("目标检查:标题是「目标检查会话」(%s)", "【目标检查会话" in pg))
# 职责边界:结果检查 ⛔ 不许有改目标状态的出口
rows.append(("结果检查 ⛔ 不含「改目标状态」的出口", "没有「改目标状态」" in pr))
rows.append(("⛔ 结果检查里不该出现 `--set-life`", "--set-life" not in pr))
# 目标检查必须有改状态的出口
rows.append(("目标检查含 `--set-life 已完成` 出口", "--set-life 已完成" in pg))
rows.append(("目标检查讲了三路完成度(含 `acceptance_state`)", "acceptance_state" in pg))
rows.append(("目标检查有「不确定就不改」的 fail-safe",
"不确定就不改" in pg))
# 排期名分类
rows.append(("排期名分类:结果检查 → [%s]" % m.CHECK_KINDS["sessions-ended"][1],
m.CHECK_KINDS["sessions-ended"][1] == "结果检查"))
rows.append(("排期名分类:目标检查 → [%s]" % m.CHECK_KINDS["queue-empty"][1],
m.CHECK_KINDS["queue-empty"][1] == "目标检查"))
# 闸④
rows.append(("闸④ 函数存在:`ws_pending_schedules()`", callable(m.ws_pending_schedules)))
ps = m.ws_pending_schedules(exclude="__不存在的排期名__")
rows.append(("闸④ 现网能读出本工作区待执行排期(%d 条,测试工作区应为 0)" % len(ps),
isinstance(ps, list)))
# ⚠️ 判据必须读**函数体**而不是 docstring —— docstring 里写"fail-safe"只是作者的说明,
# ⛔ 拿它当判据=判"作者记得写这四个字"(同 P0-36 的教训:别拿字面量当事实)。
# 真判据=**读不到库时返回非空** ⇒ 只能靠"把 RO 连接打坏"来验,见下面那条。
rows.append(("闸④ 判据注释里写明 fail-safe 方向=不建",
"fail-safe" in (m.ws_pending_schedules.__doc__ or "")))
# 真判据:临时把只读连接打坏 ⇒ 必须返回**非空**(当"有排期"⇒ 闸拦 ⇒ 不建)
_orig = m._ro_conn
try:
def _boom():
raise RuntimeError("自测:故意让读库失败")
m._ro_conn = _boom
broken = m.ws_pending_schedules()
finally:
m._ro_conn = _orig
rows.append(("🔴 闸④ 读库失败时返回**非空**(fail-safe=当有排期⇒不建),实得 %r"
% (broken[:1],), bool(broken)))
rows.append(("🔴 闸④ 读库失败时**不**返回空列表(否则闸会放行)", broken != []))
# 非法 reason 必须被拒
rows.append(("非法 reason 不在 CHECK_KINDS(%s)", "瞎写的" not in m.CHECK_KINDS))
# 旧名必须已废(⛔ 静默变成"什么都给"更坏)
try:
m.CHECK_PROMPT(1, "x")
rows.append(("旧名 `CHECK_PROMPT` 已废(会抛)", False))
except RuntimeError:
rows.append(("旧名 `CHECK_PROMPT` 已废(会抛)", True))
return rows
@case("🔴🔴 闸⑥ 同类检查冷却期:两种情况 × 每类最多一条 ⇒ 不堆积(用户「不该有任何堆积」)")
def t_check_cooldown_no_pileup():
"""🔴 2026-10-05 立(用户口径逐字:「只有两种情况会创建 定时任务创建会话,不应该又任何堆积」)。
⛔ **病根**(实测,见 `references/pitfalls.md` P0-69):排期名带递增棒次(`第%d棒`)
⇒ 闸⑤「同名去抖」**永不命中** ⇒ 同一种情况可被反复建 ⇒ 自测区 2 小时堆 11 条。
🔴 **判据必须能「改前报红」**(P0-13 同族):
· 删掉闸⑥ 那段 ⇒ ②③④ 全红;
· 把 `_stamp.update(...)` 改回整体覆盖 ⇒ ③ 红(另一类的冷却记忆被抹);
· 把 `CHECK_COOLDOWN_S` 改成 0 ⇒ ① 红。
"""
import inspect as _ins
m = imp()
src = _ins.getsource(m.maybe_spawn_check_agent)
cd = getattr(m, "CHECK_COOLDOWN_S", None)
# ① 冷却常量在位且是「真窗口」(⛔ 0 / None 等于没冷却)
ok_const = isinstance(cd, (int, float)) and cd >= 600
# ② 闸⑥ 在:按 reason 记冷却键(⛔ 只认带引号的字符串拼装,别认注释里的裸串)
ok_key = ('"last_" + reason' in src) or ("'last_' + reason" in src)
# ③ 合并写:⛔ 不许整体覆盖 stamp(覆盖 ⇒ 另一类的 last_* 被抹 ⇒ 闸⑥ 对另一情况失效)
ok_merge = "_stamp.update(" in src
# ④ 顺序:闸⑥ 必须在闸⑤(同名去抖)**之后**(⛔ 顺序颠倒 ⇒ 语义变成"先拦粗的")
p5 = src.find("active_schedule_exists(name)")
p6 = src.find('"last_" + reason')
if p6 < 0:
p6 = src.find("'last_' + reason")
ok_order = p5 >= 0 and p6 >= 0 and p5 < p6
return [("冷却窗口 CHECK_COOLDOWN_S 在位且 ≥10 分钟(实测=%s)" % cd, ok_const),
("闸⑥ 按 reason 记冷却键(同名去抖失效时的兜底)", ok_key),
("stamp 合并写(⛔ 整体覆盖会抹掉另一类的冷却记忆)", ok_merge),
("闸⑥ 排在闸⑤ 之后(先精确后兜底)", ok_order)]
@case("🔴🔴 闸⑥ 同类检查冷却期:两种情况 × 每类最多一条 ⇒ 不堆积(用户「不该有任何堆积」)")
def t_check_cooldown_no_pileup():
"""🔴 2026-10-05 立(用户口径逐字:「只有两种情况会创建 定时任务创建会话,不应该又任何堆积」)。
⛔ **病根**(实测,见 `references/pitfalls.md` P0-69):排期名带递增棒次(`第%d棒`)
⇒ 闸⑤「同名去抖」**永不命中** ⇒ 同一种情况可被反复建 ⇒ 自测区 2 小时堆 11 条。
🔴 **判据必须能「改前报红」**(P0-13 同族):
· 删掉闸⑥ 那段 ⇒ ②③④ 全红;
· 把 `_stamp.update(...)` 改回整体覆盖 ⇒ ③ 红(另一类的冷却记忆被抹);
· 把 `CHECK_COOLDOWN_S` 改成 0 ⇒ ① 红。
"""
import inspect as _ins
m = imp()
src = _ins.getsource(m.maybe_spawn_check_agent)
cd = getattr(m, "CHECK_COOLDOWN_S", None)
# ① 冷却常量在位且是「真窗口」(⛔ 0 / None 等于没冷却)
ok_const = isinstance(cd, (int, float)) and cd >= 600
# ② 闸⑥ 在:按 reason 记冷却键(⛔ 只认带引号的字符串拼装,别认注释里的裸串)
ok_key = ('"last_" + reason' in src) or ("'last_' + reason" in src)
# ③ 合并写:⛔ 不许整体覆盖 stamp(覆盖 ⇒ 另一类的 last_* 被抹 ⇒ 闸⑥ 对另一情况失效)
ok_merge = "_stamp.update(" in src
# ④ 顺序:闸⑥ 必须在闸⑤(同名去抖)**之后**(⛔ 顺序颠倒 ⇒ 语义变成"先拦粗的")
p5 = src.find("active_schedule_exists(name)")
p6 = src.find('"last_" + reason')
if p6 < 0:
p6 = src.find("'last_' + reason")
ok_order = p5 >= 0 and p6 >= 0 and p5 < p6
return [("冷却窗口 CHECK_COOLDOWN_S 在位且 ≥10 分钟(实测=%s)" % cd, ok_const),
("闸⑥ 按 reason 记冷却键(同名去抖失效时的兜底)", ok_key),
("stamp 合并写(⛔ 整体覆盖会抹掉另一类的冷却记忆)", ok_merge),
("闸⑥ 排在闸⑤ 之后(先精确后兜底)", ok_order)]
@case("检查会话的 prompt:**引用的命令必须真存在**(⛔ 引用不存在的命令 ⇒ 静默当成功)")
def t_check_prompt_cmds_exist():
"""🔴🔴 2026-10-03 实测坐实:`CHECK_PROMPT` 让检查会话用
`goalctl.py set-life已完成` 改生命周期 —— ⛔ **该命令在 `goalctl.py` 里根本不存在**。
实跑后果:只打印用法、**rc=0 静默放行** ⇒ 检查会话**以为改成功了**,实际一个字没改。
✅ 真源=`collabd.py --set-life`(`L4200` 的 CLI 分支 ⇒ `goal_life_set()`)。
✅ 本用例判据**能改前报红**:占位符个数对不上会当场 `TypeError`;
prompt 里仍写 `goalctl.py set-life` 则本条**必红**。
"""
import re as _re
m = imp()
rows = []
# 🔴 2026-10-03 08:0x:`_check_prompt` 已拆两套(`reason` 必传)⇒ **两套都要验**,
# ⛔ 只验默认那套=漏掉"结果检查里不该有 --set-life"这类越权。
for reason, want_life in (("queue-empty", True), ("sessions-ended", False)):
tag = "目标检查" if want_life else "结果检查"
try:
p = m._check_prompt(1, "自检目标", reason)
ok = True
except TypeError as e:
p, ok = "", False
rows.append(("%s:`_check_prompt()` 渲染不炸(实得 %s)"
% (tag, e.__class__.__name__), False))
if not ok:
continue
rows.append(("%s:渲染成功,无残留占位符(实得 %s)"
% (tag, _re.findall(r"%[sd]", p)), not _re.findall(r"%[sd]", p)))
rows.append(("%s:坏引用 `用...goalctl.py set-life` 不在正文里" % tag,
not _re.search(r"用`[^`]*goalctl\.py set-life", p)))
if want_life:
rows.append(("目标检查:指向 `collabd.py --set-life`(⛔ 不是不存在的命令)",
"--set-life" in p and "collabd.py" in p))
rows.append(("目标检查:`--set-life` 与值之间有空格",
"--set-life 已完成" in p))
else:
rows.append(("结果检查:⛔ 不含 `--set-life`(那是目标检查的出口)",
"--set-life" not in p))
return rows
@case("台账四态:running → blocked(带原因) → 解除后原因清掉")
def t_states():
run_cli(["--report", "C1", "--state", "running", "--by", "t", "--line", "line-a"])
ok1 = (tasks().get("C1") or {}).get("state") == "running"
run_cli(["--report", "C1", "--state", "blocked", "--by", "t", "--reason", "缺一份对接单"])
v = tasks().get("C1") or {}
ok2 = v.get("state") == "blocked" and "对接单" in str(v.get("block_reason"))
run_cli(["--report", "C1", "--state", "pending", "--by", "t"])
v = tasks().get("C1") or {}
ok3 = v.get("state") == "pending" and not v.get("block_reason")
t = tasks(); t.pop("C1", None); (TIN / "tasks.json").write_text(json.dumps(t, ensure_ascii=False, indent=2), encoding="utf-8")
return [("上报 running 生效", ok1), ("blocked 带原因", ok2), ("解除后原因被清掉", ok3)]
@case("已停总闸:goal.run=paused ⇒ --once 只写一行、⛔ 不产告警(防『人为停掉被读成机制坏了』)")
def t_goal_paused():
"""🔴 2026-10-01 体检修实测:**暂停中的需求被当成"机制故障"** 报给用户。
真因:`goal.json.run=paused` 时 `collabd.py` **零处**引用 `run` ⇒ `--once` 照跑、照写
`NEXT.md`/`STALL.md`/`NEED-USER.md`/`digest.md`,而 `digest.md` 经钩子**注入每个会话每一轮**。
⇒ 本用例钉死两件事:① 已停时**只留一行"已停"** 并把告警投影清掉 ② 已停时**不产任何唤醒/排期**。
🔴🔴 2026-10-05 `--tick` **已删**(用户:「检查程序 常驻 自己不会判断吗,非要什么 tick once
去触发?」)⇒ 原先「已停时 `--tick` ⛔ 不投递」这条断言**失去对象**(分支不存在了)。
⛔ 替换判据=**已停时 `--once` 不产唤醒**(这才是那条红线现在仅存的载体),
+**钉死 `--tick` 确已删除**(⛔ 防止有人把它加回来却不补这条闸)。
"""
gp = TIN / "goal.json"
_had = gp.exists()
_old = gp.read_text(encoding="utf-8") if _had else ""
try:
# 埋脏投影:已停时它们**必须被清掉**(⛔ 留着就被钩子继续注入)
for _n in ("NEXT.md", "STALL.md", "NEED-USER.md", "queue.md"):
(TIN / _n).write_text("(selftest 脏投影)", encoding="utf-8")
gp.write_text(json.dumps({"run": "paused", "acceptance_state": {}}, ensure_ascii=False),
encoding="utf-8")
w0 = wakeups_n()
rc1, out1 = run_cli(["--once"])
_txt = ""
try:
_txt = (TIN / "digest.md").read_text(encoding="utf-8")
except Exception:
pass
ok_line = ("已停" in _txt) and len([_l for _l in _txt.splitlines() if _l.strip()]) <= 6
ok_clear = all(not (TIN / _n).exists() for _n in ("NEXT.md", "STALL.md", "NEED-USER.md", "queue.md"))
# 🔴🔴 2026-10-05:`--tick` 已删 ⇒ 改成两件事 ——
# ① 已停时 `--once` 不产唤醒(红线载体只剩它) ② `--tick` 确已从参数表与代码里消失
rc2, out2 = run_cli(["--once"])
ok_no_wake = wakeups_n() == w0
_src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
_seg = _src.split("_KNOWN = {")[1][:400] if "_KNOWN = {" in _src else ""
ok_tick = ('if "--tick" in sys.argv' not in _src) and ('"--tick"' not in _seg)
# 恢复 active ⇒ 必须**退出已停态**(`paused` 标记被清),⛔ 不许一次暂停就永久停摆
gp.write_text(json.dumps({"run": "active", "acceptance_state": {}}, ensure_ascii=False),
encoding="utf-8")
rc3, _ = run_cli(["--once"])
ok_resume = (not state().get("paused")) and rc3 == 0
return [("已停时 --once rc==0", rc1 == 0),
("digest 只有一行『已停』(⛔ 不产告警)", ok_line),
("告警投影被清掉(NEXT/STALL/NEED-USER/queue)", ok_clear),
("已停时 ⛔ 不产唤醒(%d→%d)" % (w0, wakeups_n()), ok_no_wake and rc2 == 0),
("🔴 `--tick` 确已删除(参数表与分支都不在位)", ok_tick),
("恢复 active ⇒ 退出已停态", ok_resume)]
finally:
if _had:
gp.write_text(_old, encoding="utf-8")
else:
try:
gp.unlink()
except Exception:
pass
@case("投递:常驻程序(--once / --report)**零投递**")
def t_no_deliver():
w0 = wakeups_n()
run_cli(["--once"])
run_cli(["--report", "C2", "--state", "running", "--by", "t"])
w1 = wakeups_n()
t = tasks(); t.pop("C2", None); (TIN / "tasks.json").write_text(json.dumps(t, ensure_ascii=False, indent=2), encoding="utf-8")
return [("--once 与 --report 合计零投递(%d→%d)" % (w0, w1), w1 == w0)]
@case("投递:目标会话(**跟进会话**)在跑 ⇒ 必须延后(target-busy)")
def t_target_busy():
"""🔴🔴 2026-10-02 23:4x **本用例已随「跟进会话整套退役」失效**(⛔ 保留代码,⛔ 删了=没有用例=假绿)。
🔴 **为什么整组退役**(不是改断言、不是修代码):用户逐字「按照新的逻辑整体修改」/
「按照之前的讨论 唤醒会话 跟进会话 和 上报程序 都去掉才对」/「上报机制也不需要了」。
⇒ `follow_for_topic()` 已**短路**返回 `why="follow-retired"` ⇒ 这条路径**根本走不到**:
`target-busy`/`target-deaf`/「真投出」/「台账增条」这四组断言**永久拿不到**那个状态。
⚠️ ⛔ **不许把这四条改成"预期 follow-retired"** —— 那等于**把断言改恒真**(P0-13 同族):
改完之后任何实现都能过,等于没有守卫。
✅ **正确做法**:整组标记退役,⛔ 不动断言(将来口径变回时可直接复活)。
⚠️ 退役判据(机读,⛔ 不看代码注释):`follow_for_topic()` **真跑一次**返回 `why == "follow-retired"`。
—— 本用例**后半段(真调用)已改判这条**;前半段的旧断言**刻意留着不改**。
"""
m = imp()
# ✅ 退役判据(真调用,⛔ 不是 grep 源码)
r = m.follow_for_topic({}, "任意类别", [])
if r.get("why") != "follow-retired" or str(r.get("sid") or ""):
return [("🔴 两类改造:`follow_for_topic()` 必须短路成 `follow-retired` 且 `sid` 为空"
"(现 = %s / sid=%r)" % (r.get("why"), r.get("sid")), False)]
return [("✅ 两类改造:`follow_for_topic()` 短路成 `follow-retired`(唤醒/跟进整套退役)", True),
("⏸ 下面 2 条为**已退役**的旧断言(target-busy 的 ① 恒真 / ② 才有分量),"
"⛔ 刻意不改 —— 路径已死,改成新预期=断言恒真。判据见下方源码注释", True)]
# ═════════════ 以下=旧实现,⛔ 刻意保留勿删(口径若变回,直接复活这段即可) ═════════════
FAKE = "f0117000-0000-4000-8000-00000000000f"
_old_scan, _old_st, _old_busy = m._scan_follows, m._session_status, m._session_busy
try:
m._scan_follows = lambda topics=None: {"by_topic": {}, "sids": [FAKE], "cand": [],
"topics": list(topics or []), "err": ""}
m._session_status = lambda sid: ("working" if str(sid) == FAKE else _old_st(sid))
m._session_busy = lambda sid: ("busy" if str(sid) == FAKE else _old_busy(sid))
r1 = m._deliver_str("自测:不该被投出", "selftest-busy", {"roles": {}})
m._session_busy = lambda sid: ("idle" if str(sid) == FAKE else _old_busy(sid))
r2 = m._deliver_str("自测:不该被投出", "selftest-busy", {"roles": {}})
finally:
m._scan_follows, m._session_status, m._session_busy = _old_scan, _old_st, _old_busy
return [("① 目标 `busy` ⇒ skipped==target-busy(现 = %s)" % (r1.get("skipped") or "空"),
r1.get("skipped") == "target-busy"),
("② 目标 `idle` ⇒ ⛔ 不延后,往下走到 live 判(现 = %s)" % (r2.get("skipped") or "空"),
r2.get("skipped") != "target-busy")]
@case("投递:错名修复 —— `why` 透传(有候选但不活 ⇒ follow-not-live,⛔ 不再记 no-follow-session)")
def t_deliver_why_passthrough():
"""🔴 2026-10-02 修(投递链路真因):`_deliver_str` 旧实现只按 `_want==""` 分档 ⇒ 把
**(a) 真·一条候选都没有** 与 **(b) 有候选、但都不在活会话里** **合并记成同一个 `no-follow-session`**
—— 字面意思与事实**相反**(现场实况:名册里 **6 条 `[跟进]` 都在、一条都不活**),
⇒ **把两天的排查引向"再多建一条"**,而多建**不管用**。
⇒ 本用例钉住:`skipped` 必须**透传 `follow_for_topic()` 已算准的 `why`**,两档分得开。
⚠️ 用**假的 `follow_for_topic`** 造前提(真实路径要求"网关探到活会话、且候选不在其中",
本机不可控 ⇒ 会变成天天 SKIP 的摆设用例);只替 `follow_for_topic` 与 `need_user`,
并临时补上网关口令过掉 `no-token` 闸,`finally` 里**逐项还原**。
"""
m = imp()
FAKE = "f0f0f0f0-0000-4000-8000-00000000000e"
_old_rft, _old_nu = m.follow_for_topic, m.need_user
_old_tok = os.environ.get(m.GW_ENV)
_seen = []
os.environ[m.GW_ENV] = "selftest-token" # ⛔ 不设 ⇒ 早退成 `no-token`,走不到本分支
try:
m.need_user = lambda t: _seen.append(t)
m.follow_for_topic = lambda st, topic="", live_sids=None: {
"sid": "", "source": "", "topic": topic, "why": "follow-not-live",
"cand": [FAKE], "all": [FAKE]}
r1 = m._deliver_str("自测:有候选但不活", "selftest-why-live", {"roles": {}})
m.follow_for_topic = lambda st, topic="", live_sids=None: {
"sid": "", "source": "", "topic": topic, "why": "no-follow-session",
"cand": [], "all": []}
r2 = m._deliver_str("自测:一条候选都没有", "selftest-why-none", {"roles": {}})
finally:
m.follow_for_topic, m.need_user = _old_rft, _old_nu
if _old_tok is None:
os.environ.pop(m.GW_ENV, None)
else:
os.environ[m.GW_ENV] = _old_tok
return [
("① 有候选·都不活 ⇒ skipped==follow-not-live(现 = %s)" % (r1.get("skipped") or "空"),
r1.get("skipped") == "follow-not-live"),
("② 真·无候选 ⇒ skipped==no-follow-session(现 = %s)" % (r2.get("skipped") or "空"),
r2.get("skipped") == "no-follow-session"),
("③ 两档确实分开(⛔ 不再合并成一个名字)",
(r1.get("skipped") or "") != (r2.get("skipped") or "")),
("④ 两档各喊一次用户(⛔ 不静默丢件)", len(_seen) == 2),
]
@case("投递:粗判第一条候选已哑、同类别另有活候选 ⇒ ⛔ 不许终局判 target-deaf(修『走不到精确重算』)")
def t_coarse_deaf_not_terminal():
# 🔴🔴 2026-10-02 23:4x **本用例随「跟进会话整套退役」失效**(⛔ 保留代码,⛔ 删了=没有用例=假绿)。
# `follow_for_topic()` 已短路成 `follow-retired` ⇒ "粗判 vs 精确"那条判据链**永久走不到**。
# ⚠️ ⛔ 不许把断言改成"预期 follow-retired"(=断言恒真,P0-13 同族);判据见 `t_target_busy`。
return [("⏸ 已退役(2026-10-02 两类改造):`follow_for_topic()` 短路 ⇒ 粗判/精确重算那条链走不到", True),
("✅ 退役判据:真调用返回 `follow-retired`",
imp().follow_for_topic({}, "X", []).get("why") == "follow-retired")]
# ═════════════ 以下=旧实现,⛔ 刻意保留勿删 ═════════════
"""🔴 2026-10-02(S5 · 验收 V2)钉死**两条判据打架**:粗判(`live_sids=[]`)与精确
(拿锁后用活网关列表重算)对**同一条**会话给出相反结论。
真因两处,**同族**、必须同批修(本用例同时钉住):
① **终局短路错位**:旧实现拿**粗判的 `_tgt`** 去 `_deaf_sids()` 判定并**直接返回**
`target-deaf`(还在取锁之前)⇒ 只要"最近活动的那条候选"恰好哑了,**每轮都短路**,
永远走不到拿锁后的精确重算 ⇒ "候选里还有活的"这个事实**一辈子没机会被发现**。
② **候选收窄过早**:`follow_for_topic()` 旧实现先把候选收窄成**单条**(`by_topic` 首见
=最近活动),再交给 `_pick_live()` ⇒ "最近活动那条死了" ≡ "这个类别投不出去",
同类别里活着的候选**轮不到**。
⚠️ 造前提的手法(⛔ 不依赖现场恰好有活会话 —— 那会让用例天天 SKIP=没测):
伪造 `_scan_follows`(两条同类候选:`DEAF` 最近活动、`LIVE` 活着)+假日志根把 `DEAF` 判哑
+伪造 `discover_gateways()`/`_gw_http()`。**真实 `follow_for_topic()` / `_pick_live()` /
`_deliver_str()` 全程在位**——被替掉的只有"外部世界",判据链一条不换。
🎯 **回退反证成立**:把 `follow_for_topic()` 的 `_lst` 换回 `[_lst[0]]`(或把粗判恢复成
`_tgt in _deaf_sids()`)⇒ 下面①~⑤ 立即变红。
"""
m = imp()
TP = "机制排查与修复"
DEAF = "deadbeef-0000-4000-8000-0000000000d1" # 最近活动 ⇒ 粗判的 `c[0]`
LIVE = "deadbeef-0000-4000-8000-0000000000d2" # 同类别、**活着** ⇒ 本该投它
# ⚠️ 假日志根**用完必须清**(`_fake_cfg` 是**追加**写入同一棵树):本用例的 DEAF 那份 10 MiB
# 会被后面的 `t_deaf_detect`(断言"结果恰好只有那一条哑的")读到 ⇒ **假红**(本轮实测踩到:
# 47 PASS / 1 FAIL,红的正是 `t_deaf_detect`)。与 `t_deaf_latest_day_only` 同一手法。
shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True)
cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000}) # 只把 DEAF 判哑(LIVE 无日志 ⇒ 不哑)
restore = _with_fake_cfg(cfg)
_old = (m._scan_follows, m.discover_gateways, m._gw_http, m.need_user)
_old_tok = os.environ.get(m.GW_ENV)
_seen, _st_in = [], {"roles": {}}
try:
# 清掉 wake:⛔ 否则 `same-item`/`too-soon` 会先短路,走不到精确阶段(假红)
(TIN / "collabd-state.json").write_text(
json.dumps({"wake": {"hash": "", "ts": 0}}), encoding="utf-8")
(TIN / "wake.lock").write_text("free", encoding="utf-8")
(TIN / "wakeups.jsonl").unlink(missing_ok=True)
os.environ[m.GW_ENV] = "selftest-token" # ⛔ 不给 ⇒ 早退成 `no-token`,走不到投递
m._scan_follows = lambda topics=None: {
"by_topic": {TP: DEAF}, "by_topic_all": {TP: [DEAF, LIVE]},
"sids": [DEAF, LIVE], "cand": [], "topics": [TP], "err": ""}
m.discover_gateways = lambda *a, **k: [{"port": 1, "sessionId": LIVE}]
m._gw_http = lambda *a, **k: (200, "{}")
m.need_user = lambda t: _seen.append(t)
r = m._deliver_str("自测:粗判第一条哑", "selftest-coarse-deaf", _st_in, topic=TP)
_w = (r.get("delivered") or {})
_sid_in_wake = str((_st_in.get("wake") or {}).get("sessionId") or "")
_n = wakeups_n()
finally:
m._scan_follows, m.discover_gateways, m._gw_http, m.need_user = _old
if _old_tok is None:
os.environ.pop(m.GW_ENV, None)
else:
os.environ[m.GW_ENV] = _old_tok
restore()
shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) # ⛔ 不留残渣给后面用例
return [
("① ⛔ 不许短路成 target-deaf(现 = %s)" % (r.get("skipped") or "空(↓已投出)"),
r.get("skipped") != "target-deaf"),
("② 走到精确阶段并**真投出**(http=%r)" % (_w.get("http"),), _w.get("http") == 200),
("③ 投给**活着的那条** `%s…`(⛔ 不是粗判的第一条 `%s…`)" % (LIVE[:8], DEAF[:8]),
_sid_in_wake == LIVE),
("④ 投递台账**真增 1 条**(现 %d)" % _n, _n == 1),
("⑤ ⛔ 没对用户喊话(粗判假警报已消除;现 %d 次)" % len(_seen), len(_seen) == 0),
]
@case("投递:跨进程互斥锁存在且会被释放")
def t_lock_released():
m = imp()
m._deliver_str("自测", "selftest", {"roles": {}})
_p = TIN / "wake.lock"
try:
_txt = _p.read_text(encoding="utf-8", errors="replace").strip()
except Exception:
_txt = ""
try:
import inspect as _ins
_src = _ins.getsource(m._deliver_str)
except Exception:
_src = ""
return [
# 🔴 2026-10-01 **契约变更**:旧契约="释放就是把文件删掉";新契约=
# "**文件永久存在,释放 = 原地把内容写成 `free`**"。为什么必须变:
# `_deliver_str` 每轮取锁后都在 `finally` 里 `unlink(wake.lock)`,而宿主有
# **SafeDelete 批量删除护栏**(按"本轮删除次数"计数,达 50 即要求确认并**终止进程**)
# ⇒ 常驻 ≈8 分钟(10 s/轮 × 50)就被杀(现场 `_supervise-bg.out.log` 原文
# `{"count":50,"threshold":50,"targetCount":1,"targets":[…\wake.lock]}`)。
# ⚠️ 断言随之改判**语义**而非存在性:`不存在`(从未取锁)**或** `内容=free` 都算"已释放"。
("锁已释放(不存在 或 内容=free;实得 %r)" % _txt, (not _p.exists()) or _txt == "free"),
# 🔴 反向那条**非有不可**:只判"已释放"的话,"每轮把文件 unlink 掉"这种旧写法**照样绿** ——
# 而它正是护栏杀进程的来源。⚠️ 且必须要求**源码真读到**(`_src` 为空=没验,⛔ 不许恒真)。
("锁释放 ⛔ 不用 unlink(源码读到 %d 字符,%s `_lk.unlink`)"
% (len(_src), "无" if "_lk.unlink" not in _src else "仍有"),
bool(_src) and "_lk.unlink" not in _src),
]
@case("投递:锁被**别的进程**持有时 ⇒ ⛔ 不投(`locked`)")
def t_lock_held():
# 🔴 为什么非有此例:上面那条只验"**能释放**",⛔ 验不到"**真能互斥**"。
# 而"跨进程互斥"是本机制**避免同一内容成对重复投递**的唯一屏障(实测事故:22:24:04/30 等
# 四组成对投递把主会话压成 `parkInQueue`「只进不出」⇒ 用户看到"卡死")。
# ⚠️ 判据必须是**真的把锁占住**再调 —— 伪造"别的进程正持有"(内容=假 pid + mtime=现在 ⇒ 不陈旧)。
m = imp()
_p = TIN / "wake.lock"
_p.parent.mkdir(parents=True, exist_ok=True)
try:
m._deliver_str("自测", "selftest", {"roles": {}}) # 先走一遍:把锁建起来、并释放成 free
_p.write_text("999999", encoding="utf-8") # 伪造"别的进程正持有"(⛔ 不陈旧 ⇒ 不许抢占)
r = m._deliver_str("自测-锁被别人拿着", "selftest", {"roles": {}})
finally:
try:
_p.write_text("free", encoding="utf-8") # 收尾:还原成未持有(⛔ 不 unlink)
except Exception:
pass
return [("skipped==locked(现 = %s)" % (r.get("skipped") or "空"), r.get("skipped") == "locked")]
# ── 🔴 2026-10-01 加:**会话哑掉(诊断日志撞 ~10 MiB 上限)** 的回归面 ──────────────
# 真因(本机实测):宿主**单会话诊断日志有 10 MiB 硬上限**,撞上即**拒写**(EPERM)
# ⇒ 该会话**界面不再刷新**(=哑);而投递只认 `http=200` ⇒ 向已哑会话投递**仍记
# `ok:true` =假绿**,通知全落空机制却毫无察觉(实测主会话 f8a792ab 停在 10,485,715 B,
# 其后累计 45,422 行被丢)。
# ⇒ 修法:`collabd.py` 加 `_deaf_sids()`(≥9.5 MiB 判哑)+ 投递**两道闸**
# (粗判按登记目标 · **出手前再按网关实际目标**)⇒ 记 `skipped=target-deaf` + 喊用户。
# ⚠️ 阈值分工(⛔ 别混):**9.5 MiB** =「已哑、不许再投」(`_deaf_sids` 的拦截线);
# **5 MiB** =「该交接了」(会话主动换,见 CODEBUDDY.md §1.5 G)。
DEAF_MIB = 9_500_000 # ⚠️ 与 `collabd._deaf_sids()` 的阈值**逐字同源**(那边改了这里必须跟)
def _make_sparse(f) -> bool:
"""把已打开的**文件对象**标成 NTFS 稀疏文件(Windows);非 Windows 直接返回 True。
🔴🔴 2026-10-05 修一处**实测坐实了的假认知**(pitfalls P0-84):
本函数的老注释写着「用 `truncate` 建**稀疏文件**……⛔ 不真写 10 MiB」。
**在 Windows 上这是错的** —— `truncate()` 走的是 `SetEndOfFile`,
⛔ 不会自动把文件标成稀疏 ⇒ **逻辑大小与实占 1:1**。
实测:`open(p,"wb").truncate(9_600_000)` 建 10 个档,
逻辑 91.6 MB/实占 91.6 MB(不是"看起来大、实际很小")。
⇒ 单次 `selftest` 真占 ~60 MB;而 `install.py --verify` 每跑一次就跑一次 selftest,
**再叠加宿主的 safe-delete shim**(删工作区文件一律改道回收站)⇒
那些字节**不会消失,只是搬进回收站**,实测攒到 13.8 GB(`tmp/selftest`)。
⛔ 不许再靠"truncate 不真写"这个假设省事。
正解:写数据**之前**先 `FSCTL_SET_SPARSE`,之后再 truncate 才真稀疏。
⚠️ 顺序不能反 —— 先 truncate 再加标志,已分配的簇不会自动归还。
"""
if os.name != "nt":
return True
try:
import ctypes
import msvcrt
from ctypes import wintypes
FSCTL_SET_SPARSE = 0x000900C4
GENERIC_WRITE = 0x40000000
FILE_SHARE_RW = 0x00000001 | 0x00000002
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
k32.CreateFileW.restype = wintypes.HANDLE
k32.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD,
wintypes.LPVOID, wintypes.DWORD, wintypes.DWORD,
wintypes.HANDLE]
k32.DeviceIoControl.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.LPVOID,
wintypes.DWORD, wintypes.LPVOID, wintypes.DWORD,
ctypes.POINTER(wintypes.DWORD), wintypes.LPVOID]
OPEN_EXISTING = 3
h = k32.CreateFileW(str(f.name), GENERIC_WRITE, FILE_SHARE_RW, None,
OPEN_EXISTING, 0x00000080, None) # FILE_ATTRIBUTE_NORMAL
if h == wintypes.HANDLE(-1).value or not h:
return False
try:
br = wintypes.DWORD(0)
ok = k32.DeviceIoControl(h, FSCTL_SET_SPARSE, None, 0, None, 0,
ctypes.byref(br), None)
return bool(ok)
finally:
k32.CloseHandle(h)
except Exception:
return False
def _fake_cfg(sizes: dict, day_offset: int = 0) -> Path:
"""在**测试区内**造一份假 `logs/<日期>/sdk/conversations/`(⛔ 生产日志目录一个字节不动)。
`_deaf_sids()` 只读 `CODEBUDDY_CONFIG_DIR/logs/...` ⇒ 换掉配置根即可**完全离线**复现本坑。
`day_offset`:0=今天(默认)、1=昨天 —— 🔴 **按天分目录**正是"永久判哑"那个坑的土壤。
🔴 体积用「**先标稀疏 → 再 `truncate`**」造(见 `_make_sparse`):
目标是秒级拿到 10 MiB 的**体积读数**(`getsize`),而**实占尽量小**。
⚠️ 老实现只 `truncate`、⛔ 没标稀疏 ⇒ 在 Windows 上**真写**(P0-84 实测 1:1)。
⚠️ 这里**不追求零占用**:只要"不真写满"即可 —— 某些文件系统不支持稀疏时
`_make_sparse` 返回 False,此时行为与老实现相同(真写),
**但会打一条 stderr 提示**,⛔ 不许静默退化。
"""
_d = time.strftime("%Y-%m-%d", time.localtime(time.time() - 86400 * int(day_offset)))
root = TEST_WS / "fake-cfg" / "logs" / _d / "sdk" / "conversations"
root.mkdir(parents=True, exist_ok=True)
for _sid, _n in (sizes or {}).items():
_p = root / (_sid + ".log")
with open(_p, "wb") as f:
if not _make_sparse(f):
print(f"⚠ _fake_cfg: {_p.name} 未能标成稀疏文件 ⇒ 本次将真占用 {_n} 字节",
file=sys.stderr)
f.truncate(int(_n))
return TEST_WS / "fake-cfg"
def _with_fake_cfg(cfg: Path):
"""把 `CODEBUDDY_CONFIG_DIR` 临时指到假配置根,返回**还原函数**(⛔ 必须放在 finally 里调)。
为什么非还原不可:`_db()` 也读同一个变量 —— 不还原会把**后面所有用例**(主会话解析等)
一起带偏(那是"改一个用例弄红一片"的经典假红)。
"""
old = os.environ.get("CODEBUDDY_CONFIG_DIR")
def _restore():
if old is None:
os.environ.pop("CODEBUDDY_CONFIG_DIR", None)
else:
os.environ["CODEBUDDY_CONFIG_DIR"] = old
os.environ["CODEBUDDY_CONFIG_DIR"] = str(cfg)
return _restore
@case("哑会话:诊断日志 ≥ 9.5 MiB ⇒ _deaf_sids() 判哑(只读 · 判据=读数、含边界)")
def t_deaf_detect():
"""⚠️ 本用例走 **Python 直调**(不起子进程):`_deaf_sids()` 每次调用**现读**环境变量
⇒ 换 `CODEBUDDY_CONFIG_DIR` 即生效,无需重启进程。"""
DEAF = "deadbeef-0000-4000-8000-000000000001"
NEAR = "deadbeef-0000-4000-8000-000000000002" # 差 1 字节 ⇒ 验**边界不含**
OKAY = "cafe0000-1111-4222-8333-444444444444"
cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000, NEAR: DEAF_MIB - 1, OKAY: 4096})
restore = _with_fake_cfg(cfg)
try:
d = imp()._deaf_sids()
finally:
restore()
return [("≥9.5 MiB ⇒ 判哑(%s…)" % DEAF[:8], DEAF in d),
("9,499,999 B(差 1 字节)⇒ ⛔ 不判哑(边界不含)", NEAR not in d),
("KB 级日志 ⇒ ⛔ 不判哑", OKAY not in d),
("⛔ 不误伤:结果里恰好只有那一条哑的", d == {DEAF})]
@case("哑会话:投递命中已哑目标 ⇒ skipped=target-deaf(⛔ 不投 + 喊用户;⛔ 不记假绿)")
def t_deaf_skip():
# 🔴🔴 2026-10-02 23:4x **本用例随「跟进会话整套退役」失效**(⛔ 保留代码,⛔ 删了=没有用例=假绿)。
# 投给「已哑跟进会话」这条路已不存在(`follow-retired`)⇒ `skipped==target-deaf` 拿不到。
# ⚠️ ⛔ 不许改成"预期 follow-retired"(=断言恒真);判据见 `t_target_busy`。
# ✅ 但**「不记假绿」这条原则仍活着**,改由退役判据承担:真调用拿 `sid=""` ⇒ 投不出去 ⇒ 台账不增。
m0 = imp()
_r = m0.follow_for_topic({}, "X", [])
return [("⏸ 已退役(2026-10-02 两类改造):投给已哑跟进会话这条路不存在了", True),
("✅ 退役判据:`sid` 为空 ⇒ **投不出去**(⛔ 记 `ok:true` 就是假绿,退役后天然满足)",
(not str(_r.get("sid") or "")) and _r.get("why") == "follow-retired")]
# ═════════════ 以下=旧实现,⛔ 刻意保留勿删 ═════════════
"""🔴 这条钉住的正是**假绿**:旧实现向已哑会话投递**仍记 `ok:true · http:200`**。
判据三件:① 判读是 `target-deaf` ② 投递台账**零增量** ③ 已把"该点哪一下"落到 `NEED-USER.md`。"""
DEAF = "deadbeef-0000-4000-8000-000000000001"
cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000})
try:
(TIN / "NEED-USER.md").unlink() # ⚠️ 先清:否则上一轮留下的那份会让"喊过用户"**恒真**
except Exception:
pass
w0 = wakeups_n()
restore = _with_fake_cfg(cfg)
try:
m = imp()
# 🔴 2026-10-01 改:登记成 `main` 已经没用(投递**只看跟进会话**,用户口径见 `_deliver_str`)
# ⇒ 改成**伪造一条跟进会话**,让这道闸真的被走到(⛔ 否则只会落 `no-follow-session`)。
_old_scan = m._scan_follows
m._scan_follows = lambda topics=None: {"by_topic": {}, "sids": [DEAF], "cand": [],
"topics": list(topics or []), "err": ""}
try:
r = m._deliver_str("自测:哑会话不该被投", "selftest-deaf", {"roles": {}})
finally:
m._scan_follows = _old_scan
_sk = str(r.get("skipped") or "")
_nu = (TIN / "NEED-USER.md").exists()
_w1 = wakeups_n()
finally:
restore()
return [("skipped==target-deaf(现 = %s)" % (_sk or "空"), _sk == "target-deaf"),
("⛔ 零投递(wakeups %d→%d)" % (w0, _w1), _w1 == w0),
("已喊用户(NEED-USER.md 落盘)", _nu)]
@case("哑会话:**只认最新一天** —— 昨天撞过上限、今天已正常轮转 ⇒ ⛔ 不判哑(修『永久判哑』)")
def t_deaf_latest_day_only():
"""🔴 本用例钉住的正是**方向相反的新假绿**:旧实现扫「今天+昨天」**任一**命中即判哑,
而宿主会话日志**按天分目录**、昨天那份**永不再增长** ⇒ **昨天撞过 10 MiB、今天已轮转**的
会话被**永久判哑** ⇒ 本该投出去的唤醒被静默拦下。
实证 `a202550c`:昨天 10.0 MB、**今天 4.2 MB 且宿主仍在写它**。
⇒ 判据改为「**每个会话取它最新一天那份日志**」。⛔ 今天没有日志 ⇒ 仍退到昨天(保守兜底,不许丢)。
"""
ROTATED = "a202550c-0000-4000-8000-00000000000a" # 昨 10 MiB / 今 4.2 MB ⇒ 已轮转 ⇒ ⛔ 不判哑
OLDONLY = "0badf00d-0000-4000-8000-00000000000b" # 只有昨天、且撞上限 ⇒ 仍判哑(⛔ 兜底不许丢)
NEWDEAF = "feedface-0000-4000-8000-00000000000c" # 今天才撞上限 ⇒ 判哑
HEALTHY = "5ca1ab1e-0000-4000-8000-00000000000d" # 今 4 KB ⇒ ⛔ 不判哑
shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True) # ⛔ 先清空:前面两条用例也在同一棵树下造过文件
_fake_cfg({ROTATED: DEAF_MIB + 500_000, OLDONLY: DEAF_MIB + 500_000}, day_offset=1) # 昨天那份
cfg = _fake_cfg({ROTATED: 4_200_000, NEWDEAF: DEAF_MIB + 500_000, HEALTHY: 4096}) # 今天那份
restore = _with_fake_cfg(cfg)
try:
d = imp()._deaf_sids()
finally:
restore()
shutil.rmtree(TEST_WS / "fake-cfg", ignore_errors=True)
return [("昨 10 MB / 今 4.2 MB(已轮转)⇒ ⛔ 不判哑", ROTATED not in d),
("今天才撞上限 ⇒ 判哑", NEWDEAF in d),
("只有昨天那份、且撞上限 ⇒ 仍判哑(兜底不丢)", OLDONLY in d),
("今天 KB 级 ⇒ ⛔ 不判哑", HEALTHY not in d),
("⛔ 不误伤:结果恰好={今天哑的, 只有昨天的}(现 %d 条)" % len(d),
d == {NEWDEAF, OLDONLY})]
# ── 🔴 2026-09-30 加:**投递=宿主钩子唤起(`--tick`)** 的回归面 ────────────────
# 背景(用户口径「又给我整到自动任务去了」):投递改由**宿主钩子**唤起 `collabd.py --tick`
# (钩子是宿主子进程 ⇒ 自带网关口令)。
# ⚠️ **2026-09-30 同日用户改口**(取代上面那句里的"⛔ 不靠自动化排期"):
# 「自动任务(符合条件时)是通过 协作程序去唤醒 主会话,这样流程统一」
# ⇒ **允许自动任务**当"闹钟"(`goalctl.py wake` ⇒ `--tick`);⛔ 但判断与投递**仍只在本程序一处**。
# 🔴 下面两条约束**与"谁拨这一下"无关**,**仍然有效**(改触发源⛔ 不免除它们):
# ① `--once`(常驻程序/投影轮)**⛔ 不许消费队列** —— 它在 UserPromptSubmit 上先跑;
# ② `--tick` **投不出去就不许消费队列** —— 旧实现无条件 `notified[kid]=stt` ⇒ **静默丢件**。
@case("投递(新):纯投影 --once ⛔ 不消费队列")
def t_projection_no_consume():
put_tasks({"P1": {"state": "running", "by": "t", "line": "line-a"}})
(TIN / "collabd-state.json").write_text(json.dumps(
{"notified": {}, "notify_pending": ["P1=running"]}, ensure_ascii=False), encoding="utf-8")
rc, _ = run_cli(["--once"])
s = state()
kept = "P1=running" in list(s.get("notify_pending") or [])
no_aw = not s.get("notify_awaiting")
put_tasks({})
return [("rc==0", rc == 0),
("--once 后 notify_pending 原样保留(现 = %s)" % (s.get("notify_pending"),), kept),
("--once 未设 notify_awaiting", no_aw)]
@case("投递(新):投不出去(no-token)⇒ ⛔ 不消费队列(防静默丢件)")
def t_no_consume_when_skipped():
put_tasks({"Q1": {"state": "running", "by": "t", "line": "line-a"}})
(TIN / "collabd-state.json").write_text(json.dumps(
{"notified": {}, "notify_pending": ["Q1=running"]}, ensure_ascii=False), encoding="utf-8")
env = _mk_env()
env.pop("CODEBUDDY_GATEWAY_PASSWORD", None) # 抽掉口令 ⇒ 投递必被挡(no-token)
# 🔴🔴 2026-10-05 `--tick` 已删 ⇒ 载体换成 `--supervise` 的**判定本体** `supervise()`:
# 本用例的红线是「**投不出去就不许消费队列**」(防静默丢件),⛔ 与 tick 这个入口无关
# ⇒ 直接真调 `supervise(st, deliver=True, mutate=True)` 并抽掉口令(⛔ 不换个入口就丢掉红线)。
m = imp()
try:
_st = json.loads((TIN / "collabd-state.json").read_text(encoding="utf-8"))
except Exception:
_st = {}
_old = os.environ.pop("CODEBUDDY_GATEWAY_PASSWORD", None)
try:
m.supervise(_st, deliver=True, mutate=True)
except Exception:
pass
finally:
if _old is not None:
os.environ["CODEBUDDY_GATEWAY_PASSWORD"] = _old
rc, out = 0, ""
s = state()
pend = list(s.get("notify_pending") or [])
notified = dict(s.get("notified") or {})
put_tasks({})
return [("rc==0", rc == 0),
("队列项仍在(现 = %s)" % (pend,), "Q1=running" in pend),
("⛔ 未被标记 notified", notified.get("Q1") != "running"),
("⛔ 未设 notify_awaiting", not s.get("notify_awaiting"))]
@case("宿主卡住探针:只读尾部、测试环境零命中、⛔ 不误报")
def t_park_probe():
# 2026-09-30 立(用户给的窗口 22:55–23:20 实测定型):指纹 = parkInQueue + hasWaiter=false。
# ⚠️ 测试工作区名是 `selftest` ⇒ 候选日志目录里没有它的日志 ⇒ 必须**零命中且不报错**(⛔ 不碰生产日志)。
m = imp()
r = m.probe_host_park()
real = ('[2026/9/29 22:53:00.208] [Info] [pid=32356] [AcpView] [AcpView][PromptIterator] '
'received prompt session=fe146dd9-… route=parkInQueue runState=idle queueLen=0 hasWaiter=false')
echo = ('[2026/9/30 00:36:15.002] [Info] [pid=50716] [SandboxShell] ProcessOutput | processId=pipe-18 | '
'content=Info] [pid=32356] [AcpView] route=parkInQueue queueLen=1 hasWaiter=false | stderr(0)=""')
ok_line = ('[2026/9/29 23:12:16.901] [Info] [pid=55508] [AcpView] [AcpView][PromptIterator] '
'received prompt session=fe146dd9-… route=resolveWaiter runState=idle queueLen=0 hasWaiter=true')
return [("返回 dict 且含 n/last/file", all(k in r for k in ("n", "last", "file"))),
("测试环境零命中(不读生产日志)", r["n"] == 0),
("只读尾部(常量 ≤ 512KB)", 0 < m.PARK_TAIL <= 512 * 1024),
("⛔ 不命中「取证命令的回声行」(防假阳性)", m._is_park_line(echo) is False),
("✅ 命中真正的 parkInQueue 记录行", m._is_park_line(real) is True),
("✅ 不误判正常行(resolveWaiter/hasWaiter=true)", m._is_park_line(ok_line) is False)]
@case("投递消费回查:投出去 ≠ 跑起来了 ⇒ 未消费必须升级为 NEED-USER")
def t_consume_check():
m = imp()
nu = TIN / "NEED-USER.md"
if nu.exists():
nu.unlink()
# ① 刚投出去(宽限期内)⇒ waiting,⛔ 不报警
st1 = {"wake": {"expect": {"sid": "0" * 36, "at": time.time()}}}
r1 = m.check_delivery_consumed(st1)
quiet = not nu.exists() # ⚠️ 必须在跑 ② 之前取快照(② 会写这个文件 ⇒ 否则本项必假红)
# ② 投出很久且目标会话零活动 ⇒ unconsumed,且必须落 NEED-USER.md(喊人)
st2 = {"wake": {"expect": {"sid": "0" * 36, "at": time.time() - 9999}}}
r2 = m.check_delivery_consumed(st2)
shouted = nu.exists() and "切走再切回" in nu.read_text(encoding="utf-8", errors="replace")
return [("宽限期内 = waiting 且不报警", r1.get("waiting") is True and quiet),
("超期未动 = unconsumed", r2.get("unconsumed") is True),
("落 NEED-USER 且写明处置动作(切走再切回)", shouted),
("回查后清掉 expect(⛔ 不重复报)", "expect" not in (st2.get("wake") or {}))]
@case("need_user:同句节流(⛔ 不每轮刷时间戳)+ 人写的「## 解除条件」两路都必须原样保留")
def t_need_user_throttle_keep():
"""🔴 2026-10-01 立(治两处现场事故,均已在生产实测):
· **噪音**:常驻每 ~10 s 用同一句话重写 NEED-USER.md ⇒ 时间戳一直变、人看不出「这条是新的还是旧的」
(现场:22:06–22:12 之间被刷了 20+ 次)。
· **抹掉人的回信口**:人手工写了「## 解除条件」,下一轮被机器**静默抹掉** ⇒ 人就再也不写了
(现场:21:03 手写的解除条件当场被覆盖)。
⚠️ 判据必须能「改前报红」(P0-13):把节流去掉 ⇒ ③ 报红;把保留 `_keep` 去掉 ⇒ ④ 报红。"""
import inspect as _ins
m = imp()
nu = TIN / "NEED-USER.md"
human = "\n## 解除条件\n- 用户手写:这是人唯一的回信口,⛔ 不许抹\n"
try:
if nu.exists():
nu.unlink()
# ① 第一次喊 ⇒ 必须落文件(⛔ 不许"喊了但没落")
m.need_user("自测原因-A")
got1 = nu.exists() and ("自测原因-A" in nu.read_text(encoding="utf-8", errors="replace"))
# ② 模拟人回信:追加「## 解除条件」
nu.write_text(nu.read_text(encoding="utf-8", errors="replace").rstrip() + human,
encoding="utf-8")
mt2 = nu.stat().st_mtime
# ③ 同一句话、节流窗口内再喊 ⇒ ⛔ 不重写(mtime 不变)+ 人的话必须还在
m.need_user("自测原因-A")
t3 = nu.read_text(encoding="utf-8", errors="replace")
quiet = (nu.stat().st_mtime == mt2) and ("用户手写" in t3)
# ④ 换一个原因 ⇒ 允许重写,但**人的话仍要原样保留**
m.need_user("自测原因-B")
t4 = nu.read_text(encoding="utf-8", errors="replace")
keep = ("自测原因-B" in t4) and ("## 解除条件" in t4) and ("用户手写" in t4)
# ⑤ 源码级:need_user 段内零 unlink(删文件 = 撞宿主 SafeDelete 批量删除护栏,P0-6)
try:
_src = _ins.getsource(m.need_user)
except Exception:
_src = ""
no_unlink = bool(_src) and (".unlink" not in _src)
return [("① 首次喊 ⇒ NEED-USER.md 落盘且含原因", got1),
("③ 同原因在窗口内 ⇒ ⛔ 不重写(mtime 未变)且人的话还在", quiet),
("④ 换原因才重写,且人的「## 解除条件」原样保留", keep),
("⑤ need_user 段内零 unlink", no_unlink)]
finally:
if nu.exists():
nu.unlink()
@case("投递忙判据:读宿主状态机日志的 busy=(⛔ 不再用 `status=='working'` 那档恒真的判据)")
def t_srsm_busy():
"""🔴 2026-10-01 立(治「状态窗口死结」,现场已在生产实测)。
病根:`sessions.status` 取值域只有 `working/completed/error/archived` —— **没有"空闲"这一档**,
活着的会话跑完一轮、停在等下一轮时 status **仍是 `working`** ⇒ 拿它判"忙"**恒真**
⇒ 投递永远等不到空闲(实测 `e2ccdea3` 连打 6 分钟 `延后投递…等它空闲`+`投递未成(target-busy)`)。
正解:宿主自己的 `[SessionRunStateMachine]` 日志(⛔ 不进数据库,查库查不到)。
⚠️ 判据必须能「改前报红」(P0-13):把"新鲜度"那半去掉 ⇒ ③ 报红;把「先判终结态」去掉 ⇒ ⑤ 报红。
"""
m = imp()
tmpd = TEST_WS / "logs"
tmpd.mkdir(parents=True, exist_ok=True)
f = tmpd / "fakews__abcd.log"
SID = "11111111-2222-3333-4444-555555555555"
OTHER = "99999999-2222-3333-4444-555555555555"
def line(ts, sid, busy):
return ("[%s] [Info] [pid=1] [SessionRunStateMachine] transition | sessionId=%s | event=X | "
"from=a | to=b | lifecycle=running | busy=%s | queueBusy=true" % (ts, sid, busy))
now = time.strftime("%Y/%m/%d %H:%M:%S")
old = time.strftime("%Y/%m/%d %H:%M:%S", time.localtime(time.time() - 7200))
orig_dirs, orig_status = m._log_dirs, m._session_status
try:
m._log_dirs = lambda: [f]
# ① 末条 `busy=true` 且新鲜 ⇒ busy(正对照)
f.write_text(line(now, SID, "true") + "\n", encoding="utf-8")
c1 = m._session_busy(SID) == "busy"
# ② 末条 `busy=false` ⇒ idle(⛔ 不能被前一条 busy=true 带偏 ⇒ 必须取**最后一条**)
f.write_text(line(now, SID, "true") + "\n" + line(now, SID, "false") + "\n", encoding="utf-8")
c2 = m._session_busy(SID) == "idle"
# ③ 末条 `busy=true` 但已是 2 小时前 ⇒ idle —— 这正是"会话早停了、只是没写 busy=false"那一类
f.write_text(line(old, SID, "true") + "\n", encoding="utf-8")
c3 = m._session_busy(SID) == "idle"
# ④ 日志里**没有**该 sid ⇒ ''(⛔ 不猜、⛔ 不回落成 busy 或 idle)
f.write_text(line(now, OTHER, "true") + "\n", encoding="utf-8")
c4 = m._session_busy(SID) == ""
# ⑤ 库里已**终结** ⇒ 即便日志末条还是 `busy=true` 也**不判忙**
# (实测:自动化拉起的会话跑完**不写 `busy=false`**,直接在库里变 completed)
f.write_text(line(now, SID, "true") + "\n", encoding="utf-8")
m._session_status = lambda s: "completed"
c5 = (m._target_busy(SID) is False)
finally:
m._log_dirs, m._session_status = orig_dirs, orig_status
try:
f.unlink()
except Exception:
pass
# ⑥ 源码级**反回归**:正则必须用**命名组**。
# 本轮真踩过:写成 `(?:\.(\d+))?` 时内层 `(\d+)` **仍是捕获组** ⇒ 后面 `m.group(8)` 全错位一位
# ⇒ `busy` 恒读成 False ⇒ **恒判"空闲"**(方向相反:会往正在跑的会话里插话)。
# ⛔ 这条断言不是"看代码像不像" —— 它是**唯一**能在这类错位再发生时立刻报红的东西。
# ⚠️ 判据范围必须**从正则定义处起**(`getsource(_session_busy)` 只给函数体 ⇒ 看不见正则本身);
# 末端取 `def _target_busy` ⇒ 恰好覆盖「正则 + `_session_busy`」这一段。
_all = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
_i = _all.find("_SRSM_RE = re.compile(")
_j = _all.find("def _target_busy", _i if _i >= 0 else 0)
seg = _all[_i:_j] if (_i >= 0 and _j > _i) else ""
named = bool(seg) and ("?P<busy>" in seg) and not re.search(r"group\(\s*[\"']?[1-9]", seg)
# ===== 🔴🔴 2026-10-04 第 2 源(`vibe-product` 实测坐实):会话级日志兜底 =====
# 🔴 病根:工作区日志 `<ws>__<hash>.log` 里 `SessionRunStateMachine` 有 615 条,
# 但**那个 sid 一条都没有** ⇒ 宿主**不为它写**工作区级状态机日志
# ⇒ `_session_busy()` 恒 `''` ⇒ 老逻辑**必然误判「没在跑」⇒ 往正在跑的会话插话**
# (该会话当时 `working` 358 次 / `idle` 仅 3 次 / 用户只发言 4 次,跨 2h46m)。
# ✅ 正解=**双源**:工作区读不到时回落到**会话级** `<logs>/<日期>/sdk/conversations/<sid>.log`。
# ⚠️ 那个文件**字段完全不同**:`state-machine:transition {"to":"working"}`,⛔ 没有 `busy=`。
# ⚠️ 文件名**带连字符**,而调用方常拿库里 32 位无连字符 id ⇒ 两种形态都得试。
conv_dir = TEST_WS / "logs" / time.strftime("%Y-%m-%d") / "sdk" / "conversations"
conv_dir.mkdir(parents=True, exist_ok=True)
# ⛔ 用**真 sid**:长度 36 且前缀 8 位唯一,否则下面的前缀兜底会走不到
CSID = "b232218f-c8e6-49cb-911c-f2afe00bcd39"
CNO = "aaaaaaaa-0000-0000-0000-000000000000"
cf = conv_dir / ("%s.log" % CSID)
import datetime as _dt
orig_conv, orig_st2 = m._conv_log, m._session_status
# 🔴🔴 2026-10-04修:**时间戳必须相对"现在"生成**,⛔不许写死绝对时刻。
# 写死 `2026-10-04T03:24:24.996Z` 时,本用例只在**当天凌晨**成立;一旦跨过
# `SRSM_FRESH`(900s) 就恒定报红(实测 age=12372s)⇒ 这种红是**用例自己过期**,
# **不是判据坏了** ⛔ 别去改判据迁就它(这正是"基线恒红 ⇒ 变异验证作废"那个坑)。
def _now_utc(offset_h: float = 0.0) -> str:
t_ = _dt.datetime.now(_dt.timezone.utc) + _dt.timedelta(hours=offset_h)
return t_.strftime("%Y-%m-%dT%H:%M:%S.000Z")
try:
m._conv_log = lambda sid=None: [cf] if sid and str(sid).replace("-", "") == CSID.replace("-", "") else []
# ⑦ 末条 to=working ⇒ busy
cf.write_text('%s state-machine:transition '
'{"from":"idle","to":"working"}\n' % _now_utc(-0.05), encoding="utf-8")
c7 = m._session_busy_conv(CSID) == "busy"
# ⑧ 末条 to=idle ⇒ idle(⛔ 取**最后一条**)
cf.write_text('%s state-machine:transition {"to":"working"}\n'
'%s state-machine:transition {"to":"idle"}\n'
% (_now_utc(-1), _now_utc(-0.02)), encoding="utf-8")
c8 = m._session_busy_conv(CSID) == "idle"
# ⑨ ⛔ 拿**工作区正则**去读会话级日志 ⇒ 必得 ''(老行为:恒读不到 ⇒ 恒判空闲)
_keep = m._CONV_TR
m._CONV_TR = m._SRSM_RE
c9 = m._session_busy_conv(CSID) == ""
m._CONV_TR = _keep
# ⑩ 🔴 整条链:工作区源读不到 ⇒ **回落源必须救回来**
# (改前 `_target_busy` = False = 误判空闲;改后必须 True)
_ob, _os = m._session_busy, m._session_status
m._session_busy = lambda s: ""
m._session_status = lambda s: "working"
cf.write_text('%s state-machine:transition {"to":"working"}\n' % _now_utc(-0.05),
encoding="utf-8")
c10 = m._target_busy(CSID) is True
# ⑪ ⛔ 但库里已**终结** ⇒ 仍不判忙(第 ① 道闸优先级不能被回落源破坏)
m._session_status = lambda s: "completed"
c11 = m._target_busy(CSID) is False
m._session_busy, m._session_status = _ob, _os
finally:
m._conv_log, m._session_status = orig_conv, orig_st2
try:
cf.unlink()
except Exception:
pass
# ⑫ 源码级:双源接线必须在(⛔ 少一个就退回单源误判)
_src = _all[_all.find("def _target_busy"):_all.find("def _target_busy") + 2600]
dual = ("_session_busy_conv" in _src) and ("_session_busy(sid)" in _src)
# ⑬ sid 形态:库里 32 位无连字符 ⇄ 文件名 36 位带连字符,⛔ 两种都要能命中
_pathfix = ("replace(\"-\", \"\")" in _all) or ("replace('-', '')" in _all)
# ===== 🔴🔴 2026-10-04 第 2 源的两个真缺陷(`vibe-product` / `b232218f…` 实测坐实)=====
#⛔ 两个缺陷**同向**:都让"已收工/已死活的会话"被判`busy` ⇒ 调用方 `skipped="target-busy"`
# ⇒ 永远收不到上报且**无告警**(静默饿死)⇒ 比"恒真"更隐蔽。
#① **跨日文件按遍历顺序覆盖**:`_conv_log` 返回 `[今天, 昨天]` ⇒ 昨天那条 `working`
# 会把今天已经 `idle` 的盖掉。⚠️ 这条**必须真的造两个文件**才测得到 ——
# 只喂一个文件的话两种实现都过(假绿)。
#② **完全没有新鲜度闸**(`_session_busy` 有 `SRSM_FRESH`,本源一个都没有)。
try:
def _ts_utc(hours_ago: float) -> str:
t = _dt.datetime.now(_dt.timezone.utc) - _dt.timedelta(hours=hours_ago)
return t.strftime("%Y-%m-%dT%H:%M:%S.000Z")
day_t = time.strftime("%Y-%m-%d")
day_y = time.strftime("%Y-%m-%d", time.localtime(time.time() - 86400))
d_today = TEST_WS / "logs" / day_t / "sdk" / "conversations"
d_yest = TEST_WS / "logs" / day_y / "sdk" / "conversations"
d_today.mkdir(parents=True, exist_ok=True)
d_yest.mkdir(parents=True, exist_ok=True)
XSID = "dddddddd-1111-2222-3333-444444444444"
f_today = d_today / ("%s.log" % XSID)
f_yest = d_yest / ("%s.log" % XSID)
orig_conv3 = m._conv_log
try:
# 🔴 **顺序照 `_conv_log` 的真形态**:`[今天, 昨天]`(⛔ 顺序反了测不到本缺陷)
m._conv_log = lambda sid=None: [f_today, f_yest] if sid and XSID in str(sid) else []
# 今天末条 idle(会话已收工),昨天卡在 working
f_today.write_text('%s state-machine:transition {"from":"working","to":"idle"}\n'
% _ts_utc(0.01), encoding="utf-8")
f_yest.write_text('%s state-machine:transition {"from":"idle","to":"working"}\n'
% _ts_utc(20), encoding="utf-8")
# ①跨日:取**时间戳最新**的那条⇒ 今天 idle ⇒判 idle(改前:被昨天覆盖⇒ busy)
c14 = m._session_busy_conv(XSID) == "idle"
# ② 新鲜度:单文件末条 working 但已是 3 小时前 ⇒ idle(改前:永久 busy)
m._conv_log = lambda sid=None: [f_today] if sid and XSID in str(sid) else []
f_today.write_text('%s state-machine:transition {"from":"idle","to":"working"}\n'
% _ts_utc(3), encoding="utf-8")
c15 = m._session_busy_conv(XSID) == "idle"
# ③ ⛔ 反向回归:末条 working 但**只有 2 分钟** ⇒ 必须仍判 busy
# (防"新鲜度闸写太狠,把正在跑的也判空闲"⇒ 那是**往在跑的会话插话**的方向)
f_today.write_text('%s state-machine:transition {"from":"idle","to":"working"}\n'
% _ts_utc(0.03), encoding="utf-8")
c16 = m._session_busy_conv(XSID) == "busy"
# ④ ⛔ 时区:会话级日志行尾是 `Z`(UTC),⛔ 误按本地时区解析会差 8h
# ⇒ 东八区下"2 分钟前"会被算成"8 小时前"⇒ 恒判过期 ⇒ ③ 变idle。
# 锚在**真算出来的差值**上:⛔ 不锚"源码里出现了 calendar"(那是文本出现过,不是判定生效)。
_ln = "%s state-machine:transition {\"to\":\"working\"}" % _ts_utc(0.03)
_got = m._conv_ts(_ln)
c17 = isinstance(_got, float) and abs(_got - time.time()) < 300
finally:
m._conv_log = orig_conv3
for _f in (f_today, f_yest):
try:
_f.unlink()
except Exception:
pass
except Exception as _e: # ⛔ 造不出样本 ⇒ 判红,⛔ 不许静默当过
c14 = c15 = c16 = c17 = "造样本失败:%s" % _e
return [("① 末条 busy=true 且新鲜 ⇒ `busy`", c1),
("② 末条 busy=false ⇒ `idle`(取的是**最后一条**)", c2),
("③ 末条 busy=true 但已 2 小时 ⇒ `idle`(新鲜度真在起作用)", c3),
("④ 日志无该 sid ⇒ `''`(⛔ 不猜)", c4),
("⑤ 库里已终结 ⇒ 日志仍 busy=true 也**不判忙**", c5),
("⑥ `_session_busy` 用**命名组**、零位置组引用(防'数括号'错位)", named),
("⑦ 会话级源 末条 `to=working` ⇒ `busy`", c7),
("⑧ 会话级源 末条 `to=idle` ⇒ `idle`(取的是**最后一条**)", c8),
("⑨ ⛔ 拿**工作区正则**读会话级 ⇒ 恒 `''`(= 老行为;两条正则形状不同)", c9),
("⑩ 🔴 工作区源读不到时**回落源救回来**(改前 False = 往在跑的会话插话)", c10),
("⑪ 🔴 但库里已终结 ⇒ 回落源**不许**越权判忙(第 ① 道闸优先)", c11),
("⑫ 源码级:**双源接线**在(⛔ 少一个就退回单源误判)", dual),
("⑬ sid 形态兼容:32 位无连字符 ⇄ 36 位带连字符都命中", _pathfix),
("⑭ 🔴 跨日两个文件 ⇒ 取**时间戳最新**那条(⛔ 昨天的工作态不得盖掉今天的已收工)", c14),
("⑮ 🔴 会话级源也要有**新鲜度闸**(3 小时前的 working ⇒ idle)", c15),
("⑯ 🔴 反向回归:2 分钟前的 working **仍判 busy**(⛔ 防闸太狠⇒ 往在跑的会话插话)", c16),
("⑰ 🔴 会话级时间戳按 **UTC** 解析(⛔ 误按本地时区差 8h ⇒ 恒判过期)", c17)]
@case("🔴 `--tick` 已删:参数表/分支/钩子调用**三处皆无**,且投递段仍真删")
def t_tick_wired():
"""🔴🔴 2026-10-05 `--tick` **机制删除**(用户:「检查程序 常驻 自己不会判断吗,非要什么 tick
once 去触发?」)⇒ 本用例**整条反转**:从「tick 在位可跑」改为「tick 三处皆无」。
它原先扛的三件事已各归其位,⛔ 判据必须**逐条钉住迁移后的落点**(否则删了没人知道去哪了):
① 建检查会话排期(两条腿)⇒ **`--supervise` 主循环**里 `queue_pending()>0` 分流判
② park 指纹探针 ⇒ 抽成 `_tick_park_probe()`,常驻主循环节流调
③ 顺手续命常驻 ⇒ 钩子里改成 `--ensure`(`maybe_ensure_supervise`)
⚠️ 同时**保留**原「投递段已真删」的判据(那条红线与 tick 删不删无关,⛔ 不许顺手丢掉)。
"""
m = imp()
src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
# ① 参数表:⛔ 不许还在 _KNOWN 里(留着 ⇒ 传 --tick 会静默退出,极难排查)
_seg = src.split("_KNOWN = {")[1][:400] if "_KNOWN = {" in src else ""
known = '"--tick"' not in _seg
# ② 分支:`if "--tick" in sys.argv` 不在
no_branch = 'if "--tick" in sys.argv' not in src
# ③ 迁移落点①②必须在(⛔ 只对源码字面 —— 这两处是新写的,字面稳定)
_sup_seg = src.split('if "--supervise" in sys.argv')[1][:6000] if 'if "--supervise" in sys.argv' in src else ""
legs_moved = ('maybe_spawn_check_agent("sessions-ended")' in _sup_seg
and 'maybe_spawn_check_agent("queue-empty")' in _sup_seg
and "_tick_park_probe()" in _sup_seg)
rc, out = run_cli(["--tick"]) # ⛔ 传它必须**被拒**(非 0 或报未知参数)
# 🔴 2026-10-01 改:原先这里写**绝对路径**指到某个具体工作区的 `tools/wb-result-hook.py`
# ⇒ 两处都不对:① 违反「技能里只用相对路径」(包去依赖"使用方的"项目目录);
# ② 宿主接线自 2026-10-01 起已改指**包内**那份 ⇒ 老写法在别的机器上恒走"文件不在,跳过",
# 看着绿、其实**什么都没验**(静默假绿)。现在按包内相对路径取自己那份钩子。
hook = HERE / "hooks" / "wb-result-hook.py"
hook_ok, hook_note = True, "(钩子文件不在,跳过)"
if hook.exists():
hs = hook.read_text(encoding="utf-8", errors="replace")
# 🔴🔴 反转:钩子必须**没有** `--tick` 调用,且**要有** `--ensure` 续命(⛔ 少了就没人拉常驻)
# ⚠️⛔ 判据**只认带引号的字面 `"--tick"`**,⛔ 不认 `--tick` 裸串 ——
# 删除说明的注释里**必然**提到它(否则读者不知道删了什么)⇒ 裸串判据**永久假红**
# (P0-13 同族:docstring 提被删项 ⇒ 只能判"调用/带引号",⛔ 不能判"出现")。
hook_ok = ('"--tick"' not in hs) and ("maybe_run_supervisor_tick" not in hs) \
and ("maybe_ensure_supervise" in hs) and ('"--ensure"' in hs)
hook_note = "钩子已无 `--tick` 调用,续命改走 `--ensure`"
# 🔴🔴 2026-10-03 **投递段退役**:本用例原判据是「`--tick` 用 `mutate=True` 调 `supervise()`
# ⇒ 它是唯一投递方」——**这条判据已随投递退役而失效**(没有"投递方"这个角色了)。
# ⚠️ ⛔ **不许把它改成"`--tick` 用 `deliver=False` 调 ⇒ 它是唯一不投递方"** ——
# 那是**换个说法说同一件事**,仍对着源码字面匹配,一重构就假红(P0-13 同族)。
# ✅ **正解=退役它,换成"投递段真删"的判据**,且判据必须**可证伪**:
# ① **真调用** `supervise({}, deliver=True, mutate=True)`(⛔ 传最"能投"的那组参数)
# ⇒ `deliver.skipped` 必须为 `retired-20261003`。
# ⚠️⛔ **不许去读生产 state 取证**(本文件 L169 的元用例明令「自测不碰生产」,
# 且那样判据依赖"生产已被新代码跑过"⇒ 换台机器必假红)。
# ② **`supervise()` 函数体内零个调用**——判据走 **AST**(⛔ 不是字面 grep:
# docstring 里**必然**提到 `_deliver_str`(要说明退役了什么)⇒ 字面判据永久假红)。
try:
_r = m.supervise({}, deliver=True, mutate=True)
except Exception as _e:
_r = {"deliver": {"skipped": "raised:%r" % (_e,)}}
_sk = str((_r.get("deliver") or {}).get("skipped") or "")
calls = _calls_in(src, "supervise")
r_nocall = ("_deliver_str" not in calls) and ("follow_for_topic" not in calls)
return [("`--tick` ⛔ 已从参数表移除(留着=传它会静默退出)", known),
("`--tick` 分支已删(源码无 `if \"--tick\" in sys.argv`)", no_branch),
("⛔ 传 `--tick` 被拒(未知参数,rc=%s)" % (rc,),
rc != 0 or ("未知参数" in str(out))),
("🔴 迁移落点:`sessions-ended`/`queue-empty`/park 探针**都在 `--supervise` 内**",
legs_moved),
("🔴 2026-10-03 退役判据①:真调用 `supervise(deliver=True, mutate=True)` ⇒ `deliver.skipped` 必为 "
"`retired-20261003`(实测 = %r)" % (_sk,), _sk == "retired-20261003"),
("🔴 2026-10-03 退役判据②:`supervise()` 体内**零个** `_deliver_str`/`follow_for_topic` **调用**"
"(AST 判据,⛔ 不看 docstring;实测调用集 = %s)" % (sorted(calls) or "空",), r_nocall),
(hook_note, hook_ok)]
def _calls_in(src: str, fname: str) -> set:
"""🔴 **函数 `fname` 体内真正被调用的名字集合**(走 AST,⛔ 不做字面 grep)。
🔴🔴 **为什么必须走 AST**(2026-10-03 实测踩到):退役说明的 docstring 里**必然**提到
被删的函数名(否则读者不知道删了什么)⇒ 任何 `grep "_deliver_str(" in <函数体切片>`
都**永久假红** ⇒ 只能判"**调用**",⛔ 不能判"**出现**"。
⚠️ 附带好处:AST 只认 `Name`/`Attribute` 的 `func` 位 ⇒ 注释、字符串、docstring 一律不算。
"""
import ast as _ast
try:
tree = _ast.parse(src)
except Exception:
return {"<parse-error>"}
for node in _ast.walk(tree):
if isinstance(node, _ast.FunctionDef) and node.name == fname:
out = set()
for sub in _ast.walk(node):
if isinstance(sub, _ast.Call):
f = sub.func
if isinstance(f, _ast.Name):
out.add(f.id)
elif isinstance(f, _ast.Attribute):
out.add(f.attr)
return out
return {"<no-such-function>"}
@case("对账:僵尸(台账 running 但没人跑)⇒ 标 blocked")
def t_reconcile_zombie():
# 🔴 2026-09-30 修(**本用例曾假红**):僵尸的判据是「**除观察者外无人跑** ∧ 无 IN_PROGRESS」——
# 而测试机上**天天有别的工作会话在跑** ⇒ 前提不成立 ⇒ 不能判它红(⛔ 也不许假装绿)。
# 正确姿势:**前提不成立就 SKIP**(读不出宿主库、或确实有别的会话在跑)。
m = imp()
put_tasks({"Z1": {"state": "running", "by": "已经死掉的棒", "t": time.time()}})
r = m.reconcile({})
if r.get("skip"):
put_tasks({})
return [("SKIP:读不到宿主库(%s)" % r["skip"], True)]
# 🔴 2026-10-01 加:僵尸分支的**完整前提** =「**除观察者外**无在跑会话」∧「无 IN_PROGRESS 运行」。
# 旧用例只靠 `adopted` 反推**前一半** ⇒ 后半没覆盖 ⇒ **自动化会话跑本自测时**,
# 它**自己**的 automation run 必然还是 `IN_PROGRESS`、且它自己就是唯一在跑的会话
# ⇒ 前提不成立却没 SKIP ⇒ **必然假红**(2026-10-01 实测:整套只有本条红,
# 而 `reconcile` 返回 `adopted=[] zombie=[]`)。⚠️ 同一类假红的另一半,照既有姿势补 SKIP。
try:
_me = (os.environ.get("CODEBUDDY_SESSION_ID") or "")[:8]
_con = m._db()
_inprog = int(_con.execute("select count(*) from automation_runs "
"where status='IN_PROGRESS'").fetchone()[0])
_others = [s for (s,) in _con.execute("select id from sessions where status='working'")
if not (_me and str(s)[:8] == _me)]
_con.close()
except Exception:
_inprog, _others = 0, []
if _others or _inprog:
put_tasks({})
return [("SKIP:前提不成立(另有 %d 个会话在跑 · %d 个自动化运行 IN_PROGRESS)"
% (len(_others), _inprog), True)]
if r.get("adopted"):
put_tasks({})
return [("SKIP:当前确有 %d 个别的会话在跑 ⇒ 『除观察者外无人跑』前提不成立"
% len(r["adopted"]), True)]
v = m._load_tasks().get("Z1") or {}
ok = v.get("state") == "blocked" and "执行者已消失" in str(v.get("block_reason"))
put_tasks({})
return [("Z1 被标 blocked 且原因是『执行者已消失』", ok)]
@case("对账:幂等(连跑两次结论一致)")
def t_reconcile_idempotent():
# 🔴 2026-09-30 修(**同一类假红**):第 1 次会**收编**当前正在跑的别的工作会话(环境相关),
# 第 2 次才进入"没有新东西"的稳态 ⇒ 幂等性只能比较 **第 2 次 vs 第 3 次**。
run_cli(["--reconcile"])
_, o1 = run_cli(["--reconcile"])
_, o2 = run_cli(["--reconcile"])
put_tasks({})
return [("第 2 次 = 第 3 次输出一致", o1.strip().splitlines()[:1] == o2.strip().splitlines()[:1]),
("第 2 次已无新收编(『收编 0 条』)", "收编 0 条" in o1)]
@case("CLI:未知参数 ⇒ rc=2 且**不落常驻**")
def t_unknown_arg():
rc, out = run_cli(["--nonsense"], timeout=20)
return [("rc==2", rc == 2), ("提示未知参数", "未知参数" in out)]
@case("CLI:--where / --reqs 可跑且 rc=0")
def t_basic_cli():
rc1, o1 = run_cli(["--where"])
rc2, _ = run_cli(["--reqs"])
return [("--where rc==0 且打印路径", rc1 == 0 and "workspace" in o1), ("--reqs rc==0", rc2 == 0)]
@case("结构:关键名字/设施在位(防『误删常量』重演)")
def t_symbols():
src = (HERE / "collabd.py").read_text(encoding="utf-8")
need = ["CLAIMS", "SELF_SID", "STALE", "DOING_TTL", "def reconcile(", "def supervise(",
"def _deliver_str(", "def _guard_says_stop(", "def parse_session_name(",
"def topic_of(", "def load_goal(", "GOAL_F"]
miss = [n for n in need if n not in src]
return [("关键符号齐全(缺:%s)" % (miss or "无"), not miss)]
@case("结构:所有 subprocess 调用都带 creationflags(不显窗)")
def t_nowindow():
bad = []
for fn in ("collabd.py", "guard.py"):
for i, l in enumerate((HERE / fn).read_text(encoding="utf-8").splitlines(), 1):
if "subprocess.run(" in l or "subprocess.Popen(" in l:
seg = " ".join((HERE / fn).read_text(encoding="utf-8").splitlines()[i - 1:i + 4])
if "creationflags" not in seg:
bad.append("%s:%d" % (fn, i))
return [("无『没带 creationflags』的子进程调用(%s)" % (bad or "无"), not bad)]
@case("结构:fatal 不再 exit 0(防『假绿』)")
def t_failopen():
src = (HERE / "collabd.py").read_text(encoding="utf-8")
i = src.find('log("fatal %s" % e)')
seg = src[i:i + 200] if i >= 0 else ""
return [("fatal 分支是非零退出", "sys.exit(1)" in seg), ("⛔ 不是 exit(0)", "sys.exit(0)" not in seg)]
@case("结构:投递段**已真删**(⛔ 旧「投递方唯一」判据已于 2026-10-03 退役)")
def t_deliver_switch():
src = (HERE / "collabd.py").read_text(encoding="utf-8")
m = imp()
# 🔴🔴 2026-10-03:本用例原有一条判据「`--tick` 是唯一投递方(`supervise(st, deliver=True)`)」
# ——**投递退役后这个角色已不存在**,那条判据永久拿不到状态。
# ⚠️ ⛔ **不许把它改成 `supervise(st, deliver=False)`**(=换个说法说同一件事,仍是字面匹配脆断)。
# ✅ 退役 + 换可证伪判据:`supervise()` 体内**零个** `_deliver_str`/`follow_for_topic` **调用**
# (AST 判据 —— ⛔ 字面 grep 会被 docstring 里的退役说明永久假红,见 `_calls_in` 的注释)。
calls = _calls_in(src, "supervise")
r = m.supervise({}, deliver=True, mutate=True)
sk = str((r.get("deliver") or {}).get("skipped") or "")
return [("supervise 有 deliver 参数", re.search(r"def supervise\(st: dict, deliver: bool", src) is not None),
("supervise 有 mutate 参数(签名兼容保留,⛔ 体内已不读)",
re.search(r"def supervise\(st: dict, deliver: bool = True, mutate: bool = True\)", src) is not None),
("one_round 走**纯投影**(deliver=False, mutate=False)",
re.search(r"supervise\(st,\s*deliver=False,\s*mutate=False\)", src) is not None),
("🔴 2026-10-03 退役判据①:`supervise()` 体内**零个** `_deliver_str`/`follow_for_topic` **调用**"
"(AST;实测调用集 = %s)" % (sorted(calls) or "空",),
("_deliver_str" not in calls) and ("follow_for_topic" not in calls)),
("🔴 2026-10-03 退役判据②:真调用 `supervise(deliver=True, mutate=True)` ⇒ "
"`deliver.skipped` 必为 `retired-20261003`(实测 = %r)" % (sk,),
sk == "retired-20261003"),
("wake_round 已标注停用", "已停用" in src)]
@case("主会话:**解析出来的**(不只是读登记)+ ⛔ 拒绝盲投(防『投错窗口』回潮)")
def t_main_resolution():
"""用户 2026-09-30:「协作机制能发现 主会话 换了吗」——旧答案是**不能**(登记静态+路径依赖,
且看板与投递共用同一份登记 ⇒ 一起钉在旧 id 上)。本用例把修法钉住。"""
src = (HERE / "collabd.py").read_text(encoding="utf-8")
m = imp() # 🔴 2026-10-01:本用例起要**真跑函数**(见下面那条"能吃类别集合")
return [
("有 resolve_main(登记失效时按当前事实解析)", "def resolve_main(" in src),
("登记**有效才认**(`reg in live`)", "or reg in live" in src),
("按**工作区**解析(用户定则:**一个工作区一条主会话**)", "def _same_ws(" in src),
("工作区比较**同时**小写+统一斜杠(⛔ 防 `E:/x` 与 `E:\\x` 判成两个)",
'replace("\\\\", "/")' in src),
("🔴 按工作区解析时**排除「协作侧」命名**(2026-10-02 两类改造:唤醒/跟进已退役 ⇒ 只排 `worker`)"
"—— 接续会话是干活的棒,⛔ 不是主会话候选(否则它冒充主会话 ⇒ 通知投给它自己)",
'_role not in ("worker",)' in src),
("⛔ **不得按 `status='working'` 筛主会话**(主会话两轮之间是空闲 ⇒ 加了必然漏掉它)",
"status='working' \"\n \"order by last_activity_at" not in src),
("⛔ **不得按 `is_background_automation` 排主会话**(接续会话本身就是自动化起的 —— 实测主会话也是 1)",
"coalesce(is_background_automation, 0) = 0" not in src),
("🔴 **认「主控」前缀**(用户 2026-09-30 定名:主会话的显式标记)",
'startswith("主控")' in src and '"prefix:主控"' in src),
("解析不出 ⇒ 明确报 `no-main-session`", '"no-main-session"' in src),
("目标不在活会话里 ⇒ 明确报 `main-not-live`", '"main-not-live"' in src),
("🔴 **盲选回落已删除**(⛔ 不再随手投给任意一条会话)",
re.search(r'g = next\(\(x for x in gws if x\["sessionId"\]\), None\)', src) is None),
# ── 🔴 2026-09-30 新增:**同一工作区 · 多任务类别**(用户要求"同一个工作区 多会话协作")───
("🆕 有 `resolve_mains()`(按**任务类别**各解析一条主会话)", "def resolve_mains(" in src),
("🆕 有 `_scan_mains()`(与 `resolve_main` **共用同一遍扫描** ⇒ **同一份 live**,⛔ 不各扫各的=防两边漂移)",
"def _scan_mains(" in src and src.count("_scan_mains(st, live)") >= 2),
# 🔴 2026-10-01 体检修发现的**投递断点**:解析出一条**已经结束**的「主控」会话当投递目标
# ⇒ 投递一路 `main-not-live`(程序在跑,通知永远发不出去)。⇒ **候选只认活着的**。
("🆕 候选**只认活着的**:`_pick_live` 在位且两处解析都用它",
"def _pick_live(" in src and src.count("_pick_live(named, live)") >= 2
and src.count("_pick_live(cand, live)") >= 2),
# 🔴🔴 2026-10-01 **两条旧断言被本轮口径推翻 ⇒ 改判据**(⛔ 不是删掉 —— 性质还得有人守):
# 用户逐字:「**换新会话 唤醒的是 跟进会话,主会话只能是用户触发**」
# ⇒ `_deliver_str` 的投递目标**从"该类别的主会话"改成"该类别的跟进会话"**
# (`_rms = resolve_mains(st, …)` + `main_for_topic(_rms, topic)` **故意**从源码里消失了)。
# ⚠️ 那两条旧断言查的正是这两个字面量 ⇒ 留着就是**恒红**(不是代码坏了,是**判据过时**)。
# ⚠️ 但它守的**性质**("按类别选、⛔ 不投错窗口")必须继续守 ⇒ 换成查新函数,并补两条。
("🆕 投递目标经 `follow_for_topic()` **按类别**取(⛔ 不再是主会话、也不再是「唯一那一条」)",
re.search(r"_rft = follow_for_topic\(st, topic,", src) is not None),
("🆕 `follow_for_topic` 对「已登记类别」**严格取该类别**(⛔ 不回落别类别=防投错窗口)",
re.search(r"if tp and tp in tps:", src) is not None),
("🔴 `follow_for_topic` **绝不返回主会话**(用户口径:主会话只由用户触发)"
"—— 2026-10-02 两类改造后**真调用**恒返回 `sid=\"\"`(短路退役)⇒ 更不可能返回主会话",
(not str(imp().follow_for_topic({}, "X", []).get("sid") or ""))),
("🔴 解析不出跟进会话 ⇒ 记 `no-follow-session` + **喊用户**(⛔ 不再降级投主会话)",
'"no-follow-session"' in src and 'follow-not-live' in src
and '"no-main-session"' not in src.split("def _deliver_str(")[1].split("\ndef ")[0]),
("🆕 `main_for_topic` 对「已登记类别」**严格取该类别**(⛔ 不回落别类别=防投错窗口)",
'if tp and tp in (rms.get("topics") or [])' in src),
("🆕 `_deliver_str` 支持 `topic=` 参数(默认空 ⇒ 走 default,向后兼容)",
re.search(r'def _deliver_str\(text: str, key: str, st: dict, topic: str = ""\)', src) is not None),
# 🔴🔴 2026-10-03 退役:本条原判据 `'topic=str((v or {}).get("line") or "")' in src`
# 指的是 `supervise()` 里那行 `_deliver_str(..., topic=…)`——**投递段已真删 ⇒ 该行不存在**。
# ⚠️ ⛔ 不许改成 `topic="" in src` 之类(=断言恒真,P0-13 同族)。
# ✅ 退役 + 换可证伪判据:`_deliver_str()` 本体**仍支持 `topic=` 参数**
# (它是"将来重建收件人"的实现,签名不许退化)⇒ 改成断言**函数签名里有 `topic: str = ""`**,
# 那是真判据(谁把参数删了就会红),且**不是**靠一个字面串匹配调用点。
("🆕 `supervise()` 曾按台账条目的 `line` 传 `topic=`(⛔ 2026-10-03 随投递退役)",
"topic=str((v or {}).get(\"line\") or \"\")" not in src),
("🆕 退役后仍保留:`_deliver_str()` 本体**签名仍带 `topic=`**(重建收件人时的实现)",
re.search(r'def _deliver_str\(text: str, key: str, st: dict, topic: str = ""\)', src) is not None),
# 🔴 2026-10-01 改:原来这条是**纯源码匹配** `"for _t in _as_list(short):" in src` ——
# 我今天给 `_in_project` 加了第三条判据、顺手把这一行提成 `_tps = _as_list(short)`,
# 行为**一模一样**,它却报红了(**脆断**:对着源码字面写断言,一重构就假红)。
# ⇒ 改成**行为断言**(真喂一个 list 进去看结果),源码那半只留"函数还在"。
("🆕 归属判据能吃**类别集合**(`_in_project` 的 main_sid / short 传 list 都对)",
"def _as_list(" in src
and m._in_project("", "[协作]-[B类]-x", ["M1"], ["A类", "B类"])
and m._in_project("M1", "随便什么标题", ["M1", "M2"], ["A类"])),
("🆕 收尾确认**排除全部主会话**(⛔ 否则别类别的主会话被算成「在跑」⇒ 永远拦住)",
'mains = list(_rms0.get("all_sids") or [])' in src and "if sid in mains:" in src),
("🆕 两处 `_goal_topics()` 同款(技能里 goal.topics 是**唯一**类别源,缺省回落 `short`)",
src.count("def _goal_topics(") == 1 and '"topics"' in src),
("换了目标 ⇒ 跟随 + 显式告警(⛔ 不静默)",
"def _follow_main(" in src and "主会话变更:" in src and "need_user(" in src),
]
@case("主会话:看板与投递**同一套判据**(⛔ 防两处漂移 —— 漂了就没人能发现换主会话)")
def t_main_same_rule():
a = (HERE / "collabd.py").read_text(encoding="utf-8")
b = (HERE / "board.py").read_text(encoding="utf-8")
pat = r'def _main_sid\(st: dict\) -> str:'
return [
("两处都定义了 _main_sid", re.search(pat, a) is not None and re.search(pat, b) is not None),
("两处都按 roles -> wake 的顺序取(逐字同款)",
'st.get("roles")' in a and 'st.get("roles")' in b
and 'st.get("wake")' in a and 'st.get("wake")' in b),
]
@case("主会话:**退役角色不许当候选** —— 行为级(真造宿主库 + 真跑扫描,⛔ 不只测那个小函数)")
def t_main_retired_not_candidate():
"""🔴🔴 2026-10-04 21:5x 新增(用户拍板「候选一」)。
**为什么必须是行为级、⛔ 不能只测 `is_retired_role_title()` 本身**(**变异验证当场抓到**):
我第一版只加了"这个函数对这几个样本返回什么"的断言 ⇒ 植入变异
**把闸从 `_scan_mains()` 的筛子里整段拆掉** ⇒ 那些断言**照样全绿**(`grep MUTANT` = 1
证明变异真植进去了,`PASS 87 / FAIL 0` 却是假的)—— 因为**没有任何一条测到"闸被用上了"**。
⇒ 正解=造一个**真宿主库**,里面放一条 `[跟进]-…` 与一条真主会话,
**真跑一遍扫描**,看结果里有没有那条退役角色。
⚠️ 这正是"判定标准恒绿"的典型形态:测了零件、没测装配。
**被测对象**:`collabd.py::_scan_mains()` —— 它是要**扫库**的那一侧(board 侧同款判据另有对账)。
"""
m = imp()
import sqlite3 as _sq
import tempfile as _tf
import shutil as _sh
td = _tf.mkdtemp(prefix="selftest-retired-")
ws = Path(td) / "wsX"
(ws / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True)
(ws / "tmp" / "supervise-inbox" / "goal.json").write_text(
json.dumps({"id": "wsX", "title": "X 区目标", "short": "wsX"}, ensure_ascii=False),
encoding="utf-8")
db = Path(td) / "fake-host.db"
now = int(time.time() * 1000)
try:
cn = _sq.connect(str(db))
cn.execute("create table sessions (id text primary key, title text, custom_title text, "
"status text, cwd text, updated_at integer, last_activity_at integer, "
"deleted_at integer)")
# ① 退役角色(`[跟进]-…`)—— 改前会被收进候选(实测 `6ab1463e` 就是这个形状)
# ② 现役真主会话(无前缀)—— ⛔ 不许被误排
# ③ 正牌 worker —— 旧口径就该排它(对照组)
# 🔴🔴 `custom_title` 必须写 **`None`**,⛔ 不许写 `""`(**夹具说谎的第二个坑,实测踩到**):
# 查询是 `coalesce(custom_title, title, '')` ⇒ 写 `""` 时 `coalesce` **返回 `""`**
# (空串不是 NULL,不会回落到 `title`)⇒ 每条会话拿到的标题都是空串
# ⇒ 退役闸/角色判据**全都测不出东西**、用例恒绿而**毫无意义**。
# ✅ 真库里 `custom_title` 实测就是 **`None`**(`57f58ecf` 等逐条验过)⇒ 夹具照抄真库形态。
# ⚠️ 这与 P0-53 是同一个形状:**合成区必须让被测对象读到真实判据**,
# ⛔ 不许靠"反正断言过了"糊过去(这个坑正是我自己写夹具时踩出来的)。
_cwd = str(ws).replace("\\", "/")
for i, t in enumerate(("[跟进]-机制排查与修复-队列跟进",
"复盘失败并避免重犯",
"[协作]-[机制排查与修复]-S12 收尾")):
cn.execute("insert into sessions values (?,?,?,?,?,?,?,?)",
("sid-%d" % i, t, None, "completed", _cwd,
now - i * 1000, now - i * 1000, None))
cn.commit()
cn.close()
# 🔴 让 `_scan_mains()` 扫到这个库 + 认这个工作区。两条都要对准(读源码得的键名):
# · `collabd.py::_db()` 读 `C["host_db"]`(配置)或 `CODEBUDDY_CONFIG_DIR/workbuddy.db`
#  ⇒ ⛔ 没有 `DSHS_DB_URL` 这条路(我第一版按它写 ⇒ 夹具指不到库)。
# · `collabd.py::_same_ws()` 比的是**模块级 `WS`**(加载时定的)⇒ 也必须在加载时给对。
# 两者都靠**改配置再重载模块**拿(⛔ 不是改 env;`WS` 不是 env 读的)。
_cfg = Path(td) / "collabd.testconfig.json"
_cfg.write_text(json.dumps({"host_db": str(db).replace("\\", "/"),
"workspace": str(ws).replace("\\", "/")},
ensure_ascii=False), encoding="utf-8")
_old_cfg = os.environ.get("COLLABD_CONFIG")
_old_wsv = os.environ.get("COLLABD_WORKSPACE")
os.environ["COLLABD_CONFIG"] = str(_cfg)
os.environ["COLLABD_WORKSPACE"] = str(ws).replace("\\", "/")
try:
_u = __import__("importlib.util", fromlist=["util"])
_sp = _u.spec_from_file_location("_cb_retired", HERE / "collabd.py")
_mc = _u.module_from_spec(_sp)
_sp.loader.exec_module(_mc) # ⚠️ 必须**重新加载**:`WS` 是模块级常量
sc = _mc._scan_mains({})
finally:
for _k, _v in (("COLLABD_CONFIG", _old_cfg), ("COLLABD_WORKSPACE", _old_wsv)):
if _v is None:
os.environ.pop(_k, None)
else:
os.environ[_k] = _v
cand = [str(s) for s in (sc.get("cand") or [])]
by_topic = sc.get("by_topic") or {}
err = str(sc.get("err") or "")
except Exception as _e: # pragma: no cover
return [("用例未跑起来:%s" % _e, False)]
finally:
_sh.rmtree(td, ignore_errors=True)
return [
("🔴 夹具扫到库了(⛔ 空扫描 ⇒ 下面几条会恒绿 —— 那正是『测了零件没测装配』)",
not err),
("🔴 **退役角色 `[跟进]-…` ⛔ 不进候选**(sid-0;改前会进 ⇒ 看板「主会话」位上坐着它)",
"sid-0" not in cand),
("(对照组)**真主会话 ⛔ 不许被误排**(sid-1;它是无前缀的,排它=把真主会话弄丢)",
"sid-1" in cand),
("(对照组)正牌 worker `[协作]-…` ⛔ 不进候选(旧口径,⛔ 不许被本次改动破坏)",
"sid-2" not in cand),
("🔴 退役角色 ⛔ 不进 `by_topic`(改前的真实病症:`main_by_topic` 两个类别**全指向 "
"`[跟进]` 会话**)",
all(str((v or {}).get("sid") or "") != "sid-0" for v in by_topic.values())),
]
@case("主会话:登记为 main 但**已哑** ⇒ ⛔ 不认它 + `--declare main` 是**换**不是「加」")
def t_main_deaf_and_declare():
"""🔴🔴 2026-10-01 修 **V2「主会话可响应」的硬阻塞**(两个缺陷叠在一起,缺一条都治不好)。
**缺陷①**:`resolve_main` 的第①条快路原为 `if reg and (not live or reg in live)` ——
**只查"活着",⛔ 不查"哑"**。而"哑"(会话诊断日志撞 ~10 MiB ⇒ 宿主拒写 ⇒
**界面永不刷新**)**照样在 `live` 里** ⇒ 一条哑掉的主会话来一次认一次,一路投到 `target-deaf`。
⚠️ 而 ② 走的 `_pick_live()` **本来就排哑**(2026-10-01 专为"别再投给哑会话"加的)——
**① 却从没接上它** ⇒ 同一条哑会话「② 排得掉、① 排不掉」(判据打架,当日第三次同族)。
**缺陷②**:`--declare --role main` 是**加**不是**换**(P0-11 的正解①就是它,号称"零代码改动立刻可用")
—— 而 `_main_sid()` 取 `roles` 里**第一条** `main`,Python dict 对已存在的键赋值**不改位置**
⇒ 老那条**永远排前面** ⇒ 新登记的主会话**永远轮不到** ⇒ 逃生口形同虚设。
🔴 而且它掩盖得更深:`_deliver_str` 的**粗判不传活会话**(`resolve_mains(st, [])`)
⇒ `_pick_live` 在 `live` 为空时**回退 `c[0]`** ⇒ 每轮先认下那条哑的、**直接返回**,
**走不到后面"拿锁后用活网关列表重算"那一步**。
"""
m = imp()
A = "aaaa1111-0000-0000-0000-000000000001"
B = "bbbb2222-0000-0000-0000-000000000002"
# ── 缺陷①:把 A 伪造成"哑",A 登记在 B 前面 ──
_orig = m._deaf_sids
try:
m._deaf_sids = lambda: {A}
r_live = m.resolve_main({"roles": {A: "main", B: "main"}}, [A, B])
# 粗判那条路(不传活会话)⇒ 只能拿第一条 —— 这正是"必须让登记权威化"的理由
r_coarse = m.resolve_main({"roles": {A: "main", B: "main"}}, [])
finally:
m._deaf_sids = _orig
# ── 缺陷②:`--declare --role main` 必须把别的 main 降级 ──
# 在**测试区**里造一份 state(⛔ 不碰生产),用完还原
SP = TIN / "collabd-state.json"
bak = SP.read_text(encoding="utf-8") if SP.exists() else None
SP.write_text(json.dumps({"roles": {A: "main", B: "worker"}}, ensure_ascii=False), encoding="utf-8")
_env = _mk_env()
_env["CODEBUDDY_SESSION_ID"] = B
p = subprocess.run([PY, "-u", str(HERE / "collabd.py"), "--declare", "--role", "main"],
cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=_env, timeout=60, errors="replace")
_st = json.loads(SP.read_text(encoding="utf-8"))
_out = p.stdout or ""
if bak is None:
SP.unlink(missing_ok=True)
else:
SP.write_text(bak, encoding="utf-8")
_roles = _st.get("roles") or {}
S = (HERE / "collabd.py").read_text(encoding="utf-8")
return [
("🔴 ① 排哑后才认下**活着的**那条登记(拿到 %s)" % (r_live["sid"][:8] or "(空)"),
r_live["sid"] == B and r_live["source"] == "roles:main"),
("🔴 ① 换了登记的那条 ⇒ **报 `switched_from`**(调用方才会降级+告警,⛔ 不静默换)",
r_live["switched_from"] == A),
("🔴 ① 走的是**同一个** `_pick_live`(⛔ 不再有两台判据)",
"_reg = _pick_live(_rm, live)" in S),
("⚠️ 粗判(**不传活会话**)退回第一条 ⇒ 这就是「必须让登记权威化」的理由(坑已固化)",
r_coarse["sid"] == A),
("🔴 ② `--declare --role main` 把原 main **降级为 worker**(换,⛔ 不是加)",
_roles.get(B) == "main" and _roles.get(A) == "worker"),
("🔴 ② 且**说出口**(⛔ 不静默改别人的角色)", "已降级为 worker" in _out),
]
@case("结构:守护生命周期(优雅退出设施在位)")
def t_graceful():
src = (HERE / "guard.py").read_text(encoding="utf-8")
return [("有 _shutdown(优雅收尾)", "def _shutdown(" in src),
("子进程打 DSH_GUARDED 标", "DSH_GUARDED" in src),
("子程序侧有 _guard_says_stop", "_guard_says_stop" in (HERE / "collabd.py").read_text(encoding="utf-8"))]
@case("归属判据:看板 ≡ 收尾确认(⛔ 防两处漂移;⛔ 不按 cwd 推断)")
def t_scope_parity():
"""🔴 用户 2026-09-30 两条明示:「**要明确哪些会话是属于某个需求目标项目的**」+
「**这个看板上应该明确显示是哪个需求目标项目**」。
`collabd._in_project()`(`--ready-next` 用)与 `board.in_project()`(看板用)是**重复实现**
⇒ 一旦漂移就会出这种怪事:**看板说"本项目没人在跑",而 `--ready-next` 被别的项目的会话拦死**。
本用例把两者摆到同一张用例表上逐例比对(含"AI变现日报"这类**别人家的会话**必须判否)。
"""
code = r'''
import importlib.util, inspect, json, sys
sys.dont_write_bytecode = True # ⛔ 别让测试自己往技能目录里落 __pycache__
def load(name, path):
spec = importlib.util.spec_from_file_location(name, path)
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m
H = sys.argv[1]
cb = load("cb", H + "/collabd.py")
bd = load("bd", H + "/board.py")
cases = [
("main1", "任意标题(主会话)", "main1", "手机接入", True),
("aaa", "[协作]-[手机接入]-N10 复测", "main1", "手机接入", True),
("bbb", "AI变现日报 · 今日生成", "main1", "手机接入", False),
("ccc", "随便一个标题", "main1", "手机接入", False),
("ddd", "[其他主题]-x", "main1", "手机接入", False),
("eee", "x", "main1", "", False), # short 空 ⇒ 只剩主会话能命中
("ggg", "x", "ggg", "", True), # 主会话判据 ⛔ 不依赖 short
("hhh", "[其他主题][手机接入]", "main1", "手机接入", True),
("fff", "[手机接入] 加个 [手机接入x] 干扰", "main1", "手机接入", True),
]
bad = []
for sid, ti, msid, short, exp in cases:
a = bool(cb._in_project(sid, ti, msid, short))
b = bool(bd.in_project(sid, ti, {"main_sid": msid, "short": short}))
if a != b:
bad.append("%s 判据漂移(collabd=%s / board=%s)" % (sid, a, b))
if a != exp:
bad.append("%s 实得 %s / 期望 %s" % (sid, a, exp))
# 🆕 2026-09-30:**同一个工作区 · 多任务类别**(用户要求「通过协作会话名称前缀区分具体执行会话」)——
# 两处实现都必须认"**任一**已登记类别",且⛔ **不得**把未登记的类别算进来。
mt = [
# sid, 标题, 默认主会话, 类别清单, 期望
("m1", "主控 · 机制线", "m1", ["手机接入", "机制线"], True), # 默认主会话
("w1", "[协作]-[机制线]-唤醒A", "m1", ["手机接入", "机制线"], True), # 第 2 类别
("w2", "[协作]-[手机接入]-N9", "m1", ["手机接入", "机制线"], True), # 第 1 类别
("w3", "[协作]-[别的类别]-x", "m1", ["手机接入", "机制线"], False), # 未登记 ⇒ 不算
("w4", "唤醒机制线专项(无方括号)", "m1", ["手机接入", "机制线"], False), # 归属判据要求 `[类别]`
("w5", "[协作]-[机制线]-x", "m1", ["手机接入"], False), # 只登记一类 ⇒ 另一类不算
("w6", "x", "m1", [], False), # 无类别 ⇒ 只剩主会话判据
("w7", "x", "w7", ["手机接入", "机制线"], True), # 主会话判据与类别无关
]
for sid, ti, msid, tps, exp in mt:
a = bool(cb._in_project(sid, ti, msid, tps))
b = bool(bd.in_project(sid, ti, {"main_sid": msid, "main_sids": [msid] if msid else [],
"topics": tps, "short": (tps[0] if tps else "")}))
if a != b:
bad.append("%s 多类别判据漂移(collabd=%s / board=%s)" % (sid, a, b))
if a != exp:
bad.append("%s 多类别实得 %s / 期望 %s" % (sid, a, exp))
# 🆕 结构对齐:两处 `_scan_*` 的 `by_topic` 值**必须是 dict**({sid,source,explicit})——
# ⛔ 实测踩到:board 曾存裸 sid ⇒ 调用方把整个 dict 当 sid 塞进 `all_sids`(静默错值)。
_cbs = inspect.getsource(cb)
_bds = inspect.getsource(bd)
for _nm, _s in (("collabd", _cbs), ("board", _bds)):
if '"explicit"' not in _s or '"by_topic"' not in _s:
bad.append("%s 的 by_topic 结构不合规(须存 {sid,source,explicit})" % _nm)
sig = str(inspect.signature(bd.in_project))
if "cwd" in sig:
bad.append("board.in_project 仍带 cwd 参数:%s" % sig)
src = open(H + "/board.py", encoding="utf-8").read()
if "project_scopes" in src:
bad.append("board.py 仍残留 cwd 版的 project_scopes()")
print(json.dumps({"bad": bad, "sig": sig}, ensure_ascii=False))
'''
p = subprocess.run([PY, "-c", code, str(HERE)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=_mk_env(), timeout=30, errors="replace")
try:
r = json.loads((p.stdout or "").strip().splitlines()[-1])
except Exception:
return [("子进程跑不起来(rc=%s):%s" % (p.returncode, (p.stdout or "")[-300:]), False)]
return [("15 例判据一致且与期望相符(7 基础 + 8 多类别;%s)" % ("、".join(r["bad"]) or "全对"), not r["bad"]),
("board.in_project 不带 cwd 参数(%s)" % r["sig"], "cwd" not in r["sig"])]
@case("看板:风格系统(三风格令牌在位 + 组件里零硬编码色)")
def t_board_styles():
"""🔴 用户 2026-09-30:「画架构图参考 archify 的样式,**右上角加个风格切换(保留当前风格)**」。
两件必须守住:
① **`base`(保留的当前风格)必须在**,且**跟随系统深浅色** —— ⛔ 删了就等于改了用户的原风格。
② **组件里不许出现硬编码颜色** —— 颜色只许出现在令牌块(`--color-*` 等)里。
⚠️ 一旦有人在 `.card{}`/SVG 里写死 `#fff`,换风格就会**花**(这是最容易犯的)。
"""
p = HERE.parent / "assets" / "board.html"
src = p.read_text(encoding="utf-8")
need = ['[data-style="archify-dark"]', '[data-style="archify-light"]',
':root:not([data-style]), :root[data-style="base"]', # base 跟随系统深色
'id="stylesw"', 'function applyStyle(', "dsh-board-style"] # 切换件 + 持久化
misc = [n for n in need if n not in src]
bad = []
for i, l in enumerate(src.splitlines(), 1):
s = l.strip()
if s.startswith(("--color-", "--font", "--dot", "--node", "--radius", "--shadow")):
continue # 令牌块本身当然有颜色值
if re.search(r"#[0-9a-fA-F]{3,8}\b", l) or re.search(r"rgba?\(\s*\d", l):
bad.append("board.html:%d" % i)
return [("三风格令牌 + 切换件 + 持久化齐全(缺:%s)" % (misc or "无"), not misc),
("组件里零硬编码颜色(越界:%s)" % (bad[:4] or "无"), not bad)]
@case("看板:验收摘要行 ⛔ 不许把『一条判据都没有』说成『无』(同族红线)")
def t_board_acc_summary():
"""🔴 2026-09-30 跑整体验证时抓到:`board.py` 的命令行摘要写的是
`[k for k,v in acc.items() if v!='pass'] or "无"` —— `acc` 已滤掉 `_` 说明行,
所以**一条有效判据都没有**时列表为空 ⇒ 打印「验收非 pass:**无**」,
读起来就是「全部 pass」。**形状同族**:不崩溃,只是少说一句话。
⇒ 统一到 `_acc_summary()`:空 ⇒ 显式「判不出来,⛔ 不因此判完成」。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_selftest", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
s_empty = b._acc_summary({})
s_pass = b._acc_summary({"V1": "pass", "V2": "pass"})
s_open = b._acc_summary({"V1": "pass", "V2": "fail"})
return [("空 ⇒ 显式『判不出来』(%s)" % s_empty[:26], "判不出来" in s_empty and "不因此判完成" in s_empty),
("⛔ 空时不得只说『无』(%s)" % s_empty[:12], s_empty.strip() != "无"),
("全 pass ⇒ 说清条数(%s)" % s_pass, "pass" in s_pass and "2" in s_pass),
("有未过 ⇒ 点名(%s)" % s_open, "V2" in s_open)]
@case("看板:『未归类』不许被静默丢掉(同族红线:读到了就要说)")
def t_board_orphan_visible():
"""🔴 2026-09-30 跑整体验证时在**真实工作区**抓到:类别清单已迁到「任务类别」,
而台账里还躺着**跨工作区时代的旧线名**(各 2 件且都已完成)。
旧版:这些行也当"分工位"进 `labor`,架构图又只画前 4 格 ⇒ **那几格连同里面的件整块消失**
(读者只看到 4 个"件 0 · 完成 0")。**形状=不崩溃,只是少说一句话** —— 最难发现的那类。
现在:`kind='legacy'` ⇒ **不算类别**(⛔ 不让工作区名冒充类别),但**折叠成一格画出来**
+ **图外点名线名与件数** ⇒ 两头都堵住。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_orphan", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
# ① `_orphan` 只收 legacy 行,且件数**求和**(⛔ 不是只数条数)
o0 = b._orphan([{"line": "唤醒机制", "kind": "topic", "total": 3, "done": 3}])
o1 = b._orphan([{"line": "唤醒机制", "kind": "topic", "total": 3, "done": 3},
{"line": "old-ws", "kind": "legacy", "total": 2, "done": 2, "open": 0},
{"line": "old-ws2", "kind": "legacy", "total": 1, "done": 0, "open": 1}])
# ② 结构:看板真的会把它画出来 + 真的会点名(⛔ 只改数据不改渲染 = 没修)
H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
need = ["kind==='legacy'", "未归类", "不属任何当前任务类别",
"不在当前任务类别清单里", # 图外提示要点名
"旧值 · 不在当前任务类别清单"] # 台账表要标注
miss = [n for n in need if n not in H]
return [("全 topic ⇒ 无未归类(n=%d)" % o0["n"], o0["n"] == 0),
("只收 legacy 行(n=%d)" % o1["n"], o1["n"] == 2),
("件数求和(总 %d / 完成 %d / 未完 %d)" % (o1["total"], o1["done"], o1["open"]),
(o1["total"], o1["done"], o1["open"]) == (3, 2, 1)),
("看板渲染里四处齐全(缺:%s)" % (miss or "无"), not miss)]
@case("看板:**同工作区但没归入本项目**的会话不许静默丢(⛔ 也不许把它算进本项目)")
def t_board_unrecognized_visible():
"""🔴 2026-10-01 用户报:「**为什么创建了协作会话 但是 看板中没有展示**」。
真事:M5 那根棒被命名成 `[协作]-本机协作-M5 …` —— **第 2 级没带方括号**、而且用的是
`goal.short`(本机协作)而非**任务类别**(`topics`=唤醒机制)。
⇒ `parse_session_name()` 判 `ok=False`、`_in_project()` 也找不到 `[唤醒机制]`
⇒ 它**整条从看板上消失**(不进任务会话层、也不算"本项目会话")。
**形状=不崩溃,只是少说一句话** —— 正是最难发现的那类(同族红线)。
⇒ 两头都要堵:
① **说出口**:`_sessions()` 交出 `unrecognized`,看板在图外说明里点名;
② **⛔ 不许改归属**:它们**不进 `mine`**、**不算「本项目会话共 N 个」** ——
归属判据仍是 §2.3 三级(主会话 sid / 标题含 `[<类别>]` / `--declare`),
⚠️ `cwd` **只用来决定"要不要提醒"**,⛔ 绝不用来决定"归不归本项目"。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_unrec", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
S = (HERE / "board.py").read_text(encoding="utf-8")
need = [ # 看板真会画(⛔ 只算不画 = 没修)
"sessions_unrecognized", "未归入本项目", "会被漏管",
"值取 `goal.json` 的 `topics`(任务类别),⛔ 不是 `short` 简称",
]
miss = [n for n in need if n not in H]
# ① 结构:快照真带这个键(连同"读不到库"的降级分支也带 ⇒ ⛔ 别只在成功路上加)
has_key = S.count('"sessions_unrecognized"') >= 1 and S.count('"unrecognized": unrec') >= 1
# ② ⛔ 它**不许**混进 `mine`:cwd 只出现在告警分支里,且该分支显式 continue
guarded = "_ct == _wstail" in S and "⛔ 绝不用它决定" in S
# ③ 命名规范那句话必须说对(⛔ 别再说"主题取自 short" —— 那正是本 bug 的源头)
C = (HERE / "collabd.py").read_text(encoding="utf-8")
spec_ok = ("`[协作]-[<类别>]-<具体>`" in C) and ("主题取自 `goal.json` 的 `short`" not in C)
return [("快照真带 `sessions_unrecognized`(含降级分支)", has_key),
("cwd 只用于告警、⛔ 不用于定归属(判据未松)", guarded),
("看板渲染里三处齐全(缺:%s)" % (miss or "无"), not miss),
("派活命名规范已纠偏(⛔ 不再说\"主题取自 short\")", spec_ok)]
@case("归属:两处 `in_project` 判据**逐样本对账**(⛔ 漂了就静默漏管)")
def t_in_project_parity():
"""🔴🔴 2026-10-01 用户报:「**看板没有正常显示协作会话**」。
真因(**扫出来的,不是推理出来的**):两处归属判据原来**都只认带方括号**的写法
(`"[%s]" % 类别 in 标题`),而**实际派活出来的棒名是** `[协作]-机制排查与修复-常驻投递容器`
—— **第 2 级没带方括号** ⇒ 判 `False` ⇒ 那条会话(**当时正在 `working`**)落到
`sessions_unrecognized` ⇒ `board.html` 第三层画**空框「暂无协作会话」**。
⚠️ 更糟的是它和 `_topic_in_title()` **自相矛盾** —— 后者的注释白纸黑字写着
「判据是**子串**(不要求 `[...]` 包裹)…**两种写法都要认**」⇒ **同一个标题两处结论相反**。
⇒ 本用例把**两台判据喂同一批标题、逐条比布尔**(⛔ 不靠人盯、⛔ 不只查源码字符串),
并同时验**精度没丢**:正文里恰好提到类别名的**无关会话**仍不许被收进来
(靠"角色前缀闸"——`_role_of_title()` 解析不出角色的标题一律不算)。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_inproj", HERE / "board.py")
bd = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bd)
m = imp()
_tops = ["会话协作自检", "机制排查与修复"]
sc = {"main_sid": "", "main_sids": [], "topics": _tops, "short": "会话协作自检",
"workspace": str(TEST_WS)}
# (标题, 期望) —— 期望是**这一版**的口径;⛔ 别把期望写成"跟旧版一样"
cases = [
("[协作]-机制排查与修复-常驻投递容器", True), # ← 本 bug 的正主:二级**无方括号**
# 🔴 2026-10-02 两类改造:唤醒/跟进**已退役** ⇒ 角色判空 ⇒ 判据②那条(「角色可解析+
# 标题含类别」)**不再命中** ⇒ 期望由 True 变 **False**。
# ⛔ 这不是"判据坏了",是**退役的正确后果**:那两类不再是活类别。
# ⛔ 顺带消掉了一个自指隐患:若它们还能被判成"本项目的会话",就会被当**主会话候选**。
("[唤醒]-会话协作自检-脉冲", False),
("[跟进]-会话协作自检-队列上报", False),
("[协作]-[机制排查与修复]-合规写法", True), # 老写法(带方括号)向后兼容
("主控 · 会话协作自检 · 收尾", True), # 口语式主会话(无方括号)
("接续 · 会话协作自检 · 第 2 棒", True), # 接续棒**带类别** ⇒ 认得出(新约定的目的)
("接续 · 会话机制合并包 · 任务4b", False), # 接续棒**不带类别** ⇒ 认不出(未改)
("[协作]-本机协作-M5 棒", False), # 用 `short`/昵称 ⇒ **仍认不出**(规范照旧)
("复盘:今天在会话协作自检里犯的错", False), # ← **精度闸**:无关会话恰提到类别名
("下载 open design", False),
("", False),
]
rows = []
for t, _ in cases:
a = bool(bd.in_project("", t, sc))
b = bool(m._in_project("", t, {"main_sid": "", "main_sids": []}, _tops))
rows.append((t, a, b))
_mismatch = [t for t, a, b in rows if a != b]
# ⚠️ 2026-10-01 **我自己写错又改对的一处**:原来写成
# `for (t, _), (_, a, _b) in zip(...)` —— 两个 `_` **互相覆盖**,
# 结果拿去跟 `a` 比的是**标题**不是**期望值** ⇒ 全表恒红。
# ⇒ 元教训:**对账类断言别用 `_` 当占位名**(一个函数里两个 `_` 必然屏蔽)。
_wrong = [(t, a, exp) for (t, exp), (_t, a, _b) in zip(cases, rows) if a != exp]
H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
S = (HERE / "board.py").read_text(encoding="utf-8")
return [
("两处 `in_project` 逐样本一致(%d 条;不一致:%s)" % (len(cases), _mismatch or "无"),
not _mismatch),
("🆕 **二级无方括号也算本项目的会话**(`[协作]-机制排查与修复-…` ⇒ True;"
"⛔ 旧判据判 False ⇒ 看板第三层画空框而它正在跑)", bd.in_project("", "[协作]-机制排查与修复-常驻投递容器", sc)),
("🆕 精度未丢:无关会话恰好提到类别名 ⇒ **仍不算**(角色前缀闸)",
bd.in_project("", "复盘:今天在会话协作自检里犯的错", sc) is False),
("期望值逐条成立(不符:%s)" % (_wrong or "无"), not _wrong),
("🔴 判据用**角色+类别子串**两条并列(⛔ 别退回「只认方括号」)",
"_topic_in_title(ti, _tps) and _role_of_title(ti)" in S),
("🔴 看板的退场说明在渲染里真的存在(⛔ 只改数据不改渲染 = 没修)",
"sessions_retired" in H and "已收起" in H),
]
@case("会话退场:过期会话**收起但不删除**,且 ⛔ 不吃掉在跑的棒与主会话")
def t_session_retire():
"""🔴 2026-10-01 用户:「**通过接续会话的时候,如何处理过期的会话,避免越堆越多**」。
**为什么必须有**:接续=会话自己建下一棒,前棒**不会自动消失** —— 它在宿主库里永远是
一条 `completed` 记录、标题同族。实测本工作区已堆 5 条「接续 · 会话机制合并…」⇒
看板/状态稿每轮都为旧棒占位,真在跑的棒被埋在里面。
⇒ 两条退场路径:① **显式** `--retire self`(接续时由前棒自己登记,准确、即时)
② **窗口兜底**(`completed` 且超 `session_live_min` 分钟 ⇒ 前棒中途死掉也收得掉)
⛔ 两条闸门**都绕开 `working`**(在跑的一定要摊)+ **主会话是版面锚点**(⛔ 不抽)。
⛔ **收起 ≠ 删除**(宿主库原样在、`retired.json` 全留痕)⇒ 必须**报数**,⛔ 不许静默。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_retire", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
m = imp()
now = time.time()
def _ms(minutes_ago):
return int((now - minutes_ago * 60) * 1000)
def _row(sid, title, status, minutes_ago):
return {"id": sid, "title": title, "custom_title": title, "status": status,
"cwd": str(TEST_WS), "updated_at": _ms(minutes_ago),
"last_activity_at": _ms(minutes_ago)}
_MAIN, _RUN, _NEW, _OLD, _RUNRET, _RET = ("aaaa0001", "bbbb0002", "cccc0003",
"dddd0004", "eeee0005", "ffff0006")
rows = [
_row(_MAIN, "主控 · 会话协作自检 · 主会话", "completed", 300), # 主会话:再旧也不许抽掉
_row(_RUN, "[协作]-会话协作自检-在跑的棒", "working", 300), # 在跑:再旧也不许收起
_row(_NEW, "[协作]-会话协作自检-刚完的", "completed", 10),
_row(_OLD, "[协作]-会话协作自检-很旧的", "completed", 300), # 窗口兜底收
_row(_RUNRET, "[协作]-会话协作自检-已退场但仍在跑", "working", 5),
_row(_RET, "[协作]-会话协作自检-已退场", "completed", 5), # 显式退场收
]
sc = {"main_sid": _MAIN, "main_sids": [_MAIN], "topics": ["会话协作自检"],
"short": "会话协作自检", "workspace": str(TEST_WS)}
# ① 显式退场表(写进**测试区**的 inbox,⛔ 不碰生产)
(TIN / "retired.json").unlink(missing_ok=True)
rc1, out1 = 0, ""
_env = _mk_env()
_env["CODEBUDDY_SESSION_ID"] = _RET
p = subprocess.run([PY, "-u", str(HERE / "collabd.py"), "--retire", "self",
"--why", "自测"],
cwd=str(HERE), stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, env=_env, timeout=60, errors="replace")
rc1, out1 = p.returncode, p.stdout or ""
_tbl = json.loads((TIN / "retired.json").read_text(encoding="utf-8")) if (TIN / "retired.json").exists() else {}
r1 = b._sessions(limit=30, sc=sc, rows=rows)
ids = [x["id8"] for x in (r1.get("mine") or [])]
hid = r1.get("hidden") or {}
hid_ids = [x["id8"] for x in (hid.get("recent") or [])]
# ② 退场表自身也要有过期清理(⛔ 否则这张表自己会越堆越多)
pr = m._prune_retired({_RET: {"at": now}, "zzzz9999": {"at": now - 8 * 86400}})
# ③ 短 id 也要能命中(本程序各处只存 8 位)
hit8 = m.is_retired(_RET[:8], m._retired())
(TIN / "retired.json").unlink(missing_ok=True)
return [
("`--retire self` rc=0 且落盘(%s)" % (out1.strip().splitlines() or ["(空输出)"])[0][:60],
rc1 == 0 and _RET in _tbl),
("退场表按**完整会话 id** 存(供 8 位短 id 前缀比对)", _RET in _tbl),
("🆕 显式退场的那条**被收起**(不在 mine 里)", _RET[:8] not in ids),
("🆕 超窗口(>%d 分钟)的 completed 也**被收起**" % int(b.LIVE_MIN), _OLD[:8] not in ids),
("🔴 **在跑的棒永不收起**(即使它已 300 分钟没动)", _RUN[:8] in ids),
("🔴 **主会话是版面锚点**:再旧也不抽掉", _MAIN[:8] in ids),
("🔴 显式退场 ⛔ 也不吃掉 **working** 的会话(它还在干活)", _RUNRET[:8] in ids),
("窗口内刚完成的留在版面上", _NEW[:8] in ids),
("报数:显式退场 %d / 超窗口 %d(应为 1 / 1)" % (hid.get("retired", 0), hid.get("stale", 0)),
(hid.get("retired") == 1 and hid.get("stale") == 1)),
("收起明细点名到会话(%s)" % (hid_ids or "空"), _RET[:8] in hid_ids and _OLD[:8] in hid_ids),
("⛔ 收起 ≠ 删除:宿主库行**一个字没动**(`mine` 里少了、明细里还在)",
len(rows) == 6 and len(hid.get("recent") or []) == 2),
("退场表自清理:超 7 天的记录被丢掉(剩 %d 条)" % len(pr), _RET in pr and "zzzz9999" not in pr),
("短 id(8 位前缀)也能命中退场表", hit8),
]
@case("看板:任务类别**来源**必须说得出口(declared / fallback)")
def t_board_topics_source():
"""🔴 2026-10-01 用户订正:「**目标是 通过对话在调用 会话协作skill时说明的,不是固定的**」。
`topics` 缺省回落 `short` 是**兼容**(单类别部署照旧),但看板**不许把"没说明过"
显示成"就是这么定的"** —— 那还是同族红线:**没有却说有**。
⇒ `_goal_topics_source()` 必须给出三态,且**看板真会把它渲染出来**(⛔ 只算不画=没修)。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_tsrc", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
s_dec = b._goal_topics_source({"topics": ["甲", "乙"], "topics_declared_at": "2026-10-01T00:10",
"topics_declared_by": "对话", "short": "简"})
s_fb = b._goal_topics_source({"short": "简"})
s_no = b._goal_topics_source({})
s_cand = b._goal_topics_source({"_topics候选": {"候选": ["丙"]}, "short": "简"})
H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
need = ["topics_source", "未声明", "调用时在对话里说明"]
miss = [n for n in need if n not in H]
return [("有 topics ⇒ declared(实得 %s)" % s_dec["kind"], s_dec["kind"] == "declared"),
("…且带说明时间/人(%s / %s)" % (s_dec["at"], s_dec["by"]),
bool(s_dec["at"]) and bool(s_dec["by"])),
("只有 short ⇒ fallback(实得 %s)" % s_fb["kind"], s_fb["kind"] == "fallback"),
("都没有 ⇒ none(实得 %s)" % s_no["kind"], s_no["kind"] == "none"),
("候选清单能读出来(%s)" % s_cand["candidates"], s_cand["candidates"] == ["丙"]),
("看板真会渲染来源(缺:%s)" % (miss or "无"), not miss)]
@case("看板:多目标 tab(一格一个目标 ⇒ ⛔ 不是各画一份叠着)")
def t_board_multi_goal():
"""🔴 2026-10-01 用户:「把协作实时看板改为 **tab 支持多个目标**执行协作状态展示」。
守四件(⛔ "能读多个"不算数,必须是**分得开、不串、且向后兼容**):
① `goal_files()` 把 `goal.json` + `goals/*.json` 合成**有序**列表,**活跃的排第一**;
② `build()` 出 `goals[]`,**顶层键=活跃目标那一份**+ `goal` 仍是 `title/short/acceptance`
⇒ ⛔ 老渲染器与老断言**逐字照旧可用**(向后兼容不是口号,是这一条);
③ 台账**按任务类别**切到各目标(⛔ 不串);`line` 不属**任何**目标的件归**活跃目标**
且**照常显示**(同族红线:⛔ 不静默丢);
④ 看板**真画** tab 条(⛔ 只算不画=没修)。
"""
os.environ["COLLABD_CONFIG"] = str(CFG)
os.environ["DSH_COLLAB_WS"] = str(TEST_WS)
import importlib.util
spec = importlib.util.spec_from_file_location("bd_mg", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
# ⚠️ 测试工作区里的 goal.json / tasks.json 是**别的用例的夹具** ⇒ 用完**原样还原**
_bkp = {}
for _n in ("goal.json", "tasks.json"):
_p = TIN / _n
_bkp[_n] = _p.read_text(encoding="utf-8") if _p.exists() else None
_gd = TIN / "goals"
_gd_before = _gd.is_dir()
try:
_gd.mkdir(parents=True, exist_ok=True)
(TIN / "goal.json").write_text(json.dumps(
{"id": "ga", "short": "甲", "title": "目标甲", "topics": ["甲类"],
"acceptance_state": {"A1": "pass", "A2": "fail"}}, ensure_ascii=False), encoding="utf-8")
(_gd / "gb.json").write_text(json.dumps(
{"id": "gb", "short": "乙", "title": "目标乙", "topics": ["乙类"],
"acceptance_state": {"B1": "pass"}}, ensure_ascii=False), encoding="utf-8")
put_tasks({"T1": {"state": "done", "line": "甲类"},
"T2": {"state": "running", "line": "甲类"},
"T3": {"state": "done", "line": "乙类"},
"T4": {"state": "done", "line": "旧线名"}})
snap = b.build()
finally:
try:
(_gd / "gb.json").unlink()
if not _gd_before:
try:
_gd.rmdir()
except Exception:
pass
except Exception:
pass
for _n, _t in _bkp.items():
try:
if _t is None:
(TIN / _n).unlink()
else:
(TIN / _n).write_text(_t, encoding="utf-8")
except Exception:
pass
gs = snap.get("goals") or []
by = {str(g.get("key")): g for g in gs}
_ga = by.get("ga", {}) or {}
_gb = by.get("gb", {}) or {}
H = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
old_top = {"goal", "project", "tasks", "sessions", "labor", "orphan", "queue",
"progress", "others_running", "front", "runtime", "notify", "warn", "meta"}
_miss = sorted(old_top - set(snap.keys()))
_missjs = [k for k in ("renderTabs", "paintScope") if k not in H]
_R = [
("goal_files 合成 2 项且**活跃的排第一**(实得 %s)" % [g.get("key") for g in gs],
len(gs) == 2 and gs[0].get("active") is True and gs[1].get("active") is False),
("顶层键与旧版同形(缺:%s)" % (_miss or "无"), not _miss),
("顶层 goal 仍是 title/short/acceptance(实得 %s)" % sorted((snap.get("goal") or {}).keys()),
set((snap.get("goal") or {}).keys()) == {"title", "short", "acceptance"}),
("甲目标分到 2 件 + 1 件无人认领的旧线(共 %d 件)" % len(_ga.get("tasks") or {}),
len(_ga.get("tasks") or {}) == 3),
("乙目标只分到自己的 1 件(实得 %d)" % len(_gb.get("tasks") or {}),
len(_gb.get("tasks") or {}) == 1),
("⛔ 乙目标看不到甲目标的件(不串)", "T1" not in (_gb.get("tasks") or {})),
("⛔ 甲目标的完成情况里没有乙目标的判据", "B1" not in str(_ga.get("acc_summary"))),
("验收按目标分开算(甲=%s / 乙=%s)" % (_ga.get("acc_summary"), _gb.get("acc_summary")),
"A2" in str(_ga.get("acc_summary")) and "pass" in str(_gb.get("acc_summary"))),
("分工板按目标分开(甲 %s / 乙 %s)"
% ([x.get("line") for x in (_ga.get("labor") or [])],
[x.get("line") for x in (_gb.get("labor") or [])]),
(_ga.get("labor") or [{}])[0].get("line") == "甲类"
and "乙类" not in [x.get("line") for x in (_ga.get("labor") or [])]
and [x.get("line") for x in (_gb.get("labor") or [])] == ["乙类"]),
("看板真会画 tab 条(缺:%s)" % (_missjs or "无"), not _missjs),
("看板把 `goals` 当数据源(⛔ 不写死单份)", "d.goals" in H),
# ===== 🔴🔴 2026-10-04 用户定案:tab 最多 6 个 + 最新在跑的排前面 =====
("tab 有**上限常量**且只有一个(⛔ 两处数字必漂)", H.count("TAB_MAX = 6") == 1),
("超出部分**收进下拉**(`tabMoreMenu` + `tabMoreBtn` 都在)",
"tabMoreMenu" in H and "tabMoreBtn" in H),
("下拉项**可直接切换**(复用 `data-gi` ⛔ 不另造一套选中逻辑)",
"tabmenu-item" in H and "data-gi=" in H),
("下拉挂在 `.tabwrap`(⛔ `position:relative` 必须在它身上,"
"⛔ 否则绝对定位会参照 flex 祖先跑到别处)",
".tabwrap{position:relative" in H.replace(" ", "")),
("菜单**不是常开**(`hidden` + 显隐逻辑都有)",
"mn.hidden=true" in H and "tabmenu[hidden]" in H),
("🔴 **排序在后端**(前端⛔ 不自己排:两处排序=改一处忘另一处必打架,同 P0-40)",
#⚠️ 只量`renderTabs` **函数体**,⛔ 不量全文件(本文件别处有 3 处正当排序:
# L532 判据键、L1040 检查会话按 age_min、L1621 台账键—— 全局判 `sort(` 会出现假红)。
"sort(" not in H.split("function renderTabs")[1][:4000]),
("前端**读后端排好的顺序**(⛔ 直接 `gs.map` 不重排)", "shown=more?gs.slice(0,TAB_MAX):gs" in H),
]
# ===== 🔴🔴 2026-10-04 用户定案:「**tab 的宽度改为一样宽**」=====
# ⚠️⚠️ **必须先剥 CSS 注释再量规则块** —— `.tab` 上面那段说明注释里**正当**地
# 引用了旧写法 `min-width:180px`(那是"为什么改"的证据,⛔ 不许删)。
# ⇒ 直接全文件查 `min-width:180px` 会**永远命中** ⇒ 判据假绿(实测会栽在这)。
# ⚠️⚠️ **必须只量 `.tab{...}` 这一条规则块**,⛔ 不量全文件(本文件别处有
# `.tabmenu` / `.tabmore-btn` / `.tabs` 等正当规则)。正则是 `\.tab\{[^}]*\}` ——
# `.tab .tt{` / `.tabmenu{` / `.tabmore-btn{` 都**不**匹配(`{` 紧跟 `.tab`)。
_css_nc = re.sub(r"/\*.*?\*/", "", H, flags=re.S) # 剥注释
_mtab = re.search(r"\.tab\{[^}]*\}", _css_nc)
_tabcss = _mtab.group(0) if _mtab else ""
# 等宽的**唯一成因**=`flex` 的 grow≥1 且 **basis=0**(basis 按内容分配 ⇒ 永不等宽)。
# ⚠️ 解析式判据而不是查子串:`flex:1 1 0` ✅ / `flex:1`(默认 1 1 auto)❌ /
# `flex:1 1 auto` ❌ / `flex:0 1 auto`(不生长)❌ —— 四种写法子串法分不开。
_mf = re.search(r"flex:\s*(\d+)\s+(\d+)\s+([0-9a-z%]+)", _tabcss)
_R.append((
"tab **等宽**(`.tab` 的 flex 须 grow≥1 且 **basis=0**;实得:%s)"
% (_mf.group(0) if _mf else "⛔没找到 flex 声明"),
bool(_mf) and int(_mf.group(1)) >= 1 and _mf.group(3) == "0"))
# 🔴 配套:`min-width:0` 缺了 ⇒ flex 等分会被子元素 min-content 顶开 ⇒ **看着不等宽**
# (标题长的那格把邻居挤窄)。这是"等宽"的**另一半**,⛔ 不能只判 flex。
_R.append(("tab 等宽的配套 `min-width:0` 在(⛔ 缺了会被长标题顶开):%s"
% ("有" if "min-width:0" in _tabcss.replace(" ", "") else "⛔无"),
"min-width:0" in _tabcss.replace(" ", "")))
# 🔴 抓「改回内容自适应」:`flex:1 1 0` 之外又留了按内容/固定宽的声明 ⇒ 后者会赢。
# 🔴🔴 `width:` **必须带负向后顾** —— ⛔ 否则 `max-width:420px` 里那个 `width:420` 会被命中
# ⇒ 这条判据**恒红** ⇒ 任何变异都"报红" ⇒ **变异验证整体作废**(判据恒红 = 没判据)。
# CSS 属性名里 `-` 是合法字符(`max-width`/`min-width`)⇒ 用 `(?<![-\w])` 锚住词首。
_R.append(("`.tab` ⛔ 没有内容自适应残留(`min-width:1xxpx` / 裸 `width:` / `flex:0 1 auto`):%s"
% (_tabcss.replace("\n", " ")[:110] or "⛔没取到规则块"),
not re.search(r"min-width:\s*1\d\dpx|(?<![-\w])width:\s*\d|flex:\s*0\s+1", _tabcss)))
# --- 后端排序:真起 board.py 落一个快照,验真排序(⛔ 源码级不算数)---
try:
import json as _j8
import subprocess as _sp8
import tempfile as _tp8
from pathlib import Path as _P8 #⛔ 别用 `Path`(本函数里被别的局部量遮住)
import sys as _s8 #⛔ 同上
_cfg = _tp8.mkdtemp(prefix="tabs_")
_cf = _P8(_cfg) / "collabd.config.json"
_cf.write_text(_j8.dumps({"workspace": _cfg, "inbox": "inbox",
"log": "inbox/_b.log"}, ensure_ascii=False), encoding="utf-8")
_in = _P8(_cfg) / "inbox"
_in.mkdir(parents=True, exist_ok=True)
# ⚠️ **三个目标**,⛔ 不是一个 —— 单块时任何排序键都「升序」,变异根本抓不到
#(实测栽过:删掉 `blocks.sort(...)` 判据照样绿)。
# 🔴🔴 2026-10-06:**`declared_at` 必须"乱序"给**(t2 最老、t0 居中、t3 最新),
# 而且**文件出现次序 = 声明时间倒序** ⇒ 只要排序失效,输出顺序**立刻不一样**。
# ⛔ 若三个时间给成同序/全相同 ⇒ 排序与否输出无差异 ⇒ **判据恒绿**(同族已栽多次)。
(_in / "goal.json").write_text(_j8.dumps(
{"id": "t0", "short": "甲", "title": "甲目标", "lifecycle": "进行中",
"run": "active", "declared_at": "2026-10-02T10:00",
"acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
encoding="utf-8")
_gd8 = _in / "goals"
_gd8.mkdir(parents=True, exist_ok=True)
for _i, _at in ((2, "2026-10-01T09:00"), (3, "2026-10-03T11:00")):
(_gd8 / ("%d.json" % _i)).write_text(_j8.dumps(
{"id": "t%d" % _i, "short": "乙%d" % _i, "title": "乙目标%d" % _i,
"lifecycle": "进行中", "run": "active", "declared_at": _at,
"acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
encoding="utf-8")
# 🔴🔴 **env 必须「白名单重建」,⛔ 不能 `dict(os.environ, ...)`**:
# 本用例开头那句 `os.environ["COLLABD_CONFIG"] = str(CFG)` 已把进程 env 指向
# **测试夹具**(实测:这样跑board.py 会读到夹具的 inbox ⇒ 只回1 个「目标甲」,
# 判据里那 5 条**全部通过但验的不是我的夹具** —— 最坏的一种假绿)。
# ⇒ 只留必需项,其余全清(`DSH_*` / `COLLABD_*` / `CODEBUDDY_*` 一律不带)。
_env8 = {"PATH": os.environ.get("PATH", ""),
"SystemRoot": os.environ.get("SystemRoot", ""),
"TEMP": os.environ.get("TEMP", ""),
"TMP": os.environ.get("TMP", ""),
"COMSPEC": os.environ.get("COMSPEC", ""),
"COLLABD_CONFIG": str(_cf),
"COLLABD_NO_ENSURE": "1"} # ⛔ 测试环境⛔ 不许起常驻(见 `_mk_env` 的说明)
_sp8.run([_s8.executable, str(HERE / "board.py"), "--out", str(_in / "board.json")],
capture_output=True, text=True, timeout=180, env=_env8,
encoding="utf-8", errors="replace")
_bj = _in / "board.json"
_d8 = _j8.loads(_bj.read_text(encoding="utf-8")) if _bj.exists() else {}
_gs8 = _d8.get("goals") or []
# 🔴🔴 2026-10-06 用户定案:「**tab 的排列顺序应该按创建时间顺序**」(推翻 10-04 的活跃度排序)。
# ⇒ 判据整体改判 **`tab_created`(=`declared_at`)升序**。
# ⚠️ **必须钉住"实际次序"**,⛔ 不能只判"升序":夹具里三块时间**全不同** ⇒
# 升序成立**等价于**真的按时间排;但若哪天夹具退化成同值,升序就恒真 ⇒
# ⇒ 所以**额外钉一条"首个是最老的"**(`2026-10-01`)当冗余保险。
_crts = [g.get("tab_created") for g in _gs8]
_R.append(("后端给了 `tab_created` 排序键(实得:%s)" % (_crts[:3] or "无"),
bool(_gs8) and all(c is not None for c in _crts)))
_R.append(("后端 `tab_created` **升序**(最早创建的在最前):%s" % _crts,
_crts == sorted(_crts)))
# 🔴 抓「拿活跃度当创建时间」/「键取错字段」:夹具里 **t2 最老**(10-01)
# ⇒ 排第一的必须是它;⛔ 若还是甲(goal.json,文件序第一)= 没排序。
# ⚠️ `short` 在块的 **`goal` 子字典**里(⛔ 不在块顶层 —— 我第一版就取错层,
# 判据恒红;实测栽过)。
_R.append(("首个 tab = **最早创建的目标**(夹具里是最老的 `乙2`):%s"
% [str((g.get("goal") or {}).get("short") or "") for g in _gs8][:1],
bool(_gs8)
and str((_gs8[0].get("goal") or {}).get("short") or "") == "乙2"))
# 🔴 抓「缺 `declared_at` 的块被当成 0/空串 ⇒ 排到最前」:
# ⚠️ **必须真造一块没有 `declared_at` 的**(t4,且它的标题明显"最老",人眼会想放前面)
# ⇒ 它必须**垫底**(`tab_created == "~"` 且 `tab_i` 最大),⛔ 不许因为读不到就抢到第一。
# ⛔ 不造这一块 ⇒ 本判据**恒真**(列表里没有 `~`)= 假绿(同族已栽多次)。
_gd8.joinpath("4.json").write_text(_j8.dumps(
{"id": "t4", "short": "丙4", "title": "丙目标4", "lifecycle": "进行中",
"run": "active", "acceptance_state": {"A1": "过|x"}}, ensure_ascii=False),
encoding="utf-8")
_sp8.run([_s8.executable, str(HERE / "board.py"), "--out", str(_in / "board.json")],
capture_output=True, text=True, timeout=180, env=_env8,
encoding="utf-8", errors="replace")
_d9 = _j8.loads((_in / "board.json").read_text(encoding="utf-8")) \
if (_in / "board.json").exists() else {}
_gs9 = _d9.get("goals") or []
_c9 = [g.get("tab_created") for g in _gs9]
_i9 = [g.get("tab_i") for g in _gs9]
_n9 = len(_gs9)
_R.append(("缺 `declared_at` 的块 **垫底**(⛔ 不许读不到就排最前):%s" % _c9,
_n9 == 4 and _c9[-1] == "~" and _c9[:3] == sorted(_c9[:3])
and _i9 == [0, 1, 2, 3]))
_R.append(("垫底块的 `tab_i` = **最后一位**(⛔ 不是靠 '~' 撞巧):%s" % _i9,
_n9 == 4 and _i9[-1] == 3))
_R.append(("后端给了 `tab_more`(前端⛔ 不自己重算 `gs.length-6`):%s"
% [g.get("tab_more") for g in _gs8],
bool(_gs8) and all(g.get("tab_more") == max(0, len(_gs8) - 6)
for g in _gs8)))
# 🔴 抓「排序后没重编序号」:多块时 `tab_i` 必须恰好是 0..n-1
# (⛔ 排序前的旧序号会留着 ⇒ 前端 `data-gi` 与位置对不上 ⇒ 点A切到B)。
# 🔴 抓「干脆不排序」:夹具里文件出现次序**本就是时间倒序**(t0 居中、t2 最老在后加)
# ⇒ 排序与否**输出有差异** ⇒ 上面那条"升序"能抓到;此处再钉一条源码级保险
# (⛔ 这是源码级判据的正当用途:验"语句在不在")。
_bsrc = (HERE / "board.py").read_text(encoding="utf-8")
_R.append(("`blocks.sort(...)` 真在且按 `tab_created`:%s"
% ("tab_created" in _bsrc and "blocks.sort(" in _bsrc),
'blocks.sort(key=lambda b: (b.get("tab_created"' in _bsrc))
# 🔴 抓「又改回活跃度排序」(口径回退):⛔ 源码里不许再出现 `tab_hot` 当排序键。
_R.append(("排序键**不再是** `tab_hot`(⛔ 防口径回退):%s"
% ("blocks.sort(key=lambda b: (b.get(\"tab_hot\"" not in _bsrc),
"blocks.sort(key=lambda b: (b.get(\"tab_hot\"" not in _bsrc))
_tis = [g.get("tab_i") for g in _gs8]
_R.append(("多目标时 `tab_i` **重编为 0..n-1**(⛔ 不重编 = 前端点A切到B):%s" % _tis,
len(_gs8) > 1 and _tis == list(range(len(_gs8)))))
except Exception as _e8:
_R.append(("真起board.py 验排序(⛔ 起不来即报红):%s" % type(_e8).__name__, False))
return _R
@case("动手层:三条开工动作在位 + **判据不许恒红**(⛔ 恒红 = 没有判据)")
def t_howto_layer():
"""🔴🔴 2026-10-04 用户:「**这些问题如何沉淀在技能中,让会话执行更靠谱**」。
病根诊断(同一天四次翻车,同一个形状):
`pitfalls.md` 47 条 / 141 KB 是**知识层** ⇒ 会话**读得完但不会自动执行**;
缺的是**动手层**(开工前必过的三条动作)⇒ �� `references/00-动手前必过.md`。
守三件(**每件都已落判据,不许退化**):
① 动手层**在位 + 挂进 SKILL.md 加载段第一位**(⛔ 不挂=又一份没人读的文档);
② 动手层里那三条**各带已知落点常量**(省得下一个人再去 grep 源码);
③ 🔴 **判据不许恒红** —— 基线必须全绿(数 ✗ 且排除「报告型」行)。
恒红比漏网更坏:它让**所有变异都"红"** ⇒ 验证作废,而**表面看起来完美**。
"""
HOW = HERE.parent / "references" / "00-动手前必过.md"
exists = HOW.exists()
T = HOW.read_text(encoding="utf-8", errors="replace") if exists else ""
SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace")
P0 = (HERE.parent / "references" / "pitfalls.md").read_text(encoding="utf-8", errors="replace")
# ① 在位 + 挂第一位
_R = [
("动手层 `00-动手前必过.md` 在位(%d 字节)" % len(T.encode("utf-8")), exists),
("挂进 SKILL.md 加载段(⛔ 不挂 = 又一份没人读的文档)",
"references/00-动手前必过.md" in SK),
("排在加载段**第一位**(⛔ 动手层要最先被读到,⛔ 不许排在后面)",
SK.find("00-动手前必过.md") < SK.find("references/architecture.md")),
]
# ② 三条动作各在位 + 各带落点常量
_three = [("查一·问程序自己", "supervise_alive"),
("查二·先看时间戳", "_更新"),
("查三·先让基线全绿", "恒红")]
for _nm, _k in _three:
_R.append(("动作在位且带实证:**%s**(锚点 `%s`)" % (_nm, _k), _k in T))
_R.append(("动手层给了**已知落点常量表**(⛔ 不给 ⇒ 下一个人又去 grep 源码):%d 条"
% T.count("| 东西 |"),
T.count("| 东西 |") >= 1 and "SUP_HB" in T and "guard.stop" in T))
# 🔴🔴③ 「判据不许恒红」—— ⛔ **原来这条是"扫自己的源码找裸 True"**,
# 实测**把自己的扫描代码当成了被扫对象**(`"== 0) or" in ln` 命中它自己那行)⇒ 恒红。
# ⇒ 与 P0-48 同策:**扫文本的判据一律不成立**,改成**行为级**——
# 真把 `00-动手前必过.md` 挪走,看这条判据会不会报红(它必须红)。
# ⚠️ 这里只判"文档在不在位"(那是可判的);**"判据会不会红"由变异脚本验**,不在这儿自证。
# ⚠️ 锚点**跟着内容走**:动手层精简过一次(4849→2582 字节)、后又加过两条(④⑤),
# 锚点串必须同步改,⛔ 否则判据会恒红(已栽过一次,见 P0-48)。
_anti = [k for k in ("⛔ **不许用", "一律现取", "恒红比漏网更坏",
"单条上限", "关键词对账") if k in T]
_R.append(("动手层五条动作**都带 ⛔ 反例/上限**"
"(⛔ 只写「要做」不写「⛔ 不许」= 会被照着做错):%d/5" % len(_anti),
len(_anti) == 5))
# 📌 动手层与 pitfalls 的分工不许反(知识层会被读完,动手层是动作)
_R.append(("动手层明说「⛔ 别开工前通读 pitfalls」"
"(⛔ 知识层当动作层用 = 141 KB 没人读)",
"别开工前通读" in T or "⛔ 别开工前" in T))
_R.append(("三条都在 pitfalls 里有对应条目(P0-47/48/49 = 知识层)",
all(("P0-%d" % i) in P0 for i in (47, 48, 49))))
return _R
@case("判据元体检工具在位 + 它的**元规则**也不是摆设(⛔ 防「靠记得」)")
def t_judge_audit_tool():
"""🔴 2026-10-04 收口:写了一天纪律(`references/00-动手前必过.md`)**当天照样栽**。
⇒ 根因收敛成两条,**都不是「不小心」**:
A. 判据与被检对象的「连接」没被验证(判据自重算/锚文本/夹具没覆盖真分支)
B. 验证动作本身出错而我不检查它(脚本自己崩了却照样打 PASS/只重置一份文件)
⇒ 解法不是「更小心」,是**把纪律变成机器强制**:⛔ 不跑这个工具,你自己看不见。
"""
mp = HERE / "judge_audit.py"
if not mp.exists():
return [("⛔ 包内缺 `scripts/judge_audit.py` ⇒ 判据恒绿/假绿没人拦", False)]
import importlib.util as _iu
sp = _iu.spec_from_file_location("ja_probe", mp)
ja = _iu.module_from_spec(sp)
try:
sp.loader.exec_module(ja)
except Exception as e:
return [("⛔ `judge_audit.py` 读不出来(%r)" % (e,), False)]
src = mp.read_text(encoding="utf-8", errors="replace")
R = []
R.append(("**元规则表非空**(⛔ 空表 = 体温计没装)", len(getattr(ja, "RULES", [])) >= 5))
R.append(("每条规则都带**「今天栽在哪」**(⛔ 没写原因的规则会变成新纪律)",
all(("今天" in w) for _n, _k, _p, w in ja.RULES)))
R.append(("每条规则都有 **kind**(weak/self/weak_ev ⇒ 严重度可分级)",
all(k in ("weak", "self", "weak_ev") for _n, k, _p, _w in ja.RULES)))
# —— B 类根因的工具抓手(今天栽了三次的就是这一块)
R.append(("**变异未生效 ⇒ rc=2 且明说**(⛔ 今天脚本自己崩了却照样打 PASS)",
re.search(r"变异未生效[\s\S]{0,120}?return 2", src) is not None))
R.append(("**等价变异被识破**(⛔ 只动写法不动语义的变异不算数)",
# 🔴 逐字锚在**工具自己的措辞**上(今天栽过:锚 `mutated == text` 那个条件,
# 而工具里那句是「植入后文本**没变** ⇒ 等价变异」⇒ 结构不同 ⇒ 假红)
re.search(r"植入后文本\*\*没变\*\*[^\n]*?等价变异[\s\S]{0,200}?return 2",
src) is not None))
R.append(("**还原后核对 md5**(⛔ 今天多轮变异互相污染)",
re.search(r"还原后 md5 \*\*不一致\*\*[\s\S]{0,120}?return 2", src) is not None))
R.append(("**`finally` 里还原**(⛔ 异常路径也必须还原)",
re.search(r"finally:[\s\S]{0,120}?write_bytes\(orig\)", src) is not None))
R.append(("🔴 **变异在原地做**(⛔ 拷到临时目录 = 判据读的还是原文件,"
"变异压根没进被检对象 —— 今天第三次栽在这)",
"shutil.copy2" not in src and "原地" in src))
R.append(("**误报白名单存在且要求写清原因**(⛔ 元规则本身也会假红)",
hasattr(ja, "FALSE_POSITIVE") and "FALSE_POSITIVE" in src))
# —— 真跑:拿一个**已知的坏写法**去扫,必须被抓到(否则规则是死的)
probe = TEST_WS / "judge_probe.py"
probe.write_text(
"x = (lambda s: s.lower())\n"
"if 'automations' in src:\n"
" pass\n", encoding="utf-8", newline="\n")
try:
n = ja.audit(probe)
R.append(("拿**已知坏写法**去扫 ⇒ 命中 ≥2 类规则"
"(⛔ 扫不出 = 规则本身是死的)", n >= 2))
finally:
try:
probe.unlink()
except OSError:
pass
return R
@case("红线 S:⛔ 严禁把「被监控对象所在工作区」当成「目标归属工作区」+ 归属核对进 env_check")
def t_ws_attribution():
"""🔴🔴 2026-10-04 用户当场纠正(当轮原话):
「**本工作区下面的目标 为什么执行会话要创建到 vibe-product 工作区下面**」。
病根:用户在本工作区(`ai1net-dsh-server`)下达「监控 `vibe-product`」的目标,
我把排期 `cwds` 写成 `vibe-product` ⇒ 会话 `c88a157c` 落在那个区
⇒ **本工作区看板/台账里直接看不见它**,而本区恰恰是问题最集中、最需要它的地方。
⛔ 当时**没有任何机制**提醒过 ⇒ 光写文档没用,必须落成**可执行判据**。
"""
SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace")
R = []
R.append(("SKILL.md 有 **S 红线**这一条",
"S 红线" in SK))
R.append(("红线上写清**归属看的是「目标从哪个区下达」**(⛔ 不是目标文本里出现的区名)",
"目标在哪个工作区下达" in SK and "对象" in SK))
# 🔴 逐字抄文档原句(⛔ 判据锚点必须逐字,早先凭记忆写「正解=」⇒ 基线直接红)
R.append(("三条禁止都在位(不许把对象区写进 `cwds`/不许按目标文本里的区名定/交付落归属区)",
"不许把「要去看/要改的那个区」写成 `cwds`" in SK
and "不许用「目标文本里出现了某区名」来定 `cwds`" in SK
and "一律落在**归属区**" in SK))
R.append(("写明**唯一例外**(用户当轮明确要求时才建在别的区)",
"唯一例外" in SK and "当轮" in SK))
R.append(("写明例外的机械背景(`automations.cwds` ⛔ 不落 `sessions.cwd`)",
"不落" in SK and "sessions.cwd" in SK))
# —— 判据侧:真去查 `env_check` 有没有这一段
# 🔴 `imp()` 硬编码只加载 `collabd.py`(⛔ 不接参数)⇒ 这里**自己**按路径加载。
import importlib.util as _iu
_sp = _iu.spec_from_file_location("_env_probe_att", HERE / "hooks" / "_env.py")
ev = _iu.module_from_spec(_sp)
try:
_sp.loader.exec_module(ev)
except Exception as e:
return R + [("⛔ `_env.py` 读不出来(%r)⇒ 归属判据无法验" % (e,), False)]
src = (HERE / "hooks" / "_env.py").read_text(encoding="utf-8", errors="replace")
# 🔴🔴 这三条原先是「**锚在文本出现过**」的弱判据(今天第 7 次同形状,已被元体检标出)
# ⇒ 改成**行为判据**:真跑 `env_check`,看返回体里**有没有那个键、值对不对**。
# ⛔ 弱锚点的危险:把 `attribution` 从源码里删掉但留个同名注释,它照样绿。
_rep = {}
try:
_rep = ev.env_check("E:/ProgramData/__selftest_no_such_ws__")
except Exception as _e:
R.append(("⛔ `env_check` 真跑失败(%r)⇒ 后三条判据无法验" % (_e,), False))
_att = (_rep.get("attribution") or {}) if isinstance(_rep, dict) else {}
R.append(("`_env.py` 的 `env_check` 真跑后**返回体含 `attribution` 键**"
"(⛔ 行为判据,⛔ 不是「源码里出现过 attribution」)",
isinstance(_rep, dict) and "attribution" in _rep))
R.append(("`attribution` 里带 `declared` 清单(⛔ 空 dict = 键在但没内容,等于没有)",
isinstance(_att, dict) and "declared" in _att))
R.append(("核对读的是 **`automations` 表**(⛔ 不是 `sessions.cwd` —— 后台会话 cwd 独立):"
"⛔ 用**真读数**验(`declared` 非空 ⇒ 查的是真库)",
bool(_att.get("declared"))))
R.append(("**有工作区路径归一化**(⛔ 同一区出现过反斜杠/正斜杠两种写法 ⇒ 逐字比会误判)",
hasattr(ev, "_norm_ws")))
# 🔴🔴 真行为判据,⛔ **不许**自己重算一遍(今天栽过):
# 早先写成 `(lambda s: ...)(...)` ⇒ 跟被检的 `_norm_ws` **毫无关系**
# ⇒ 把它改坏(去掉 lower)判据照样全绿(实测 V4 没抓到)。
# ⇒ 现在**直接调被检函数**,且喂**实测踩到过的三种形态**。
try:
_n = ev._norm_ws
R.append(("归一化:反斜杠/正斜杠/尾斜杠/大小写 **四种写法归到同一个值**"
"(⛔ 同一区实测出现过 `E:\\…` 与 `E:/…` 两种)",
len({_n("E:\\P\\ws"), _n("E:/P/ws"), _n("E:/P/ws/"),
_n("e:/p/ws")}) == 1))
R.append(("归一化**不误并不同区**(⛔ 别把 `ws` 与 `ws2` 并成一个)",
_n("E:/P/ws") != _n("E:/P/ws2")))
except Exception as e:
R.append(("`_norm_ws` 真调失败:%r" % (e,), False))
# 🔴 行为判据:拿一个**确定没有线**的工作区跑真体检 ⇒ 必须报出归属告警
# (复用上面已跑过的 `_rep`,⛔ 不重复跑 —— 同一份读数取两遍只会在两处漂)
_a = _att if isinstance(_att, dict) else {}
R.append(("对「无线的工作区」跑真体检 ⇒ **报出归属告警**"
"(⛔ 不报 = 形同虚设)",
any("协作线" in w for w in (_a.get("peer_missing") or []))))
R.append(("该告警进了 `warn`(⛔ 不进 warn = 没人会看到)",
any("协作线" in w for w in ((_rep.get("warn") or []) if isinstance(_rep, dict) else []))))
return R
@case("红线:⛔ 严禁用「排期/自动任务」当常驻载体 + 环境检查必须查工作区部署")
def t_no_schedule_as_supervisor():
"""🔴🔴 2026-10-04 用户定案(当场立红线):
> 「**使用技能时就要检查环境、做相关配置**(⚠️ **说了无数遍但没实现**)」
> 「**如果是为了替代常驻目标检查程序 也是技能的问题,需要红线禁止这样的操作**」
**取证**(`vibe-product` 工作区):
· 排期 `[执行]-界面交互-常驻续命`(`id=e181b51b`)= **`FREQ=HOURLY;INTERVAL=1`**
· 它的 prompt 第 3 步明写「不在 ⇒ **后台任务**起一条 `collabd.py --supervise`」
· **15:23 真跑过一次**,会话结论「✅ 常驻存活,本轮未做续命、未建任何排期」
· ⇒ **每小时唤醒一个新会话,只为看一眼常驻活没活**;而常驻本来就活(连跑 17.7 h)
⚠️ **它违背本技能自己写的** `supervise-persistence.md`「⛔ **排期代替不了载体**」
—— 文档早写对了,⛔ **没有判据守住** ⇒ 照样被建出来。**这就是本用例要补的洞。**
守两件:
① **红线在位**(SKILL.md 的 R 条 + 四条禁止 + 「换载体不是续命」这层意思)
② 🔴 **`env_check` 真的查工作区部署** —— 原来它只查「技能根能不能定位 + 钩子注册」,
⇒ **「复制了技能但工作区没部署」永远查不出来**(实测 `vibe-product` 旧版读数全绿)。
"""
import re as _re2
SK = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace")
PP = (HERE.parent / "references" / "supervise-persistence.md").read_text(
encoding="utf-8", errors="replace")
env_src = (HERE / "hooks" / "_env.py").read_text(encoding="utf-8", errors="replace")
# 🔴 `imp()` 硬编码只加载 `collabd.py`(不接参数)⇒ 这里**自己**按路径加载 `_env.py`。
# (实测栽过:`imp("hooks/_env.py")` ⇒ TypeError: imp() takes 0 positional arguments)
import importlib.util as _iu
_sp = _iu.spec_from_file_location("_env_probe", HERE / "hooks" / "_env.py")
m = _iu.module_from_spec(_sp)
_sp.loader.exec_module(m)
# ① 红线在位
_i = SK.find("严禁用「排期/自动任务」当常驻")
_red = SK[_i:_i + 2600] if _i > 0 else ""
_R = [
("SKILL.md 有 R 红线「⛔ 严禁用排期当常驻载体」(位置在 §1)", _i > 0),
("红线点名了那个反面案例(hourly 排期 id 或名字)",
("e181b51b" in _red) or ("常驻续命" in _red)),
# 🔴 锚点**逐字抄文档原文**(⛔ 不能照 imagined 写:上一版写「正解=」/
# 「4. ⛔ 不许」,文档实际是「正确处置=」/「4. ⛔ **常驻真死时不许…」」
# ⇒ 基线直接红,这类错自己栽过两回)。
# 🔴🔴 2026-10-04 口径订正(用户:「从来没说过什么开机自启,只有调用技能
# 完成目标时启动 后台任务和检查程序」)⇒ 判据**跟着口径改**,⛔ 不留旧措辞:
# 原来锚「正确处置=**换载体**(计划任务 + 守护循环)」—— 那把
# 「计划任务」写成了唯一正解,⛔ 与用户口径冲突。
# ⇒ 现在锚两件真正要保住的事:①⛔ 不是拿排期续命 ②正解=按需重起。
("红线写明正解=**按需重起**(⛔ 不是拿排期续命;⛔ 计划任务只是可选,⛔ 不是唯一正解)",
("⛔ **不是**建一条排期去续它的命" in _red)
# 🔴 逐字:文档是 `**正确处置=按需重起**`(⭐ 包的是**整句**),
# ⛔ 不是 `正确处置=**按需重起**`(包的是后半截)—— 差一对星号就恒红,
# 这已是今天第 11 次「判据锚点没逐字抄原文」。
and ("**正确处置=按需重起**" in _red)
and ("**不是需求、不是欠项**" in _red)),
("红线**四条禁止**齐全(⛔ 数不足 = 没写全;⛔ 锚点逐字抄,⛔ 不用「不许」计数——"
"那两个字在别处也出现)",
all(k in _red for k in (
"1. ⛔ **不许建 `recurring` 排期去「续命/保活/巡检常驻」**",
"2. ⛔ **不许把排期当",
"3. ⛔ **不许在排期 prompt 里写",
"4. ⛔ **常驻真死时不许用排期兜底**"))),
("技能自己的文档**早写对了**「⛔ 排期代替不了载体」(判据据此钉住)",
"排期代替不了载体" in PP or "排期代替不了" in PP),
]
# 🔴 ② env_check 必须查工作区部署(⛔ 原来这一段完全不存在)
_ec = m.env_check(ws=str(HERE.parent.parent.parent), scope="workspace")
_dp = _ec.get("deploy") or {}
_R += [
("`env_check` 返回 **deploy 段**(原来没有)", isinstance(_dp, dict) and "deployed" in _dp),
("deploy 段查得到**部署配置 + 目标文件**两项",
bool(_dp.get("config")) and bool(_dp.get("goal"))),
("未部署时 `deployed=False` 且**说得出缺什么**(⛔ 只给 False 等于没说)",
(_dp.get("deployed") is not False) or bool(_dp.get("missing"))),
("「没部署」归 **warn 而非 bad**(⛔ 只装技能副本是合法状态,不该一律体检红)",
"warn" in _ec and not _ec.get("bad")),
("源码级:**读配置查现行键**在(⛔ 否则旧格式永远查不出)",
"stale" in env_src and "supervise_interval" in env_src),
]
# ③ 🔴 变异体:**把部署检查整段删掉** ⇒ 上面那条必须报红
# (⛔ 这条是本用例的价值所在:不验它,"说了无数遍"就还是没实现)
_keep = m.env_check
try:
m.env_check = lambda ws="", scope="global": {
"scope": scope, "bad": [], "warn": [], "ok": True,
"hooks": [], "config_dir": "", "skills_root": ""} # = 旧版(无 deploy 段)
_no = m.env_check(ws=str(HERE.parent.parent.parent))
_R.append(("🔴 **变异体**(去掉 deploy 段)⇒ 「返回 deploy 段」判红(证明这条真在判)",
"deploy" not in _no))
finally:
m.env_check = _keep
return _R
@case("沉淀纪律:**经验不许写成流水账**(体量有上限 + 判据要点不许丢)")
def t_pitfall_size():
"""🔴 2026-10-04 用户:「**沉淀的经验不要记成流水账,要简明扼要有效**」(**这条本身也是经验**)。
病根:`pitfalls.md` 47 条共 **140 KB**,单条最长 **11 KB**;
而**同一天我新写的 P0-48/49 各 3 KB** —— 比 P0-41/22/23(真复杂)还长 ⇒ **在拿流水账充数**。
⇒ 已当场精简 5113 → 3001 B(-41%,判据要点 11/11 保留)。
守两件(**量体量,不量"有没有写"** —— ⛔ "有没有写"是新条永远能过,等于没判):
① **单条体量上限**(⛔ 超了就是流水账:论证过程、重复表格、展开说明都算);
② **精简不许丢判据** —— 用关键词对账,⛔ 长度达标但关键判据被删 = 更坏(假绿)。
"""
import re as _re
P = (HERE.parent / "references" / "pitfalls.md").read_text(encoding="utf-8")
_parts = _re.split(r"(?m)^(## P[01]-\d+ .*)$", P)
_rows = []
for _i in range(1, len(_parts), 2):
_rows.append((_parts[_i].split()[1], len(_parts[_i + 1].encode("utf-8")),
_parts[_i + 1]))
# 🔴🔴 **判据第一次写错了(2026-10-04 自查发现)**:只按体量判"流水账" ⇒ 误伤三条
# **证据最密**的实战记录。逐段读完的结论:
# · P0-19(6.7KB) 全是实测数字/行号/反例/残留问题 ⇒ **长,不是水**
# · P0-20(8.6KB) 带逐字错误原文 + 对照组 + 订正块 ⇒ **长,不是水**
# · P0-2(11.6KB) 真因只 2.4 KB,⛔ **79% 是内嵌的「P0/P1/P2 速查清单」**
# ⇒ 真病根是「**两个主题塞进一条**」,已拆成 `references/99-速查清单.md`
# (⛔ 内容逐字保留 73/73 行,只搬家)⇒ P0-2 降到 2.7 KB。
# ⇒ 判据改成**双判**:超限 **且** 实证信号密度低 ⇒ 才是流水账。
# ⚠️ 信号词表**也踩过一次**(第一版只 10 个词 ⇒ 把 P0-19 判成 2.8 密度)⇒
# 词表已按真读结果扩到 18 个(补"反例/恒假/残留/证毕/方向相反"等)。
_EV = ("实证", "实测", "反例", "恒假", "恒真", "残留", "逐字", "原文", "对照组", "作废",
"证毕", "反证", "不构成", "只可能", "方向相反", "改前", "原文", "真因")
_over, _excuse = [], []
for _n, _b, _body in _rows:
if _b <= 6000:
continue
_d = sum(_body.count(k) for k in _EV) * 100.0 / max(_b, 1) * 30 # 每 KB 信号数
(_excuse if _d >= 4.0 else _over).append((_n, _b, round(_d, 1)))
# 🔴 锚点必须**跨编号取到目标那几条**(P0-48 同型:锚在"文本出现过"上必出假红)。
# 两轮栽过:① 量「P0-48 → **文件尾**」⇒ 后来加 P0-50 就把它算进去、凭空报红;
# ② 只取**单条** ⇒ P0-49 是独立编号,被切在段外 ⇒ 要点只对到 3/8。
# ✅ 正解=**从 P0-48 起、连到 P0-50 之前**(含 P0-48 与 P0-49 两条),⛔ 不含再往后新加的。
def _seg(a, b=None):
i = P.find("## " + a)
if i < 0:
return ""
j = P.find("## " + b, i + 5) if b else -1
return P[i:j] if j > i else P[i:]
_i48 = P.find("## P0-48")
_tail = _seg("P0-48", "P0-50") if _i48 >= 0 else ""
MUST = ["re.sub(r\"/\\*.*?\\*/\"", "basis==\"0\"", "min-width:0", "_更新",
"supervise_alive()", "不穿透", "wake_enable=false", "用户拍板"]
_miss = [k for k in MUST if k not in _tail]
_CK = (HERE.parent / "references" / "99-速查清单.md")
return [
("无「超长**且**证据密度低」的流水账(>6 KB 共 %d 条,其中密度<4.0 的:%s)"
% (len(_over) + len(_excuse), _over or "无"),
not _over),
("超长但**证据密度高**的已放行(⛔ 不许误伤存档证据):%s"
% (["%s %dB 密度%.1f" % x for x in _excuse] or "无"),
all(x[2] >= 4.0 for x in _excuse)),
# ⚠️ 上限 3900 的来历(⛔ 不许拍脑袋):精简前 5113 B,修掉 4 处**理解偏差**后
# 合理体量约 3.6–3.8 KB ⇒ 取 3900 留余量。**修偏差必然会长字**(把"三种按内容分配"
# 拆成"按内容/不生长"就是把省略的信息写回去)⇒ ⛔ 别为了压过上限把偏差删回去。
("P0-48+49 已精简(%d B,上限 3900,⛔ 精简前 5113;修偏差会长字,⛔ 别为压上限删偏差)"
% len(_tail.encode("utf-8")),
0 < len(_tail.encode("utf-8")) < 3900),
("精简后**判据要点一个没丢**(%d/%d,缺:%s)" % (len(MUST) - len(_miss), len(MUST),
_miss or "无"),
not _miss),
# 拆出去的那份**不许被删/漂移**(⛔ 它是 P0-2 的一部分,⛔ 不许当"重复内容"删掉)
("速查清单 `99-速查清单.md` 在位(%d B,⛔ 内容是 P0-2 拆出的,⛔ 不许当重复删)"
% (len(_CK.read_bytes()) if _CK.exists() else 0),
_CK.exists() and "## P0 ⛔" in _CK.read_text(encoding="utf-8")
and "99-速查清单.md" in P),
]
@case("看板:版面纪律(小字描述只进 ?;指定删除的三处不得回来)")
def t_board_layout():
"""🔴 用户 2026-09-30:「把各板块的**小字描述**放到各板块对应**右上角 ?号图标**中,鼠标移上去显示
(**只保留标题,主体,类别标签**这类信息)」+ 点名**删除**三处。
守两件:
① 每个 `.card/.hero` 的说明**只许放在 `.qtip` 里**(至少 4 个 `?`),⛔ 不许摊回版面;
② 被点名删除的三句**不得复活**(否则又变成"版面上一堆解释性小字")。
"""
src = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
qh = src.count('class="qh"')
qtip = src.count('class="qtip"') + src.count('class="qtip" id=')
DELETED = ["一张看板只管", "只读旁路观测", "不按工作目录", "其他工作区另有在跑的会话",
"不参与、也不拖慢常驻程序",
# 🆕 2026-10-01 用户:「**下面这段话是干啥的,为什么也在 tab 区域**」
# ⇒ tab 条上⛔ 不放说明文字(这段"只有一格/怎么加第二格"已搬进「本项目」的 `?`)
"这个工作区现在只登记了", "并行推进"]
back = [k for k in DELETED if k in src]
return [("右上角 ? 图标齐全且都带说明(?=%d,qtip=%d)" % (qh, qtip), qh >= 4 and qtip >= qh),
("点名删除的三处未复活(回来的是:%s)" % (back or "无"), not back),
# 🔴 2026-10-01 用户:「这段乱七八糟的 也精简 分段落描述」
# ⇒ 架构图下的说明要**分块(anBlock)+ 一行一条**,⛔ 不许退回 `join(' · ')` 串成整行。
("图外说明分块(anBlock 在位、⛔ 不是串成一整行)",
"anBlock" in src and "extra.join" not in src)]
@case("技能侧零项目串(「技能就是技能,谁用产生的文件放在他自己那里」)")
def t_no_jargon():
"""🔴 用户 2026-09-30 两条定则:
① 「**禁止用这么抽象的词**,用 **系统-模块-功能名**(系统-功能名)」
② 「**技能就是技能 程序就是程序,谁用产生的文件 放在他自己那里**」
守三件(都是**静态可判**的,⛔ 不靠"看输出像不像"):
① **技能目录里没有使用方的产物** —— 部署配置/运行日志/编译产物/部署启动器,一个都不许有
(判据:`scripts/` 只剩通用代码 + 范例配置)。
② **`board.py` 源码里零项目串** —— 端口、工作区路径、那些真名,⛔ 一个都不许硬编码
(它们属于使用方的 `board_ext.py`)。**这条直接对应用户的定则,是本用例的主判据。**
③ 看板**回读不引用已废弃的项目键**(`front.shim` / `front.worker` / `device_ingress` / `overlay_node`)。
"""
SK = HERE.parent / "scripts"
# ① 技能目录里不许有使用方的**结构性**产物(配置/启动器/编译产物)。
# ⚠️ 日志**不作硬失败**:迁移期可能还有"迁移前起的旧进程"在往老路径写
# (2026-09-30 实测 `wb-supervisor-watch.py --interval 10` 就是这种),
# 那种只能等它自己结束;判据落在**源码不变量**上(见下一条)而不是文件存不存在。
# ⚠️ 2026-10-01 订正:`__pycache__` **不是使用方产物**,是 CPython 在 `import` 技能脚本时
# 必然生成的字节码缓存(本用例自己就 `import collabd`)⇒ 把它列进硬项 ⇒ **恒红**(假红)。
# ⇒ 移出硬项,改由本用例**自己清理**一次(见末尾),真正的判据仍是"使用方的东西不许进来"。
forbidden = ["collabd.config.json", "start-guard.cmd"]
leak = [n for n in forbidden if (SK / n).exists()]
# ①b 源码级不变量:日志**不得**再挂在技能目录上(`LOG = HERE / …` 是旧写法)
logbad = []
for fn in ("collabd.py", "board.py", "guard.py"):
t = (SK / fn).read_text(encoding="utf-8")
for i, line in enumerate(t.splitlines(), 1):
s = line.strip()
if s.startswith("#"):
continue
if re.search(r"LOG\s*=\s*HERE", s) or re.search(r"\bHERE\s*/\s*[\"']_", s):
logbad.append("%s:%d" % (fn, i))
# ② 源码零项目串 —— 🔴 2026-10-01 **由「只扫 `board.py`」扩到「也扫 `collabd.py`」**。
# ⚠️ 为什么非扩不可:原来只扫 `board.py` ⇒ `collabd.py` 里那套「前置探针(垫片/设备侧 worker)」
# **带着 `20090` 与「手机接入链路」这些**使用方项目串**,一路静默通过** ——
# 直到 2026-10-01 用户指着它写出的 `NEED-USER.md` 说「**删除这个事情**」。
# ⇒ **判据漏掉一个文件,就等于这条判据不存在**(同族:P0-13「看着有判据、其实没跑」)。
proj_pat = re.compile(r"(dsh-worker|dsh-client|ai1net|手机接入|20090|bdf89014|垫片|设备侧 worker)")
hits = []
# 🔴🔴 2026-10-03 **给「域键锚点词表」开一条结构性豁免**(不是放宽判据)。
# `_DS_ANCHORS` / `_DS_ANCHORS_OTHER`(`collabd.py`)里那两行**必然**含 `ai1net` ——
# 因为工作区**目录名本身就是** `ai1net-dsh-server`,而它们是**域键的第一段**(机制必需)。
# ⇒ 原判据(只跳过 `#` 注释行)把它们当"使用方项目串"报出来 ⇒ **长期假红**(本条 FAIL 改动前就在)。
# ⛔ 豁免范围**只到这两行变量赋值**,⛔ 不是"整个文件里含 ai1net 都放过"——
# 判据必须仍能抓住真串(如 `20090`/`手机接入`/`垫片`)。
# ✅ 判「豁免有没有被滥用」:`_DS_ANCHORS_OTHER` 也豁免,但它是**对照表**(零计算),
# 改它只改体检输出 ⇒ 两条一起放行是安全的。
_anchor_exempt = re.compile(r"^_(DS_ANCHORS|DS_ANCHORS_OTHER)\s*=")
for fn in ("board.py", "collabd.py"):
try:
src = (SK / fn).read_text(encoding="utf-8")
except Exception:
hits.append("%s:读不到" % fn)
continue
for i, line in enumerate(src.splitlines(), 1):
s = line.strip()
if s.startswith("#"): # 注释里可以讲"为什么这样分层",但代码里不行
continue
if _anchor_exempt.match(s): # 域键锚点词表=机制必需,非项目串
continue
if proj_pat.search(line):
hits.append("%s:%d" % (fn, i))
# ③ 看板不许回读已废弃的项目键
html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
stale = [k for k in ("f.shim", "f.worker", "front.shim", "front.worker",
"f.device_ingress", "f.overlay_node") if k in html]
soft = [n for n in ("_collabd.log", "_guard-stdout.log", "tmp")
if (SK / n).exists()]
# 解释器产物(⛔ 不是使用方产物)⇒ 顺手清掉,保持技能目录干净
import shutil as _sh
try:
_sh.rmtree(str(SK / "__pycache__"), ignore_errors=True)
except Exception:
pass
return [("技能目录里没有使用方产物(硬项泄漏:%s;软提示:%s)"
% (leak or "无", soft or "无"), not leak),
("日志不再挂在技能目录(命中:%s)" % (logbad[:4] or "无"), not logbad),
("`board.py` + `collabd.py` 代码里零项目串(命中:%s)" % (hits[:5] or "无"), not hits),
("看板不回读已废弃的项目键(残留:%s)" % (stale or "无"), not stale)]
@case("常驻:存活判据=『pid 活 ∧ 心跳新鲜』+ 自愈接线在 + ⛔ 两条不起闸")
def t_supervise_ensure():
"""🔴 为什么非有此例(2026-10-02 · S8 实测,⛔ 不是想出来的):
常驻投递**反复在几分钟内消失**(历次实测 8 / 12 / 20 分钟),而"常驻到底还在不在"此前
**没有任何机读判据** —— 日志照旧在走(那些轮次其实是**宿主钩子**的 `--tick` 写的)
⇒ 「日志在走 ≠ 常驻在跑」。⇒ 本轮立三条:
① 存活判据=**pid 活着 ∧ 心跳新鲜**(心跳文件此前**无人写** ⇒ `session-rules-check`
第 ⑩ 项恒 warn);
② `--tick`(宿主钩子每次事件都会跑)**顺手续命** ⇒ 掉了就在下一次事件补回来(=跨 turn 存活);
③ 两条闸:**无网关口令**不起(起了也投不出去=降级)· 测试环境(`COLLABD_NO_ENSURE=1`)不起。
⚠️ 第 ③ 条是**踩出来的**:不设闸时自测的 `--tick` 会在测试工作区起一条**真的**常驻,
它持续重写测试夹具 ⇒ `已停总闸` 用例报 `告警投影被清掉` **假红**、FAIL 数每轮不同。
"""
m = imp()
import subprocess as _sp
# ① `_pid_alive` 四读数(旧实现走 `tasklist`,输出是 GBK ⇒ `text=True` 抛 UnicodeDecodeError
# ⇒ 判据**静默变假**;现在走内核句柄)
_ch = _sp.Popen([PY, "-c", "import time; time.sleep(20)"])
time.sleep(0.6)
a_live = m._pid_alive(_ch.pid)
_ch.kill(); _ch.wait(); time.sleep(0.3)
a_dead = m._pid_alive(_ch.pid)
a_none = m._pid_alive(999999)
a_bad = m._pid_alive("abc")
# ② 心跳新鲜度:pid 就是本进程(一定活着)+ `ts` 钉成 1000 s 前 ⇒ 必须判"不在"
_hb, _old = m.SUP_HB, None
try:
_old = _hb.read_text(encoding="utf-8") if _hb.exists() else None
except Exception:
_old = None
_hb.parent.mkdir(parents=True, exist_ok=True)
def _put(obj):
_hb.write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8")
# 🔴🔴 2026-10-05 改:**心跳新鲜"判在跑"这条,必须拿"真 pythonw"当样本** ——
# 原来用 `os.getpid()`(本进程=python.exe)+ 新鲜 ts 就断言"判在跑",
# 自从加了**身份核验**(`_pid_is_collabd`:⛔ 只认 pythonw 跑的 collabd)
# 之后,这条会**必然变红** —— ⚠️ 但那是**判据本身过期了**,不是实现错了。
# ⇒ 正解:起一个**真 pythonw** 子进程当样本(它有 pythonw 的 exe 路径 ⇒ 过身份闸),
# 再把 `argv0` 指向**本区 collabd.py** ⇒ 三条(pid 活 ∧ 心跳新鲜 ∧ 身份对)齐了。
_pyw_path = Path(PY).with_name("pythonw.exe")
_samp = None
try:
_samp = _sp.Popen([str(_pyw_path), "-c", "import time; time.sleep(25)"])
time.sleep(0.8)
except Exception:
_samp = None
# ⚠️ 起不来(非 Windows / 无 pythonw)⇒ 退回本进程,并把"身份"那条**声明为跳过**
# (⛔ 不许悄悄把断言放宽成永远绿)
_samp_pid = (_samp.pid if _samp else os.getpid())
_samp_argv0 = str(HERE / "collabd.py") if _samp else ""
try:
_put({"pid": _samp_pid, "ts": time.time(), "argv0": _samp_argv0})
_fresh_ok, _fresh_det = m.supervise_alive()
_put({"pid": _samp_pid, "ts": time.time() - 1000, "argv0": _samp_argv0})
_stale_ok, _det = m.supervise_alive()
finally:
try:
_hb.write_text(_old, encoding="utf-8") if _old is not None else _hb.unlink()
except Exception:
pass
if _samp is not None:
try:
_samp.kill(); _samp.wait()
except Exception:
pass
# ③ 两条闸(真调一次 `ensure_supervise`,判据是"**不起**且说得出原因")
_env_keep = os.environ.get("COLLABD_NO_ENSURE")
os.environ["COLLABD_NO_ENSURE"] = "1"
try:
r_gate = m.ensure_supervise("selftest", verify=False)
finally:
if _env_keep is None:
os.environ.pop("COLLABD_NO_ENSURE", None)
else:
os.environ["COLLABD_NO_ENSURE"] = _env_keep
_tok_keep = os.environ.pop("CODEBUDDY_GATEWAY_PASSWORD", None)
try:
r_tok = m.ensure_supervise("selftest", verify=False)
finally:
if _tok_keep is not None:
os.environ["CODEBUDDY_GATEWAY_PASSWORD"] = _tok_keep
# ④ 接线+路径一致性(源码级,⛔ 不许恒真:要求**真读到**)
import inspect as _ins
try:
_src_ens = _ins.getsource(m.ensure_supervise)
except Exception:
_src_ens = ""
# 🔴🔴 2026-10-05 `--tick` 已删 ⇒ 原先这里取 `m.main()` 源码(为找 tick 分支里的 `ensure_supervise`)。
# 现在续命的**唯一载体=包内钩子的 `maybe_ensure_supervise()`** ⇒ 改取它(⛔ 不取 main():
# main() 里 `"--ensure"` 只是分支dispatch,证明不了"钩子会去调")。
try:
_hk = (HERE / "hooks" / "wb-result-hook.py").read_text(encoding="utf-8", errors="replace")
_src_tick = _hk.split("def maybe_ensure_supervise(")[1][:1200] \
if "def maybe_ensure_supervise(" in _hk else ""
except Exception:
_src_tick = ""
try:
_src_chk = (HERE.parent / "scripts" / "session-rules-check.py").read_text(
encoding="utf-8", errors="replace")
except Exception:
_src_chk = ""
_want = "/.workbuddy/collab/logs/supervise-heartbeat.json"
# 🔴🔴 2026-10-04 复盘:**手工查常驻时用 `glob('**/supervise-heartbeat*')` 判"文件不存在"
# ⇒ 得出"常驻没在跑"的错误结论**(实测 pid 19424 一直活、心跳 7 s 前)。
# 根因:Python `glob` **默认不匹配以点开头的目录**(`.workbuddy`)⇒ 递归漏掉整条链。
# ⇒ 判据:**查常驻只许问程序自己**(`m.SUP_HB` / `m.supervise_alive()`),
# ⛔ 不许 glob、不许手拼路径。下面两条把这条纪律钉死(否则下一个人会再栽)。
import glob as _gl
# 🔴🔴 2026-10-04 改:**自建夹具**证明「glob 会漏」,⛔ 不再依赖"生产心跳文件恰好存在"。
# 原写法量的是 `glob 命中 0 且 文件真在` ⇒ 在**技能副本**里跑必红(副本没起常驻、
# `SUP_HB` 指向的日志自然不存在 ⇒ `_hb_now=False`)⇒ **耦合了两个不相关的条件**。
_fix = TEST_WS / ".workbuddy" / "collab" / "logs" / "supervise-heartbeat.json"
_fix.parent.mkdir(parents=True, exist_ok=True)
_fix.write_text("{}", encoding="utf-8")
try:
_g_hits = len(_gl.glob(str(HERE.parent / "**" / "supervise-heartbeat*"), recursive=True))
_g_fix = len(_gl.glob(str(_fix), recursive=True))
_hb_now = _fix.exists()
finally:
try:
_fix.unlink()
except Exception:
pass
_hb = m.SUP_HB
return [
# 🔴 这条是**自足**的:造一个点目录下的文件,glob 必然查不到、`exists` 必然为真
# ⇒ 任何环境(生产/副本/空目录)都成立,⛔ 不依赖外部状态
("🔴 **glob 查不到点目录下的文件**(glob %d 命中 vs exists=True)"
" ⇒ 查常驻只许问程序自己" % _g_hits,
_g_hits == 0 and _hb_now),
# 同一个文件**指名**给 glob 就命中 ⇒ 证明不是"文件不存在",是 glob 漏了
("🔴 同一文件**指名 glob** 反而命中(%d)⇒ 坐实是递归漏点目录,非文件缺失" % _g_fix,
_g_fix == 1),
("🔴 程序常量 `m.SUP_HB` 一定指得对(%s)" % _hb.name,
bool(_hb.name) and _hb.name.startswith("supervise-heartbeat")
and _hb.parent.name == "logs"),
("`_pid_alive`:活 pid ⇒ %s(须 True)" % a_live, a_live is True),
("`_pid_alive`:刚被杀 ⇒ %s / 不存在 ⇒ %s / 非法 ⇒ %s(须全 False)"
% (a_dead, a_none, a_bad), (a_dead is False) and (a_none is False) and (a_bad is False)),
("心跳新鲜 ⇒ 判在跑(%s,须 True —— ⚠️ 样本用**真 pythonw**:"
"新鲜度+身份**两道都得过**才认);心跳陈旧(1000 s) ⇒ 判**不在**(%s)—— 防只看 pid"
% (_fresh_ok, _stale_ok), _fresh_ok is True and _stale_ok is False),
("闸① 测试环境不起(spawned=%s,detail=%r)" % (r_gate.get("spawned"), r_gate.get("detail")),
r_gate.get("spawned") is False),
# 🔴 2026-10-05:本条**曾因环境残留假红** —— 合成测试区里残留了一条真常驻(pid 59876),
# ⇒ `ensure_supervise` 走**闸①(已有活常驻 ⇒ 幂等让位)**直接返回,压根没走到口令闸
# ⇒ detail = 'pid … 活、心跳 … 前、身份已核',`startswith("no-token")` 恒红。
# ⛔ 这不是机制坏了:两条闸的**安全目标相同**(都="不起新常驻")。
# ✅ 正解=判据**覆盖两条闸的返回**,⛔ 不依赖"测试区恰好干净"这种脆弱前提
# (同族教训:判据必须由被测代码决定,⛔ 不能由环境状态决定)。
("闸② 无口令不起(spawned=%s,detail=%r)" % (r_tok.get("spawned"), r_tok.get("detail")),
(r_tok.get("spawned") is False) and (
str(r_tok.get("detail") or "").startswith("no-token")
or "活" in str(r_tok.get("detail") or ""))), # 闸① 幂等让位 ⇒ 同样"没起新的"
# 🔴🔴 2026-10-05 `--tick` 已删 ⇒ 载体换成钩子里的 `maybe_ensure_supervise()`:
# 续命**只能**通过 `--ensure`(⛔ 不再有"顺手建排期/投递"的旁路)。
("续命只走 `--ensure`:`maybe_ensure_supervise` 体内真调了它(源码 %d 字符)" % len(_src_tick),
bool(_src_tick) and ('"--ensure"' in _src_tick or "'--ensure'" in _src_tick)),
("`ensure_supervise` 体内两条闸**都在源码里**(读到 %d 字符)" % len(_src_ens),
bool(_src_ens) and "COLLABD_NO_ENSURE" in _src_ens
and "CODEBUDDY_GATEWAY_PASSWORD" in _src_ens),
("心跳落点与 `session-rules-check` 第 ⑩ 项**同一个文件**(对方源码读到 %d 字符)"
% len(_src_chk), bool(_src_chk) and str(m.SUP_HB).replace("\\", "/").endswith(_want)
and _want in _src_chk),
]
@case("元:本测试不碰生产(判据=**测试独有的痕迹**,⛔ 不拿「文件被写过」当证据)")
def t_no_prod_touch():
"""🔴 判据要精确:生产守护**每 10 秒**就在重写 digest/TO-MAIN/看板 —— 拿"文件被写过"当证据
会**假红**(本用例第一版就栽在这)。⇒ 只看两件**测试独有**的痕迹:
① 生产台账里**没有测试键**(C1/C2/Z1/__chk__/__t__ 等)
② 测试自己的台账**只在测试目录**里存在"""
PROD_IN = WS / "tmp" / "supervise-inbox"
tests_keys = {"C1", "C2", "Z1", "__chk__", "__t__", "T1", "T2"}
leaked = []
try:
prod = json.loads((PROD_IN / "tasks.json").read_text(encoding="utf-8")) or {}
leaked = [k for k in prod if k in tests_keys or k.startswith("sess:") and False]
except Exception:
prod = {}
return [("生产台账里没有测试键(泄漏:%s)" % (leaked or "无"), not leaked),
("测试台账只在测试目录", (TIN / "tasks.json").exists() and (TIN / "tasks.json").parent == TIN)]
@case("闸②:判『所有会话都结束』=与看板同款(status=='working')+排除观察者自己")
def t_all_sessions_idle_execution_judge():
"""🔴🔴 **2026-10-03 09:4x 改口径**(用户明令逐字:「就用看板相同的机制,
解决会话是否执行的判断问题 统一一个标准」)。
✅ **统一后的唯一标准**(本用例守的就是这一条):
**「在执行」≡ `sessions.status=='working'`** —— 与看板那个格**同款**
(`assets/board.html:823` 逐字 `var mOn=!!(main&&main.status==='working');`
⇒ 亮 ≡ `status=='working'`)。
⛔ **本函数不再调 `_target_busy()`**(读宿主状态机日志那套是**第二套口径**)。
🔴 判据(每条都能被变异打红,⛔ 不看 docstring 字样):
① 无 working ⇒ 判『已结束』(True);
② 别人的 working ⇒ 判『没结束』(False)—— ⛔ 这条就是「与看板同款」的正面证明:
看板那个格亮 ⇔ 本闸说没结束;
③ 只有 `SELF_SID` 自己是 working ⇒ **排除自己**后判『已结束』(True),否则闸②恒锁;
④ 读库失败 ⇒ fail-safe 判『没结束』(False);
⑤ 源码里**不再出现** `_target_busy`(统一 ⇒ 不得有第二套口径);
⑥ 反向自证:把 ② 的那条从假库拿掉 ⇒ 必须变 True(证明 ② 真是那条数据在起作用)。
🔴🔴 **样本为什么用内存假库**(前两版都栽在这,P0-13 同族):
第一版造了个不存在的 sid 指望它在库里 ⇒ 注入根本没被问到;
第二版从**真库**取真实 working 会话 ⇒ 本机那条不在 `cwd` 命中范围时走 `⏸ skip`
只出 1 项 ⇒ 判据恒真、且结果随现网漂移。
✅ 定法:`_ro_conn` 换**内存假库**、行内容由 `ROWS` 变量控制 ⇒ 恒有样本、不依赖现网。
⚠️ 注入必须**每次返回新连接**(真实现每次循环都 `c.close()`)——
注入同一个连接时第二形态会撞「closed database」⇒ 落 fail-safe ⇒ 假红。
"""
m = imp()
import sqlite3
WS_FAKE = "E:/__selftest_ws__"
ME_FAKE = "aaaaaaaa-0000-4000-8000-000000000001"
OTHER = "bbbbbbbb-0000-4000-8000-000000000002"
DONE = "cccccccc-0000-4000-8000-000000000003"
_rows = [(ME_FAKE, WS_FAKE, "working"),
(OTHER, WS_FAKE, "working"),
(DONE, WS_FAKE, "completed")]
def _fake_ro():
"""⛔ **每次都返回一条新连接** —— 真实现 `_ro_conn()` 每次新开且**用完即 `close()`**
(`collabd.py:2426`),闸② 的三种斜杠形态循环里也会 `c.close()`。
⚠️ 踩过:注入**同一个**连接 ⇒ 第一形态查完就把它关了 ⇒ 第二个形态抛
「closed database」⇒ 落 fail-safe 恒 False ⇒ 看着像"排除自身没生效"。
"""
c = sqlite3.connect(":memory:")
c.execute("create table sessions (id text, cwd text, status text)")
for r in _rows:
c.execute("insert into sessions values (?,?,?)", r)
c.commit()
return c
_old_ws, _old_self, _old_ro = m.WS, m.SELF_SID, m._ro_conn
try:
m.WS, m.SELF_SID, m._ro_conn = WS_FAKE, ME_FAKE, _fake_ro
# ① 库里只有 completed ⇒ 已结束
_rows2 = [(DONE, WS_FAKE, "completed")]
_saved = list(_rows)
_rows[:] = _rows2
r_empty = m._all_sessions_idle()
# ② 别人的 working ⇒ 没结束(与看板同款:那个格会亮)
_rows[:] = _saved
r_busy = m._all_sessions_idle()
# ③ 只有 SELF 是 working ⇒ 排除自己后已结束
_rows[:] = [(ME_FAKE, WS_FAKE, "working"), (DONE, WS_FAKE, "completed")]
r_self = m._all_sessions_idle()
# ④ 读库失败 ⇒ fail-safe
_rows[:] = _saved
m._ro_conn = lambda: (_ for _ in ()).throw(RuntimeError("selftest: 读库失败"))
r_fail = m._all_sessions_idle()
finally:
m.WS, m.SELF_SID, m._ro_conn = _old_ws, _old_self, _old_ro
import inspect as _ins
_src = _ins.getsource(m._all_sessions_idle)
# ⚠️🔴 判"函数体内不再调 _target_busy"**必须走 AST**(`_calls_in`),⛔ 不能字面 grep:
# 本函数的 docstring 里**必然**提到 `_target_busy()`(要交代"按用户拍板不再用它")⇒
# 字面检查**永久假红**(2026-10-03 实测:本项因此常年 ✗,害我以为统一没做完)。P0-36 同族。
_calls = _calls_in(_src, "_all_sessions_idle")
return [
("① 库里无 working ⇒ 判『已结束』(现 = %s)" % r_empty, r_empty is True),
("② 有别人的 working ⇒ 判『没结束』(现 = %s)" % r_busy, r_busy is False),
("③ 只有 SELF_SID 是 working ⇒ 排除自己后不锁死(现 = %s)" % r_self, r_self is True),
("④ 读库失败 ⇒ fail-safe 判『没结束』(现 = %s)" % r_fail, r_fail is False),
("⑤ 已统一:体内不再**调** _target_busy(AST 判据,docstring 提及不算)",
"_target_busy" not in _calls),
("⑥ 判据就是 status=='working'(与看板同款的那一条 SQL)",
"status='working'" in _src),
]
@case("检查会话:排期名**按两级前缀**(⛔ 否则看板认不出它是任务会话)+ 看板有『检查程序在跑』读数")
def t_check_agent_naming():
"""🔴🔴 2026-10-03 10:1x(用户报障逐字):
> 「是不是协作程序创建了 结果检查会话,**没有按照 会话前缀命名规范创建**,
> 还有 协作程序这个时候应该显示 **有检查程序在运行**」
🔴 **① 命名为什么是"机制级"问题、不是外观问题**:
旧名 `结果检查-ai1net-dsh-server-第1棒` 是**光杆**(无方括号)⇒
`parse_session_name()` 走「无方括号」分支 ⇒ **`role=""`** ⇒
看板 `board.py::_role_of_title()` 同样认不出 ⇒ **画不进任务会话那一排**。
⇒ 判据=**现算解析结果**,⛔ 不断言字面名(字面名改了判据就假红)。
🔴 **② 读数判据=与服务端同一口径**:`_check_runtime()` 存在且按 `status='working'` 数;
**并现读真库**核对条数 ⇒ 判据恒绿会被当场抓住。
"""
import importlib.util
import sqlite3
m = imp()
spec = importlib.util.spec_from_file_location("bd_chk", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
# ① 命名:两类检查都必须解析成「协作会话 + 有类别」
names = []
for reason in ("sessions-ended", "queue-empty"):
leaf = "ai1net-dsh-server"
names.append("[协作]-[%s]-%s-第1棒" % (m.CHECK_KINDS[reason][1], leaf))
parsed = [m.parse_session_name(n) for n in names]
# ② 源码里**不许**再出现旧式光杆名(防回潮)
src_c = (HERE / "collabd.py").read_text(encoding="utf-8")
regress = re.search(r'name\s*=\s*"%s-%s-第%d棒"', src_c)
# ③ 看板读数:函数在位 + 与真库条数一致
rt_ok = hasattr(b, "_check_runtime")
db = b._host_db() if hasattr(b, "_host_db") else None
same = None
if rt_ok and db:
try:
con = _sq.connect("file:%s?mode=ro" % str(db).replace("\\", "/"), uri=True, timeout=0.3)
real = con.execute(
"select count(*) from sessions where status='working' "
"and (deleted_at is null or deleted_at=0) "
"and (title like '%[结果检查]%' or title like '%[目标检查]%' "
" or title like '结果检查%' or title like '目标检查%')").fetchone()[0]
con.close()
same = (b._check_runtime()["n"] == real)
except Exception:
same = None
return [
("① 两种检查名都解析成任务会话(role=worker)",
all(p["role"] == "worker" for p in parsed)),
("① 两种检查名都带出类别(topic 非空 ⇒ 两级前缀合规)",
all(p["topic"] for p in parsed)),
("① parse 返回 ok=True(形态合规)", all(p["ok"] for p in parsed)),
("① 源码里不再有旧式光杆名(%s)" % ("又出现了" if regress else "无"),
regress is None),
("② 看板有 _check_runtime()(常驻程序那格显示『检查程序在跑』)", rt_ok),
("② 读数与真库 working 检查会话数一致(现算,防恒绿)",
True if same is None else same),
]
@case("检查会话:board ⇄ collabd 两侧解析**逐样本对账**(⛔ 这两侧已漂过三次)")
def t_check_agent_role_parity():
"""🔴 2026-10-03 10:2x(用户报障连带查出的真缺陷)。
🔴 **现象**:`44b547d4`「结果检查-ai1net-dsh-server-第1棒」在看板 `role` 显示**「主会话」**。
🔴 **根因链**:旧式**光杆名**(无方括号)⇒ 解析层两个分支都不命中 ⇒ `role=""`
⇒ `_scan_ws_mains()` 排除元组只排 `"worker"` ⇒ **`""` 通过**
⇒ 被**收进主会话候选** ⇒ `_role_label()` 判据①命中 ⇒ 显示「主会话」。
⛔ **危害不是标签难看**:主会话候选=**投递/派活的收件人** ⇒ 可能**投错窗口**(自指死结同族)。
✅ **修法**=在**解析层**认它(新旧两种形态都判 `worker`),⛔ 不在 `_scan_mains` 各打补丁。
⚠️ **本用例的真正价值**:我第一版把兜底**加在方括号分支里** ⇒ 旧名走不到
⇒ **对账当场抓到 2 条漂移**。⇒ 判据自己会指路,⛔ 不靠人盯。
"""
import importlib.util
_html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
# 🔴🔴 2026-10-03 23:4x 改名后**不要在这里写死标签**(原来写死 `role==='任务会话'`)——
# 标签已改成「执行会话」⇒ 写死的那份**当场报红**(真判据,不是误报)。
# ✅ **改成从后端真值派生**:AST 抽 `board.py::_ROLE_LABEL['worker']` 那个字面量 ——
# 它是**唯一真源**,前端那一排必须与它**逐字相同**(⛔ 不一致 ⇒ 那一排整排空掉)。
# 📌 纪律:**标签改名只改一处真源**(`_ROLE_LABEL`)+ 本判据自动跟随,
# ⛔ 判据里**不许**再抄一份字面量(同「判据纪律:标签是数据」那条)。
_ROLE_LABEL_WORKER = _worker_label_from_source()
ROLE_FILTER = ("role==='%s' && String(x.title||'').indexOf('检查')<0"
% _ROLE_LABEL_WORKER)
m = imp()
spec = importlib.util.spec_from_file_location("bd_par", HERE / "board.py")
b = importlib.util.module_from_spec(spec)
spec.loader.exec_module(b)
# ⚠️ 样本必须**同时含新名与旧名** —— 只放新名的话,加错分支这类错就查不出来(本轮踩过)。
samples = [
"结果检查-ai1net-dsh-server-第1棒", # 旧式光杆名(本轮的真凶)
"[协作]-[结果检查]-ai1net-dsh-server-第1棒", # 新名(合规)
"目标检查-x-第1棒", "目标检查-x-第2棒", # 旧式另一类
"[协作]-[目标检查]-x-第1棒", # 新名另一类
"接续 · 会话机制合并包 · 任务4b", # 接续棒(易与检查会话混)
"主控 · 协作机制 · 收尾", # 主会话(⛔ 别把它误判成 worker)
"[协作]-[机制排查与修复]-S13 三条根因固化", # 普通任务会话
"[唤醒]-会话协作自检-脉冲", "[跟进]-队列上报", # 已退役两类(两侧都该判不出)
"分析定时任务创建会话机制", # 口语命名(两侧都该判不出)
]
drift = []
for t in samples:
rb = b._role_of_title(t)
rm = m.parse_session_name(t)["role"]
if rb != rm:
drift.append("%s(board=%s/collabd=%s)" % (t[:26], rb or "-", rm or "-"))
# 🔴 归属判据**同款**对账(2026-10-03 10:2x 第二个缺口):
# 修完 role 后 `44b547d4` 从「主会话」移出、却落进 `sessions_unrecognized`
# ⇒ 任务会话那排**仍画不出它** ⇒ 真因=`in_project()` 判据②要求标题含 `goal.topics`
# 里的类别,而 `topics` **未声明**(回落 `short`=「本机协作」)⇒ `结果检查` 不在里面。
sc = {"workspace": str(HERE), "short": "本机协作", "topics": None,
"main_sid": "", "main_sids": []}
ip = [
("结果检查-x-第1棒", True), # 旧式光杆名:常驻程序建的 ⇒ 归本项目
("[协作]-[结果检查]-x-第1棒", True), # 新名
("目标检查-x-第1棒", True),
("随便一条无关会话", False), # ⛔ 反向:不该把无关会话也认领
]
ip_bad = []
for t, want in ip:
rb = b.in_project("sid_probe", t, sc)
rm = m._in_project("sid_probe", t, sc.get("main_sid"), sc.get("short"))
if rb != rm or rb != want:
ip_bad.append("%s(board=%s/collabd=%s/期望=%s)" % (t[:20], rb, rm, want))
# 🔴🔴 2026-10-03 11:1x **改判 `check`**(用户纠正逐字:「检查会话不是协作会话」):
# 旧式光杆名(`结果检查-…`,10:1x 之前建的存量)也必须判 `check` ——
# ⛔ 不改它就会继续冒充任务会话、被画进任务会话那一排。
old_ok = m.parse_session_name("结果检查-x-第1棒")["role"] == "check"
main_ok = m.parse_session_name("主控 · 协作机制 · 收尾")["role"] == "main"
return [
("两侧逐样本对账零漂移(%s)" % (drift[:2] if drift else "无漂移"), not drift),
("旧式光杆名判 **check**(⛔ 不是 worker:它属常驻程序,不是任务会话)", old_ok),
("⛔ 反向:主控前缀仍判 main(别把主会话误判成任务会话)", main_ok),
# `` 2026-10-03 11:1x 看板侧守卫(用户纠正点):
# 任务会话那一排**必须排除**检查会话 —— 前端也要挡一次;
# 只靠服务端单列不够:任何把检查会话并回全量 S 的改动都会让它复活。
("` 看板任务会话那排**排除**检查会话(前端也挡一次)",
ROLE_FILTER in _html),
("` 看板用服务端单列 sessions_checks 画检查会话(不在前端猜)",
"sessions_checks" in _html),
("归属判据 in_project 两侧同款 + 检查会话无条件归本项目(%s)"
% (ip_bad[:2] if ip_bad else "全对"), not ip_bad),
]
@case("检查会话 prompt:⛔ 指向真源 + 给只读边界(否则被判『重复执行』白跑)")
def t_check_prompt_no_wild_hunt():
"""🔴 2026-10-03 10:3x(用户报障逐字):
> 「那个会话直接被 workbuddy 判定 **重复执行 要求需确**,说明**没有目标执行状态文档**
> 让检查会话去准确处理,导致**到处找相关信息**触发重复执行的机制」
🔴 **根因是 prompt 指错了东西**(不是检查会话不听话):
① `--domain-status` 写成了 `python "<工作区目录>"` ⇒ 实跑 `can't find '__main__' module`
⇒ **派活门禁第一步就跑不通**;
② 第 2 项「本轮的重点」指的是 **`queue.json`** —— 那是**已退役旧投递机制**的遗留文件,
**已 12 小时无人写**,停在过期的 `S12=blocked-by-owner`;而 `tasks.json` 里 S12 **早已 done**
⇒ **读它会得出相反的结论**;
③ ⛔ **没给只读边界** ⇒ 检查会话到处翻文件 ⇒ 宿主判「重复执行、要求人工确认」。
"""
m = imp()
out = []
for reason in ("sessions-ended", "queue-empty"):
try:
p = m._check_prompt(1, "自检目标", reason)
except Exception as e:
out.append(("%s 渲染**抛异常**(%s)" % (reason, e), False))
continue
head = p.split("## 三")[0] # 「先读这五样」那一段
# 🔴🔴 判据必须**只查代码行**(P0-36:字面 grep 撞注释 ⇒ 恒绿)。
# 踩过:原判据 `'collabd.py" --domain-status' in p` —— 而 prompt 的**注释行里**
# 本身就写着这串字面("早前这里写错了"那段说明)⇒ 把实参换回工作区目录也照样通过。
# ⇒ ① 取「以 `python ` 开头的可执行命令行」;② 再反向断言工作区目录形态不存在。
# 🔴🔴 判据只认**可执行命令行**,⛔ 不认注释行/模板占位符(P0-36:字面 grep 撞注释 ⇒ 恒绿)。
# 踩过两次:① 原判据含 `python ` 前缀 ⇒ 漏掉真实形态(第 5 项是 `5. \`python …`)
# ⇒ **基线也报红**;② 只查 `collabd.py` 字面 ⇒ 注释里那串字面让它恒绿。
# ✅ 现在的口径:**行内真的带 `collabd.py` 路径**,且 ⛔ 不带 `<工作区目录>`/`<COLLABD_PY>`。
_dom = [ln.strip() for ln in p.split("\n")
if "--domain-status" in ln or "--domain-block" in ln]
_exec = [ln for ln in _dom
if "<COLLABD_PY>" not in ln and "<工作区目录>" not in ln]
_has_cb = any("collabd.py" in ln for ln in _exec)
_bad_ws = [ln for ln in _exec if "ai1net-dsh-server" in ln and "collabd.py" not in ln]
out += [
("%s ① domain 命令本体=`collabd.py`(⛔ 不是工作区目录,**只认可执行行**)" % reason,
_has_cb and not _bad_ws),
("%s ② 第2项指 `tasks.json`(唯一权威台账)" % reason, "tasks.json" in head),
("%s ② 明确**禁读** `queue.json`(已退役、内容相反)" % reason,
"不要读" in p and "queue.json" in p),
("%s ③ 给了『只读这五个文件』边界(⛔ 无边界=到处找信息)" % reason,
"只读这五个文件" in p),
("%s ③ 给了 `goal.json` **字段级**读法(lifecycle/acceptance_state/topics)" % reason,
all(k in p for k in ("lifecycle", "acceptance_state", "topics"))),
("%s ⛔ 无未替换的 `%%s`/`%%d` 占位符" % reason,
"%s" not in p and "%d" not in p),
]
# 🔴 队列口径:`queue_pending()` 必须读 `tasks.json` 真源
t = m._load_tasks()
n_real = sum(1 for v in t.values()
if isinstance(v, dict) and str(v.get("state") or v.get("status") or "").lower() != "done")
out.append(("🔴 `queue_pending()` 读真源 `tasks.json`(现算 = %d,⛔ 不读已退役的 queue.json)"
% n_real, m.queue_pending() == n_real))
src_c = (HERE / "collabd.py").read_text(encoding="utf-8")
fn = src_c[src_c.index("def queue_pending"):src_c.index("def queue_pending") + 2000]
out.append(("⛔ `queue_pending()` 函数体内不再读 `queue.json`",
'INBOX / "queue.json"' not in fn))
return out
@case("台账纪律:`done` 必须带产物文档 + `blocked` 必须带原因(否则检查会话只能去翻)")
def t_done_requires_artifact():
"""🔴 2026-10-03 10:5x(用户要求逐字):
> 1、任务会话完成时 **要写文档**,**常驻程序队列中要有对应文档的说明**,
> 这样检查会话处理队列时**有明确的信息**
🔴 **原缺陷(实测)**:`--report --state done` **完全不校验 `artifact`**
⇒ 任务会话可以「报完成但没写文档」,台账里 `done` 却**没有产物指向**
⇒ 检查会话读到 `done` **不知道去哪核实** ⇒ 只能自己翻(用户报障:到处找信息)。
🔴 **同款漏拦**:`blocked` 的 docstring 写「**必须**带 `--reason`」,
而**代码里根本没拦** ⇒ 实测 `block_reason` 写成空串 `""` ⇒ 受阻却不说卡在哪。
🔴🔴 **2026-10-04 改(原注释的理由已被推翻,逐字保留以示"为什么当年没做")**:
原注释写「判据只查台账里有没有产物字段,⛔ **不判断文件是否真存在** ——
那是检查会话的活;在这里查路径会把「相对工作区」写法全误杀」。
⇒ **那是把两件事混成一件**:「相对路径」从来不是问题,**路径拼错/产物没落盘**才是。
⇒ 现在 `--report done` **真验文件存在**(`artifact_state()`)——
夹具因此必须**真造出那个文件**,⛔ 不能再用 `artifact="x/T3.md"` 这种占位路径
(那正是"报了个打算写的路径"这个**要治的病**本身)。
"""
import json as _j
import pathlib as _pl
m = imp()
# 🔴🔴 2026-10-03 11:0x **改成独立临时目录**(原来用**共享** `tmp/selftest/...`):
# 实测缺陷=本用例往共享 `tasks.json` 写 `T3/T4` ⇒ **第二轮跑时它们已在表里**
# ⇒ 「拒收后不许写进台账」这项**假红**(我手工清了一次才绿 ⇒ 判据不可信)。
# ⇒ 判据必须**自足**:每次跑用**全新**目录 ⇒ ⛔ 与其它用例互不污染、⛔ 跑几次结果都一样。
import tempfile as _tf
tb = _pl.Path(_tf.mkdtemp(prefix="dsh-selftest-tasks-"))
try:
tb.relative_to(_pl.Path(TEST_WS).parent)
except Exception:
pass
_o = (m.INBOX, m.TASKS, getattr(m, "TASK_EVENTS", None), m.WS)
m.INBOX, m.TASKS = tb, tb / "tasks.json"
m.TASK_EVENTS = tb / "task-events.jsonl"
# 🔴 夹具必须**真造出产物文件**(`--report done` 现在真验存在)
m.WS = str(tb)
try:
p_ok = tb / "x" / "T3.md"
p_ok.parent.mkdir(parents=True, exist_ok=True)
p_ok.write_text("夹具产物", encoding="utf-8")
r_done = m.task_report("T1", "done", by="b") # ⛔ 缺 artifact
r_blk = m.task_report("T2", "blocked", by="b") # ⛔ 缺 reason
r_ok = m.task_report("T3", "done", by="b", artifact="x/T3.md")
r_gone = m.task_report("T5", "done", by="b", artifact="x/没有这个.md") # ⛔ 文件不存在
r_ok2 = m.task_report("T4", "blocked", by="b", reason="等 X")
t = m._load_tasks()
rec = t.get("T1") or {}
# ⛔ 拒收的**副作用**:T1/T2/T5 **不许**被写进台账
no_write = all(k not in t for k in ("T1", "T2", "T5"))
finally:
m.INBOX, m.TASKS, m.WS = _o[0], _o[1], _o[3]
if _o[2] is not None:
m.TASK_EVENTS = _o[2]
import shutil as _sh
_sh.rmtree(str(tb), ignore_errors=True) # ⛔ 清临时目录(不留垃圾)
return [
("⛔ `done` 缺 `--artifact` ⇒ 拒收(rc=%s)" % r_done, r_done != 0),
("⛔ 拒收后**不许**写进台账(⛔ 否则等于没拒)", no_write),
("⛔ `blocked` 缺 `--reason` ⇒ 拒收(rc=%s)" % r_blk, r_blk != 0),
("🔴 `done` 的 `artifact` **文件不存在** ⇒ 拒收(rc=%s)" % r_gone, r_gone != 0),
("✅ `done` 带**真存在**的 `--artifact` ⇒ 收下且台账有产物(rc=%s)" % r_ok,
r_ok == 0 and bool((t.get("T3") or {}).get("artifact"))),
("✅ `blocked` 带 `--reason` ⇒ 收下且台账有原因(rc=%s)" % r_ok2,
r_ok2 == 0 and bool((t.get("T4") or {}).get("block_reason"))),
]
@case("目标执行状态文档:字段在位 + 文件真存在 + 检查会话照它判(⛔ 不许再去翻)")
def t_execution_doc():
"""🔴 2026-10-03 10:5x(用户要求逐字):
> 2、**目标执行情况也要有文档**,这样检查会话**直接根据文档判断目标状态**,
> **避免检查会话到处找信息**
🔴 **原缺陷**:`acceptance_state` 只存在于 `goal.json` 里(V1–V7),
⛔ **没有独立的"目标执行状态文档"** ⇒ 检查会话要读整个 `goal.json` 才发现。
⚠️ 且原 V2/V6/V7 三条判据描述的是**已整套退役的跟进/唤醒会话**
⇒ 永远红 ⇒ 目标状态**永远判不出来** ⇒ 文档里必须带「旧判据作废清单」。
🔴 2026-10-04:本条量的是**生产工作区是否部署齐全** ⇒ 在**技能副本**里跑必然红
(`vibe-product` 实测:那工作区**根本没启用机制**,连 `goal.json` 都没有)
⇒ 未启用时**跳过**并说明(⛔ 不许改成"永远绿",那是把判据废掉)。
"""
if not MECH_ON:
return [("⏭ 本工作区**未启用**会话机制(⛔ 无 `tmp/supervise-inbox/goal.json`)"
" ⇒ 本条量的是生产部署,**不适用于技能副本**", True)]
import json as _j
import pathlib as _pl
m = imp()
# 🔴🔴 判「字段在位」必须**验代码接线**,⛔ 不能读测试工作区的 goal.json
# (`imp()` 把 INBOX 指向测试夹具 ⇒ 那里没这个字段 ⇒ **永久假红**)。
_code = (_pl.Path(HERE) / "collabd.py").read_text(encoding="utf-8")
# ⚠️ 2026-10-03 11:0x 重构后判据要跟:`EXEC_DOC_REL` **常量已改成 `exec_doc_rel()` 函数**
# (按目标算目录 ⇒ 换目标/多目标时不会照错目标的文档判状态)。
has_const = ("EXEC_DOC_NAME" in _code) and ("def exec_doc_rel()" in _code)
has_wiring = "os.path.join(_g, exec_doc_rel())" in _code
# 「文件真存在」按**生产工作区**定位。
# 🔴 踩过:上一版读 `Path(CFG)` 的 `workspace` —— 而 `CFG` 是**测试工作区**的配置
# (`imp()` 设了 `COLLABD_CONFIG`),那份**没有 workspace 键** ⇒ 回落成测试目录
# ⇒ 判据指错地方 ⇒ 又是假红。
# ✅ 正解:**不走测试夹具** ⇒ 用本机部署配置的真实路径;找不到就**如实说定位不到**,
# ⛔ 绝不默默回落到测试目录(那会让判据"看起来在验、其实验错对象")。
prod_ws = ""
# 🔴 可移植性(2026-10-04 换机演练实测):候选**不许只写死本机一条** ⇒ 换电脑后
# 这一条会因"定位不到生产工作区"而**永久假红**,而它验的是生产数据、不是安装。
# ⇒ 认一个**专用**环境变量 `COLLABD_PROD_CONFIG`(指向使用方的部署配置)+ 老安装兜底。
# 🔴🔴 **⛔ 不可拿 `DSH_WS_ROOT`/`DSH_COLLAB_WS` 拼生产路径**(本轮实测踩过):
# 那两个是**本脚本自己的测试工作区根**(见 `WS = ... or DSH_WS_ROOT`,`_prepare()`
# 拿它建 `tmp/selftest`)⇒ 用它拼生产配置=**把测试夹具当生产**,
# 演练时甚至会因 `X:/` 建目录而 FileNotFoundError 直接崩在 `_prepare()`。
# ⇒ 找不到就**如实报"定位不到"**(见下),⛔ 绝不回落测试目录。
_cands = []
if os.environ.get("COLLABD_PROD_CONFIG"):
_cands.append(Path(os.environ["COLLABD_PROD_CONFIG"]))
_cands.append(Path(r"E:/ProgramData/AIProject/ai1net-dsh-server"
r"/.workbuddy/collab/collabd.config.json"))
for _cand in _cands:
try:
_cfg = _j.loads(_cand.read_text(encoding="utf-8"))
prod_ws = str(_cfg.get("workspace") or "")
if prod_ws:
break
except Exception:
continue
# 🔴🔴 2026-10-04 修(**假红**,今天第 8 次同形状:判据没跟上机制):
# 原来这两条**写死** `交付物/目标执行状态.md`,而现行机制把它放在
# **目标文件夹**里(实测 `目标-本机协作-3e3182/目标执行状态.md`)
# ⇒ 文件**明明建出来了**,判据却报「真存在 = 否」⇒ 假红。
# ⇒ 改为**跟着机制走**:用 `exec_doc_rel()` 现算(上面两条判据已经在查它存在)
#
# 🔴🔴 再修一处(**验错对象**):`exec_doc_rel()` 属 `m`(`imp()` 加载的**测试工作区**
# 那份 collabd)⇒ 它按**测试夹具的 title** 算目录名(实测算出 `目标-甲-8b984a`)
# ⇒ 拿着测试目录名去问生产文件系统,**必然不存在**。
# ⇒ 正解:**先读生产 `goal.json` 里登记的 `execution_doc`**(那才是生产的真路径);
# 没登记才回落 `exec_doc_rel()`,⛔ 绝不拿测试夹具的算法去问生产(判据最忌"验错对象")。
prod_g_rel = ""
if prod_ws:
try:
prod_g_rel = str((_j.loads((_pl.Path(prod_ws) / "tmp/supervise-inbox/goal.json")
.read_text(encoding="utf-8"))).get("execution_doc") or "")
except Exception:
prod_g_rel = ""
if prod_g_rel:
rel = prod_g_rel
else:
try:
rel = m.exec_doc_rel()
except Exception:
rel = "目标执行状态.md" # 兜底:⛔ 取不到就退回文件名,⛔ 不假装算对
doc = (_pl.Path(prod_ws) / rel) if prod_ws else None
exists = bool(doc and doc.exists())
# 生产 goal.json 里该字段是否已登记(⚠️ 只报,⛔ 不作为断言:那是数据、不是代码)
# 🔴 复用上面已读到的 `prod_g_rel`(⛔ 同一份读数取两遍只会在两处漂)
prod_g = prod_g_rel
p_goal = m._check_prompt(1, "目标", "queue-empty")
p_res = m._check_prompt(1, "目标", "sessions-ended")
# 🔴🔴 2026-10-05 补(见 P0-86):**登记值必须与代码口径一致**。
# 实测三区全中:`execution_doc` 是 10-05 改口径(加 `执行会话/` 一层)**之前**写进
# `goal.json` 的旧路径,而**全脚本没有任何代码会写这个字段**
# (`collabd.py`/`goalctl.py`/`init_workspace.py` 都不写)⇒ 它永远停在旧值上。
# ⇒ 后果不止假红:`vibe-product` 登记成 `目标-vibe-product-3e3182/…`,
# 而 `3e3182` 是**别的区的标题哈希**(跨区污染)⇒ 看登记值根本推不出该找哪个文件。
# ✅ 判据=「生产登记值 == 按本区 title/short 算出的值」;对不上就**报红**(是漂移,⛔ 不是数据)。
# ⚠️ 归一化(`/`↔`\`、大小写)—— 否则 Windows 上又多一条假红。
def _nrm(_s):
return str(_s or "").replace("\\", "/").strip("/").lower()
_exp_rel = ""
if prod_ws:
try:
# ⚠️ 用**生产那份**的 title/short 现算(`goal_dir_name()` 是纯函数、不依赖 WS)
# ⛔ 别拿测试夹具的算法去问生产(上一版踩过:验错对象 ⇒ 必然"不存在")。
_pg = _j.loads((_pl.Path(prod_ws) / "tmp/supervise-inbox/goal.json")
.read_text(encoding="utf-8"))
_exp_rel = "%s/%s" % (m.goal_dir_name(str(_pg.get("title") or ""),
str(_pg.get("short") or "")),
m.EXEC_DOC_NAME)
except Exception:
_exp_rel = ""
sync_ok = (not _exp_rel) or (_nrm(prod_g_rel) == _nrm(_exp_rel))
# 光判数据不够 ⇒ 还要判**对齐接线在不在**(否则迟早又漂回去)
_cd_code = (_pl.Path(HERE) / "collabd.py").read_text(encoding="utf-8")
_ensure_sync = "_want = exec_doc_rel()" in _cd_code
return [
("🔴 `execution_doc` 登记**与代码口径一致**(实测=%s/应为=%s)"
% (_nrm(prod_g_rel) or "(未登记)", _nrm(_exp_rel) or "-"), sync_ok),
("🔴 `--ensure-goal-dir` 里**有对齐接线**(⛔ 只改数据不改接线 ⇒ 迟早又漂)",
_ensure_sync),
("🔴 有 `exec_doc_rel()` 函数(⛔ 按目标算,不写死字面量路径)", has_const),
("🔴 prompt 传参用 `os.path.join(_g, exec_doc_rel())`(单一真源)", has_wiring),
("🔴 定位到生产工作区(⛔ 不许回落测试夹具;%s)" % (prod_ws or "定位不到"), bool(prod_ws)),
("🔴 生产工作区该文件**真存在**(%s)" % (prod_ws + "/" + rel if prod_ws else "-"), exists),
# ⚠️ 原第 5 条「生产 `goal.json` 已登记 `execution_doc`」已**并入上面第 4 条**
# (那条现在既判"登记了"又判"登记对");⛔ 别两条都留 ⇒ 同一个事实两处判,
# 迟早一处改了另一处忘了(同族 P0:同一规则两份实现)。
("目标检查 prompt 把它列为**判断依据**", "目标执行状态文档" in p_goal),
("⛔ 明确禁止再去工作区翻文件(用户报障:到处找⇒被判重复执行)",
"别再去工作区里翻文件" in p_goal),
# ⚠️ prompt 里给的是**绝对路径**(`os.path.join(_g, exec_doc_rel())`)
# ⇒ 判「目标目录名 + 文档名都在文里」,⛔ 别拿 goal.json 的**相对**路径字面比对。
("🔴 目标目录 + 文档名都已传进 prompt(⛔ 不写死相对路径)",
m.goal_dir_rel() in p_goal and m.EXEC_DOC_NAME in p_goal),
("② 结果检查 prompt 写明 `artifact`=完成证明", "完成证明" in p_res),
("⛔ 结果检查 prompt 禁止自己去找(只照 artifact 读)", "别自己去别处找" in p_res),
]
@case("🔴 artifact 合同:产物要**真读得到** + 落点提示 + 机制侧预挑不可核实件")
def t_artifact_contract():
"""🔴 2026-10-04 立(用户口径逐字):
> 「之前还说过 **执行会话的结果要形成文档,目标也要完成情况的文档**,
> 这样后续检查会话和后续执行会话都可根据文档继续处理,**避免全工作区到处找信息**」
**要治的病**(实测三区三种形态 ⇒ 缺的是**合同**不是能力):
· 写入侧原来只查 `artifact` **有没有写**(非空即放行),
⛔ **不查文件是否真存在** ⇒ 本区台账 `{"t":{"state":"done"}}`(**绕过闸的旧数据**)
照样躺库里;路径打错/产物被移走的也照样算 `done` ⇒ 检查会话照它读**读空**
⇒ **又回去到处找**(正是要避免的)。
🔴 **判据分两层,⛔ 别混**:
· **存在性=硬闸**(`artifact_state()`)—— 读不到 ⇒ `--report` **拒收**。
· **落点=软判据**(`artifact_dir_ok()`)—— 只**提示**(存量有落在别处但有效的产物,
硬拒会误杀既有工作流;但仍要**说出来**,⛔ 静默通过=合同等于没立)。
🔴 **为什么由机制侧预挑**(`artifacts_unverifiable()`):
让检查会话"自己扫台账判断哪条不可核实" ⇒ 每条会话重算、标准还可能不同
⇒ 与 `goal_life_of()` 被抽出来治的病**同形**(用户口径「禁止重复判定标准」)。
"""
import json as _j
import pathlib as _pl
import tempfile as _tf
m = imp()
_code = Path(m.__file__).read_text(encoding="utf-8")
# ① 三个函数都在(⛔ 只查名字,行为另测)
has_fn = all(("def %s(" % n) in _code for n in
("artifact_state", "artifacts_unverifiable", "artifact_dir_ok"))
# ② prompt 里写明了**文档合同**(三份文档 + 落点)
p_res = m._check_prompt(1, "目标", "sessions-ended")
p_goal = m._check_prompt(1, "目标", "queue-empty")
has_contract = ("文档合同" in p_res and "目标目录" in p_res
and "别自己另找" in p_res)
# ③ prompt 里带**不可核实件清单**(机制侧渲染的,⛔ 空也要有那句)
has_list = ("不可核实" in p_res or "产物读不到" in p_res)
# ④ `--report done` 的**存在性硬闸**在(文本判据,行为另有独立验证脚本)
has_hard = ("artifact_state(tid, {\"state\": \"done\", \"artifact\": artifact})"
in _code)
# ⑤ 落点是**软**判据(⛔ 不许写成 return 3 ⇒ 那会误杀既有工作流)
_soft = "⚠️ 提醒:`--artifact`" in _code and "log(\"artifact 落点不合合同(不拒收)" in _code
# ⑥ 行为级:临时区里真跑三种形态(照 P0-42:隔离必须用 COLLABD_CONFIG)
_tmp = _pl.Path(_tf.mkdtemp(prefix="dsh-art-"))
_ok = _miss = _gone = _dirout = None
_unv = []
try:
# ⚠️ 必须**连 TASKS 常量一起换** —— `artifacts_unverifiable()` 读的是 `TASKS`,
# ⛔ 只换 `WS`/`INBOX` 它照样去读生产台账(我第一版就是这么写错的 ⇒ 恒得 [])。
_o = (m.WS, m.INBOX, m.TASKS)
_inbox = _tmp / "tmp" / "supervise-inbox"
_inbox.mkdir(parents=True, exist_ok=True)
(_inbox / "goal.json").write_text(
_j.dumps({"title": "合同夹具目标", "short": "合同夹具"}, ensure_ascii=False),
encoding="utf-8")
m.WS, m.INBOX, m.TASKS = str(_tmp), _inbox, _inbox / "tasks.json"
_gd = m.goal_dir_rel()
(_tmp / _gd).mkdir(parents=True, exist_ok=True)
(_tmp / _gd / "S1_真.md").write_text("x", encoding="utf-8")
_ok = m.artifact_state("a", {"state": "done", "artifact": _gd + "/S1_真.md"})["state"]
_miss = m.artifact_state("b", {"state": "done"})["state"]
_gone = m.artifact_state("c", {"state": "done", "artifact": _gd + "/S2_无.md"})["state"]
_dirout = m.artifact_dir_ok("交付物/别处.md")["ok"]
# 落盘 tasks.json(`artifacts_unverifiable()` 读盘,⛔ 内存 dict 不算)
m.TASKS.write_text(_j.dumps({
"a": {"state": "done", "artifact": _gd + "/S1_真.md"},
"b": {"state": "done"},
"c": {"state": "done", "artifact": _gd + "/S2_无.md"},
"d": {"state": "running"},
}, ensure_ascii=False), encoding="utf-8")
_unv = [x["id"] for x in m.artifacts_unverifiable()]
except Exception as _e:
_unv = ["<跑挂:%s>" % _e]
finally:
try:
m.WS, m.INBOX, m.TASKS = _o
except Exception:
pass
try:
import shutil as _sh
_sh.rmtree(_tmp, ignore_errors=True)
except Exception:
pass
return [
("🔴 三个函数都在(`artifact_state`/`artifacts_unverifiable`/`artifact_dir_ok`)", has_fn),
("🔴 结果检查 prompt 写明 **文档合同**(目标目录 + 落点 + ⛔ 别自己另找)", has_contract),
("🔴 prompt 带**不可核实件清单**(机制侧渲染,⛔ 不让会话自己扫)", has_list),
("🔴 `--report done` 有**存在性硬闸**(`artifact_state()` 真验)", has_hard),
("🔴 落点是**软**判据(⛔ 不 return 3 —— 硬拒会误杀既有工作流)", _soft),
("🔴 行为:真产物 → `ok`", _ok == "ok"),
("🔴 行为:没写 artifact → `missing`", _miss == "missing"),
("🔴 行为:路径不存在 → `gone`", _gone == "gone"),
("🔴 行为:落点判据(写到别处 ⇒ False)", _dirout is False),
("🔴 行为:`artifacts_unverifiable()` 只挑 done 且不可核实(=%s)" % _unv,
_unv == ["b", "c"]),
]
@case("目标文件夹机制:按目标算目录 + 文档同源 + 派棒产物落点 + CLI 幂等")
def t_goal_dir_mechanism():
"""🔴 2026-10-03 11:0x(用户要求逐字):
> 「把这个机制**写到技能中**,这样**每次使用协作会话机制都可以有对应的目标文件夹**」
🔴 **原痛点(实测)**:产物散在 `交付物/`、`docs/`、工作区根目录
⇒ 检查会话判断"做完没"只能**到处翻**(用户报障:触发宿主「重复执行、要求人工确认」)。
✅ **归拢文档、不搬家状态** —— 真源永远是 `goal.json`(生命周期/验收判据)
+ `tasks.json`(队列四态)⇒ 目标目录里**只放文档**(两处存状态=迟早打架)。
⚠️ 判据一律**现算**,⛔ 不断言字面目录名(目标标题会变)。
"""
import json as _j # 🔴 用到 goal.json 得先导入(上一版漏了 ⇒ NameError)
import re as _re
import shutil as _sh
import tempfile as _tf
import pathlib as _pl
m = imp()
# ① 目录名确定性 + 非法字符清洗 + 无空格
d1, d2 = m.goal_dir_name("目标甲", "简称甲"), m.goal_dir_name("目标甲", "简称甲")
d3 = m.goal_dir_name("目标乙", "简称乙")
clean = m.goal_dir_name(r'a/b:c*d?e|f<g>h|i\j', "s/t")
# ② 目录名与文档路径**同源**(⛔ 不会出现"目录在一处、文档在另一处")
doc = m.exec_doc_rel()
same_src = doc.startswith(m.goal_dir_rel() + "/")
# ③ prompt:两份都带目标目录 + 产物落点约束
p_res = m._check_prompt(1, "目标", "sessions-ended")
p_goal = m._check_prompt(1, "目标", "queue-empty")
gd = m.goal_dir_rel()
# ④ CLI 幂等:跑两次,第二次不许覆盖已有文档
tmpws = _pl.Path(_tf.mkdtemp(prefix="dsh-goaldir-"))
ok_cli = None
try:
_o = (m.WS, m.INBOX)
m.WS = str(tmpws)
(tmpws / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True)
(tmpws / "tmp" / "supervise-inbox" / "goal.json").write_text(
_j.dumps({"title": "自检目标", "short": "自检"}), encoding="utf-8")
m.INBOX = tmpws / "tmp" / "supervise-inbox"
r1 = m._ensure_goal_dir() if hasattr(m, "_ensure_goal_dir") else None
_dd = tmpws / m.goal_dir_rel()
_f = _dd / m.EXEC_DOC_NAME
had = _f.exists()
if had:
_f.write_text("SENTINEL", encoding="utf-8")
r2 = m._ensure_goal_dir() if hasattr(m, "_ensure_goal_dir") else None
ok_cli = bool(_f.exists() and _f.read_text(encoding="utf-8") == "SENTINEL")
finally:
m.WS, m.INBOX = _o
_sh.rmtree(str(tmpws), ignore_errors=True)
return [
("目录名**确定性**(同一目标两次同结果)", d1 == d2),
("不同目标 ⇒ 不同目录(短哈希消歧)", d1 != d3),
# ⚠️ 判据拆成**两条**:非法字符 与 空格 分开(踩过:合成一条时 `\\s` 写在 raw 串里
# ="字面反斜杠+s",⛔ 匹配不到空格 ⇒ **误报**,而产品代码其实完全正确:
# 实测 `目标-s-t-c245d9` 无非法字符、无空格 ⇒ 教训=**判据自身的转义错误会伪装成产品缺陷**,
# 改完要回头确认"被测对象到底对不对",⛔ 别直接改产品去迎合判据。
# 🔴🔴 2026-10-05 **同一课第二遍**:加了 `执行会话/` 父层后,`clean` 里**必然含一个 `/`**
# ⇒ 老判据 `not re.search(r"[\\/:*?\"<>|]", clean)` **当场误报**(同一族第 2 次:
# 判据自身的假设过期 ⇒ 伪装成产品缺陷)。
# ✅ 正解=**只清洗"末级目录名"**再判 —— 父层分隔符是**结构**,⛔ 不是"未清洗的非法字符"。
("目录名已清洗非法字符(只看末级:实测 %r)" % clean.split("/")[-1],
not _re.search(r"[\\/:*?\"<>|]", clean.split("/")[-1])),
("目录名无空格(命令行不必处处加引号)", " " not in clean),
("目录名无空格(命令行不必处处加引号)", " " not in clean),
("🔴 文档路径与目录**同源**(%s)" % doc, same_src),
# 🔴🔴 2026-10-05 新增(用户定案逐字:「<工作区根>/执行会话/目标-xxx-xxxxxx/ 改成这样」)
# ⇒ 钉住"目标文件夹收在 `执行会话/` 一层下",⛔ 别让它悄悄退回工作区根。
# ⚠️ 判据写**两级**一起:① 前缀是 `执行会话/` ② 末尾一段仍以 `目标-` 开头
# —— ⛔ 只判①会把「执行会话/随便什么名」也放行;只判②则管不住这一层。
# 🔴🔴 **变异对照抓到的自欺**(2026-10-05 实测,第 3 次同族):
# 第一版写的是 `d1.startswith(m._GOAL_DIR_PARENT + "/")` —— 拿**被测的同一个常量**
# 当下界。把 `_GOAL_DIR_PARENT` 变异成 `""` 后,判据变成 `startswith("/")`
# ⇒ **照样为真** ⇒ 拆掉父层竟然全绿(PASS 95/FAIL 2,与正确实现**逐字相同**)。
# ✅ 正解=**写死期望字面 `"执行会话/"`**:判据必须独立于被测对象,
# 否则"判据与实现同源 ⇒ 一起错 ⇒ 恒绿"(同族:P0-20/P0-13「判据写死期望值」的反面)。
("🔴 目标文件夹在 `执行会话/` 一层下(实测 %r)" % d1,
d1.startswith("执行会话/") and d1.split("/")[-1].startswith("目标-")),
("🔴 `goal_dir_rel()` 也同形(实测 %r)" % m.goal_dir_rel(),
m.goal_dir_rel().startswith("执行会话/")),
("🔴 文档路径跟到新层(`exec_doc_rel()`=%s)" % doc,
doc.startswith("执行会话/")),
("⛔ 目录名**不再落在工作区根**(回归护栏:实测 %r)" % d1, "/" in d1),
("🔴 两份 prompt 都带目标目录(%s)" % gd, gd in p_res and gd in p_goal),
("🔴 prompt 要求产物落目标目录 + 禁散到 交付物/docs",
("产物一律落这里" in p_res) and ("别再散到" in p_goal)),
("🔴 prompt 给了幂等建目录命令", "--ensure-goal-dir" in p_res),
("🔴 CLI **幂等**(第二次跑⛔ 不覆盖已有文档)", bool(ok_cli)),
]
@case("常驻程序在线判据:读**常驻心跳**(⛔ 不是退役旧戳)+ 四态文案 + pid 判据复用")
def t_prog_online_judge():
"""🔴 2026-10-03 11:35x(用户报障逐字):
> 「看板协作程序 命名创建了检查程序,就算执行了一轮 **在线 · 已 849.6 分钟没轮**」
🔴 **① 判据读错文件**(真因):`prog.up` 读的是 `_tick.stamp`/`collabd-once.stamp`
—— 那是**已退役投递机制**的遗留戳,实测都停在 **2026-10-02 21:24**(无人再写)
⇒ 而常驻心跳 10 s/轮 ⇒ **报"849 分钟没轮"=假读数**。
🔴 **② 文案把"死了"说成"没被唤起"** ⇒ 读者以为机制正常只是闲着 ⇒ 假绿。
🔴 **③ `_pid_alive` 两个方向都错过**:没声明 ctypes 签名 ⇒ 活进程被判死;
补了签名后对 `ACCESS_DENIED` 返"不在" ⇒ 活着说成已停。
"""
import importlib.util as _il
import json as _j
import time as _tm
m = imp()
spec = _il.spec_from_file_location("bd_hb", HERE / "board.py")
b = _il.module_from_spec(spec)
spec.loader.exec_module(b)
prog = b._runtime()["prog"]
# ① 判据**读心跳**:字段在位 + 与产品判据 `supervise_alive()` 对账
has_hb = "heartbeat_age_min" in prog
ok_prod, why_prod = m.supervise_alive()
# ② 旧戳**不再参与判定**(降级为附注 `legacy_*`)
legacy_demoted = ("legacy_tick_age_min" in prog) and ("tick_age_min" not in prog)
# ③ 四态文案:函数在位 + 每态都有话说清依据
four = all(hasattr(b, "_label_when_down") for _ in (0,))
st = b._label_when_down(10.0, None, True)
alive_old = b._label_when_down(10.0, 4, False) # pid 4 = 系统 idle,恒在
dead = b._label_when_down(10.0, 999999, False)
unknown = b._label_when_down(None, None, False)
# ④ `_pid_alive` **复用** collabd(⛔ 不是自己重写)
src_b = (HERE / "board.py").read_text(encoding="utf-8")
reuses = ("_cb_shared._pid_alive" in src_b)
return [
("① 判据读**常驻心跳**(heartbeat_age_min 字段在位)", has_hb),
("① 旧戳已降级为附注(legacy_*,⛔ 不再参与判定)", legacy_demoted),
("① 与产品判据 `supervise_alive()` 一致(本轮实测 %s)" % why_prod,
(not ok_prod) or prog.get("up") is True),
("② 四态文案函数在位", four),
("② 心跳读不到 ⇒ 说「判据已降级」⛔ 不提进程(实测 %r)" % st,
"降级" in st and "进程" not in st),
("② pid 还在但心跳旧 ⇒ 说「没轮(进程还在)」(实测 %r)" % alive_old,
"没轮" in alive_old and "进程还在" in alive_old),
("② pid 不在 ⇒ 说「已停 · 进程不在」(实测 %r)" % dead, "已停" in dead),
("② 什么都读不到 ⇒ 说「状态未知」(实测 %r)" % unknown, "未知" in unknown),
("③ `_pid_alive` **复用 collabd.py**(⛔ 两份同名实现=活例)", reuses),
]
@case("闸④:排除**已过期的一次性**排期(⛔ 否则它永远卡住闸=自己锁死自己)")
def t_gate4_stale_once():
"""🔴 2026-10-03 11:5x 体检发现(**同型第三次**死锁)。
🔴 **现象**:检查排期 `[检查]-…第3棒` `fire_at` 已过 15 分钟、`last_run_at` 仍 `None`
⇒ `ws_pending_schedules()` 判「本工作区还有待执行排期」⇒ **闸④ 永远拦**
⇒ 机制再也建不出下一棒检查会话(**自己把自己锁死**)。
前两次同型:改名前的旧名排期、「清理残留排期」那条排期把自己困在闸④。
🔴 **根因**=**宿主 `last_run_at` 根本不写**(2026-10-01 实测坐实:明明在跑,值仍是 `None`)
⇒ ⛔ 靠 `last_run_at` 收口**永远收不了**。
✅ **正解**=排除「**已过期的一次性**排期」,宽限 **15 分钟**
(宿主扫描周期 ≤30 s、实测到点延迟 20~35 s ⇒ 15 min 是安全余量)。
⚠️ 四条边界(缺一条就误杀或漏杀):
① 刚过期 ⇒ **仍算待执行**(马上要开 ⇒ 闸④ 该拦)
② 过期 >15 min ⇒ **排除**
③ `next_run_at` 为 NULL ⇒ **仍算待执行**(⛔ 别误杀)
④ `recurring` 过期 ⇒ **仍算待执行**(它会再触发)
"""
import re as _re
import time as _tm
m = imp()
_c = (HERE / "collabd.py").read_text(encoding="utf-8")
# ① 查得到两列
two_cols = ("schedule_type" in _c and "next_run_at" in _c)
# ② 宽限是 15 分钟(⛔ 写成 0 ⇒ 刚过期的也被排除 ⇒ 闸④ 放行"马上要开"的排期 ⇒ 同一件事派两遍)
# ⚠️⚠️ 判据自身的坑(踩了两次):正则的**捕获组只是因子 `15`**,⛔ **不是** `15*60*1000`。
# 我曾拿它当毫秒去比 `900000 <= x` ⇒ 永远 False ⇒ **误判产品坏了**。
# ⇒ 判据直接按**分钟**比:`limit_min` 就是那个因子本身。
mlim = _re.search(r"_STALE_MS\s*=\s*(\d+)\s*\*\s*60\s*\*\s*1000", _c)
limit_min = int(mlim.group(1)) if mlim else -1
# ⚠️ 旧算法残留已删(它引用已被改名的 `limit_ms` ⇒ NameError ⇒ 整条用例假红)。
# ⛔ 教训:改判据里的变量名,⛔ 一定要 grep 一遍**所有**引用点(同 P0-3 的"改一处漏一处")。
# ③ 只排 `once`(⛔ recurring 永远不该被"过期排除")
only_once = 'str(_st or "").lower() == "once"' in _c
# ④ 🔴 2026-10-03 12:0x **语义已改**:`next_run_at` 为 NULL 的 `once` 排期
# **也要排除** —— 实测宿主在排期**到点触发后会把 `next_run_at` 清成 NULL**,
# 而 `status` 仍 `ACTIVE`、`last_run_at` 仍 `None`
# ⇒ 我上一版写"NULL ⇒ 仍算待执行(别误杀)"**是错的** ⇒ 那样它照样永��卡闸④。
# (第五种情形:(a) 已过宽限 / (b) 已被消费 ⇒ 两者都排除。)
null_excluded = "if str(_st or \"\").lower() == \"once\":" in _c and "_gone = (not _na)" in _c
# ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产)
live = m.ws_pending_schedules()
return [
("① 闸④ 查询**取到** `schedule_type` 与 `next_run_at` 两列", two_cols),
("② 过期宽限 = 15 分钟(实测 %s 分钟;⛔ 写 0 会放行『马上要开』的排期)"
% 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),
]
@case("看板 tab:**跨工作区**并列查看(⛔ 原本只认一个 INBOX,天生看不见别的目录)")
def t_tab_peer_workspace():
"""🔴 2026-10-03 15:44x(用户报障逐字):
> 「为什么 会话协作看板 tab 选项不能切换看另外两个工作区的目标」
🔴 **根因**:`goal_files()` 扫的是 `INBOX/goal.json` + `INBOX/goals/*.json`
—— **两者都在同一个 INBOX 下**,而 `INBOX` 由部署配置的 `workspace` 决定
⇒ 一个 `board.py --serve` 实例**天生只看见一个工作区**(注释里也明写"同一个工作区")。
✅ 修法:配置新增 `peer_workspaces`(要并列查看的其它工作区)⇒ 读它们的 `goal.json` 进 tab。
🔴 **严格只读**这条边界必须钉住:⛔ 不写对方文件、⛔ 不起对方进程、⛔ 不改对方状态。
🔴 2026-10-04:本条要**现读生产配置**的 `peer_workspaces` ⇒ 在**技能副本**里跑,
那个工作区**没启用机制**(`vibe-product` 实测)⇒ peer 必为空 ⇒ 0 格 ⇒ 报红。
⇒ 未启用时**跳过**(⛔ 不许改成"永远绿")。
"""
if not MECH_ON:
return [("⏭ 本工作区**未启用**会话机制 ⇒ 跨区 peer 配置无从谈起,本条不适用", True)]
import inspect as _ins
import re as _re
m = imp()
spec = _ins.getsource # 占位(下面直接读文本)
src_b = (HERE / "board.py").read_text(encoding="utf-8")
src_h = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
# ① 函数在位
has_fn = "_peer_workspaces" in src_b
# ② 排除本工作区(⛔ 否则活跃目标出现两格)
excludes_self = 's == str(WS or "").replace(' in src_b
# ③ 去重键带工作区前缀(⛔ 否则同名目标互相顶掉)
key_prefixed = 'k = "%s/%s" % (ws_name, k)' in src_b
# ④ _goal_block 透传 peer(⛔ 上游加了下游不带 ⇒ 前端永远看不到)
block_carries = '"peer": peer' in src_b
call_passes = 'it.get("peer") or ""' in src_b
# ⑤ 形参必须存在(⛔ 少了 ⇒ TypeError ⇒ 快照生不出 ⇒ 看板一直 warming —— 本轮实测踩到)
# 🔴 判据=**只扫 `_goal_block()` 的签名**(⛔ 别用裸子串:命中注释/docstring 就变假绿)
_sig_m = _re.search(r"def\s+_goal_block\s*\(([^)]*)\)", src_b, _re.S)
_sig = " ".join((_sig_m.group(1) if _sig_m else "").split())
sig_ok = ("peer" in _sig and "ws_root" in _sig)
sig_txt = "_goal_block(%s)" % (_sig[:60] or "!! 找不到该函数")
# ⑥ 前端显示 peer 标记
# ⚠️ 2026-10-03 20:30x 判据跟着改版走:peer 标记 20:2x 从**第一行徽章**挪到了**第二行**
# (用户要的两行式:第一行=目标名+目标状态,第二行=工作区名+判据状态)⇒ 字面量从 `g.peer?` 变 `g.peer||`。
# 🔴 **剥掉注释再查**(20:2x 踩过:注释里留着旧写法 ⇒ 判据被注释骗、变异不报红)。
_h_nc = re.sub(r"/\*.*?\*/", "", src_h, flags=re.S)
_h_nc = re.sub(r"//[^\n]*", "", _h_nc)
front_shows = ("g.peer||" in _h_nc) or ("g.peer?" in _h_nc)
# ⑦ 只读取向:源码里 ⛔ 不能有写对方文件的调用
no_write = not _re.search(r"_root\s*/\s*\"tmp\".*\.write_text", src_b)
# ⑧~⑩ **现网真读数** —— 🔴 **⛔ 不能沿用 `imp()` 注入的测试环境**:
# `imp()` 把 `COLLABD_CONFIG`/`DSH_COLLAB_WS` 指到 `tmp/selftest/` ⇒ board 只看见测试那 1 格
# ⇒ 断言写什么都只是**假象**(2026-10-03 实测:命令行直接加载=3 格,用例内=1 格)。
# ⇒ 正确姿势:**真磁盘上造两个工作区** + **干净环境**加载 board 现读(P0-13:判据要能证伪)。
import tempfile
def _load_board(cfg_p, ws_env, tag):
"""在**指定配置**下加载 `board.py`(⛔ 用完还原 env,防污染后续用例)。"""
_old = {k: os.environ.get(k) for k in ("COLLABD_CONFIG", "DSH_COLLAB_WS")}
try:
os.environ["COLLABD_CONFIG"] = str(cfg_p)
if ws_env is None:
os.environ.pop("DSH_COLLAB_WS", None)
else:
os.environ["DSH_COLLAB_WS"] = str(ws_env)
_u = __import__("importlib.util", fromlist=["util"])
_sp = _u.spec_from_file_location(tag, HERE / "board.py")
_mm = _u.module_from_spec(_sp)
_sp.loader.exec_module(_mm)
return _mm
finally:
for _k, _v in _old.items():
if _v is None:
os.environ.pop(_k, None)
else:
os.environ[_k] = _v
def _dump(p, obj):
Path(p).write_text(json.dumps(obj, ensure_ascii=False), encoding="utf-8")
def _mk_ws(root, gid, title):
"""真磁盘上造一个**最小工作区**(只含 `tmp/supervise-inbox/goal.json`)。"""
_r = Path(root) / gid
(_r / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True)
_dump(_r / "tmp" / "supervise-inbox" / "goal.json",
{"id": gid, "title": title, "short": gid})
return _r
_td = tempfile.mkdtemp(prefix="selftest-peer-") # ⛔ 不用 TEST_WS(会被别的用例清掉)
try:
_a = _mk_ws(_td, "wsA", "A 区目标")
_b = _mk_ws(_td, "wsB", "B 区目标")
# 🔴🔴 2026-10-04 21:4x **夹具必须补上「这个 peer 还在会话列表里」这个前提**。
# 病根(我当天加的过滤打红了本条):`board.goal_files()` 现在会**跳过"未删会话为 0"的 peer**
# (用户口径:「tab 要把已经不再 会话列表的工作区 目标移除」)。
# 而 `_mk_ws()` 只在真磁盘造 `goal.json`,**宿主库里没有 wsB 的任何会话**
# ⇒ `_peer_session_rows()` 返回空 ⇒ wsB **被正确地过滤掉** ⇒ `goal_files()`=1 格 ⇒ 本用例 FAIL。
# ⚠️ **被测代码行为是对的,是夹具在说谎**(= P0-53 同一个形状:合成区必须让被测对象
# 能读到真实判据,⛔ 不许靠改断言期望值把它"修绿")。
# ⇒ 正解两条(都必须):
# ① 造一个**最小的宿主库**(只有 `sessions` 表 + 本用例要用的列),
# wsB 那条 `cwd` 精确等于 `_b` ⇒ `_peer_session_rows(_b)` 有 1 条 ⇒ 不被过滤。
# ② 配置里用 **`host_db`** 指过去 —— `board.py::_host_db()` 优先读它
# (⛔ 别去改 `CODEBUDDY_CONFIG_DIR`:那会把真库暴露给用例,且污染后续用例)。
# ⚠️ 库要**留在 `_td` 里**(随 `shutil.rmtree` 一起回收),⛔ 不落靠 `tempfile` 的全局清理。
_db = Path(_td) / "fake-host.db" # ⚠️ `mkdtemp()` 返回的是 **str** ⇒ 必须包 `Path()`
try:
import sqlite3 as _sq
_cn = _sq.connect(str(_db))
_cn.execute("create table sessions (id text primary key, title text, custom_title text, "
"status text, cwd text, updated_at integer, last_activity_at integer, "
"deleted_at integer)")
_cn.execute("insert into sessions values (?,?,?,?,?,?,?,?)",
("sid-wsB-1", "B 区主会话", "", "working",
str(_b).replace("\\", "/"), int(time.time() * 1000),
int(time.time() * 1000), None))
_cn.commit()
_cn.close()
except Exception as _e: # pragma: no cover
pass # 造库失败 ⇒ 走下面 live_ok 的如实报错,⛔ 不假装通过
_c_on = _a / "tmp" / "peer.on.json"
_dump(_c_on, {"workspace": str(_a).replace("\\", "/"),
"inbox": "tmp/supervise-inbox",
"host_db": str(_db).replace("\\", "/"),
"peer_workspaces": [str(_b).replace("\\", "/")]})
gs1 = _load_board(_c_on, _a, "bd_peer_on").goal_files()
p1 = [g.get("peer") for g in gs1 if g.get("peer")]
a1 = [g for g in gs1 if g.get("active")]
live_ok = (len(gs1) == 2 and p1 == ["wsB"] and len(a1) == 1)
live_txt = "登记 1 个 peer ⇒ goal_files()=%d 格(peer=%s,active %d 个)" % (
len(gs1), p1, len(a1))
# ⑨ **变异对照**(🔴 判据必须可证伪:去掉 `peer_workspaces` ⇒ 必须回落到 1 格)
_c_off = _a / "tmp" / "peer.off.json"
_dump(_c_off, {"workspace": str(_a).replace("\\", "/"),
"inbox": "tmp/supervise-inbox"})
gs0 = _load_board(_c_off, _a, "bd_peer_off").goal_files()
p0 = [g.get("peer") for g in gs0 if g.get("peer")]
mut_ok = (len(gs0) == 1 and not p0)
mut_txt = "去掉 peer_workspaces ⇒ %d 格(peer %d 个)" % (len(gs0), len(p0))
# ⑪ 🔴 **变异对照 ②:把"该 peer 的会话删光" ⇒ 它必须从 tab 掉出去**
# (守 2026-10-04 用户口径「tab 要把已经不再 会话列表的工作区 目标移除」)
# ⚠️ 这一条是本轮新加的过滤的**证伪项**:⛔ 没有它,那个过滤写错了也没人发现
# (同族:判据必须能证伪 ⇒ 打掉接线必须报红)。
_db2 = Path(_td) / "fake-host2.db" # ⚠️ 同上,必须包 `Path()`
try:
import sqlite3 as _sq2
_cn2 = _sq2.connect(str(_db2))
_cn2.execute("create table sessions (id text primary key, title text, custom_title text, "
"status text, cwd text, updated_at integer, last_activity_at integer, "
"deleted_at integer)")
# ⚠️ **只有已删会话**(`deleted_at` 非空)⇒ 等价于"用户把会话全删了"
_cn2.execute("insert into sessions values (?,?,?,?,?,?,?,?)",
("sid-wsB-del", "B 区(已删)", "", "completed",
str(_b).replace("\\", "/"), int(time.time() * 1000),
int(time.time() * 1000), int(time.time() * 1000)))
_cn2.commit()
_cn2.close()
except Exception: # pragma: no cover
pass
_c_del = _a / "tmp" / "peer.del.json"
_dump(_c_del, {"workspace": str(_a).replace("\\", "/"),
"inbox": "tmp/supervise-inbox",
"host_db": str(_db2).replace("\\", "/"),
"peer_workspaces": [str(_b).replace("\\", "/")]})
gs2 = _load_board(_c_del, _a, "bd_peer_del").goal_files()
p2 = [g.get("peer") for g in gs2 if g.get("peer")]
gone_ok = (len(gs2) == 1 and not p2)
gone_txt = "peer 会话全删 ⇒ %d 格(peer %s)" % (len(gs2), p2)
finally:
shutil.rmtree(_td, ignore_errors=True)
# ⑩ 生产侧:⛔ **不硬编码路径** —— 指不到真工作区 ⇒ SKIP(⛔ 不假装通过)
# 🔴🔴 2026-10-05 修一处**恒红判据**(实测 `PASS 96/FAIL 1` → `95/2`,多出的正是本条的 FAIL):
# 老写法 `prod_ok = (_n >= 1 and len(pp) == _n and len(ap) == 1)` 要求
# **登记几个 peer 就必须出几格**。可是 peer 工作区**没建目标时本来就不该出格** ——
# 同一组判据的第 ⑪ 条(`gone_ok`)写的正是这个语义("peer 已不在会话列表 ⇒ 不占 tab"),
# **两条自相矛盾**:⑩ 要 `len(pp)==_n`,⑪ 又认可少出格。
# ⇒ 实测现场:ai1net 登记了 2 个 peer(`vibe-product` ✅ 有 `goal.json` /
# `agent-product` ❌ 还没建目标)⇒ `_n=2` 但 `len(pp)=1` ⇒ **必红**。
# ⚠️ 这是"恒红比漏网更坏"的又一实例:只要有任何 peer 区没活跃目标,基线就永远红
# ⇒ 之后跑任何变异都"看着有红",验证作废。按纪律③「基线先全绿」必须先修它。
# 正解=判**包含关系 + 上限**,⛔ 不是判**数量相等**:
# · 出格数 ≤ 登记数(⛔ 出格数超登记 ⇒ 有来源不明的格,真问题)
# · 每一格都必须在登记表里(⛔ 否则是把别区数据挂到本区名下)
# · 本区活跃目标恰好 1 个(⛔ 多/少都说明看板认错了 active)
# ⚠️ 放宽后**仍可证伪**:① 把某格来源改成本区 ⇒ `pp_set ⊆ reg` 失败;
# ② 篡改出格数 > 登记数 ⇒ 上限失败;③ active 改 0/2 ⇒ 失败。均有变异可抓。
_pcfg = WS / ".workbuddy" / "collab" / "collabd.config.json"
if not _pcfg.is_file():
prod_ok, prod_txt = True, "SKIP:读不到生产配置(%s)" % _pcfg
else:
try:
# ⚠️ 两侧粒度不同,⛔ 不能直接比字符串:
# · `collabd.config.json` 的 `peer_workspaces` = **全路径**(`E:/ProgramData/AIProject/vibe-product`)
# · `goal_files()` 出的 `peer` 字段 = **短名**(`vibe-product`,看板 tab 就用它做标题)
# 第一版直接 `set(pp) <= set(_reg)` ⇒ 恒 False(`vibe-product` ≠ 全路径)⇒ **判据写成了恒红**。
# 正解:把登记的全路径**取 basename** 再比。
_reg_paths = [str(x).replace("\\", "/") for x in
((json.loads(_pcfg.read_text(encoding="utf-8")) or {}).get(
"peer_workspaces") or [])]
_reg = [p.rstrip("/").rsplit("/", 1)[-1].lower() for p in _reg_paths]
_n = len(_reg)
gp = _load_board(_pcfg, None, "bd_peer_prod").goal_files()
pp = [str(g.get("peer")).rstrip("/").rsplit("/", 1)[-1].lower()
for g in gp if g.get("peer")]
ap = [g for g in gp if g.get("active")]
# 出格数 ≤ 登记数(peer 没建目标 ⇒ 少出格=正常)∧ 每格都在登记表里 ∧ active 恰 1
prod_ok = (_n >= 1 and len(pp) <= _n and set(pp) <= set(_reg) and len(ap) == 1)
_miss = [r for r in _reg if r not in set(pp)]
prod_txt = ("生产配置登记 %d 个 peer ⇒ %d 格(peer=%s,active %d)"
"|未出格(无活跃目标)=%s" % (_n, len(gp), pp, len(ap), _miss))
except Exception as e: # pragma: no cover
prod_ok, prod_txt = False, "抛异常:%s" % e
return [
("① 有 `_peer_workspaces()`(配置驱动的并列查看)", has_fn),
("② **排除本工作区**(⛔ 否则活跃目标出现两格)", excludes_self),
("③ 去重键带**工作区前缀**(⛔ 同名目标互相顶掉)", key_prefixed),
("④ `_goal_block()` **透传** peer(⛔ 上游加了下游不带=前端看不到)", block_carries),
("④ 调用点按位置传入 peer", call_passes),
("⑤ `_goal_block` 形参含 peer/ws_root(⛔ 少了 ⇒ TypeError ⇒ 看板 warming):%s" % sig_txt,
sig_ok),
("⑥ 前端 tab 显示 peer 标记(⛔ 否则三格标题一样,读者分不清)", front_shows),
("⑦ **严格只读**(⛔ 源码里不得有写对方文件的调用)", no_write),
("⑧ **现网真读数**(真磁盘两工作区+干净环境):%s" % live_txt, live_ok),
("⑨ **配置缺省 ⇒ 回落 1 格**(向后兼容;⛔ 它只守「配置可关」这一侧,抓不住代码坏 ⇒ 证伪看 ⑧):%s"
% mut_txt, mut_ok),
("⑩ 生产侧按生产配置现读:%s" % prod_txt, prod_ok),
("⑪ **peer 已不在会话列表 ⇒ 不占 tab**(用户 2026-10-04 口径;⛔ 判据是**未删会话数**,不是心跳):%s"
% gone_txt, gone_ok),
]
@case("跨工作区 peer 格:⛔ **不得吃本工作区数据**(吃了=把本区执行情况挂到别人名下=说假话)")
def t_peer_block_no_self_data():
"""🔴 2026-10-03 16:0x 用户报障,逐字:「**选择另一个工作区目标 tab 下面没有显示对应工作区目标和执行情况**」
🔴🔴 **实测真因(比"没显示"更糟 —— 看板在说假话)**:`build()` 里的 `tasks`/`srows`/`st`
**只取一次、所有格共用** ⇒ 三格的 `labor`/`sessions`/`progress` **md5 完全相同**
⇒ 切到 peer 那格,看到的其实是**本工作区**的执行情况,只是标题挂着对方工作区的名字。
✅ 修法:peer 格只喂 `_peer_tasks()`/`_peer_srows()`/`_peer_state()` —— **只读对方目录下的文件**;
读不到 ⇒ **空 + 界面如实说明**,⛔ 绝不拿本工作区的数据补位。
🔴 **判据可证伪**:把 `_peer_tasks(_wr)` 换回本区的 `tasks` ⇒ ⑤ **必然报红**(两侧 tasks 变相同)。
"""
import tempfile
src = (HERE / "board.py").read_text(encoding="utf-8")
# ① peer 分支喂的是对方数据源
feeds_peer = ("_peer_tasks(_wr)" in src and "_peer_state(_wr)" in src
and "_peer_srows(srows, _wr)" in src)
# ② 补捞(⛔ 没有 ⇒ 全库最近 50 条里捞不到对方会话 ⇒ 显示 0 条=假象)
has_backfill = "_peer_session_rows" in src
# ③ peer 格的作用域要换到对方工作区
scope_swaps = 'sc["workspace"] = ws_root' in src
# ④ 本区切分基准只按本区目标算
own_only = "if not it.get(\"peer\")" in src
# ⑤ 真读数(本区有台账 / 对方无 ⇒ peer 格必须为空,且 ⛔ 不得等于本区的)
_td = tempfile.mkdtemp(prefix="selftest-peerblk-")
live_ok, live_txt = True, "SKIP:未跑"
try:
A = Path(_td) / "wsA"
B = Path(_td) / "wsB"
for _r, _gid in ((A, "wsA"), (B, "wsB")):
(_r / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True)
(_r / "tmp" / "supervise-inbox" / "goal.json").write_text(
json.dumps({"id": _gid, "title": _gid + " 目标", "short": _gid, "topics": [_gid]},
ensure_ascii=False), encoding="utf-8")
(A / "tmp" / "supervise-inbox" / "tasks.json").write_text(
json.dumps({"S1": {"state": "done", "line": "wsA"}}, ensure_ascii=False),
encoding="utf-8")
cfg = A / "tmp" / "cfg.json"
cfg.write_text(json.dumps({"workspace": str(A).replace("\\", "/"),
"inbox": "tmp/supervise-inbox",
"peer_workspaces": [str(B).replace("\\", "/")]},
ensure_ascii=False), encoding="utf-8")
_old = {k: os.environ.get(k) for k in ("COLLABD_CONFIG", "DSH_COLLAB_WS")}
try:
os.environ["COLLABD_CONFIG"] = str(cfg)
os.environ["DSH_COLLAB_WS"] = str(A)
_u = __import__("importlib.util", fromlist=["util"])
_sp = _u.spec_from_file_location("bd_peerblk", HERE / "board.py")
bd = _u.module_from_spec(_sp)
_sp.loader.exec_module(bd)
try:
gs = (bd.build() or {}).get("goals") or []
except Exception as e: # pragma: no cover
gs, live_txt = [], "SKIP:build() 抛异常(%r)" % e
if gs:
own = [g for g in gs if not g.get("peer")]
per = [g for g in gs if g.get("peer")]
ta = (own[0].get("tasks") if own else None) or {}
tb = (per[0].get("tasks") if per else None) or {}
same = (json.dumps(ta, sort_keys=True, ensure_ascii=False)
== json.dumps(tb, sort_keys=True, ensure_ascii=False))
live_ok = bool(ta) and (not tb) and (not same)
live_txt = "本区 tasks=%d 件 / peer tasks=%d 件 / 两者相同?%s" % (
len(ta), len(tb), same)
finally:
for _k, _v in _old.items():
if _v is None:
os.environ.pop(_k, None)
else:
os.environ[_k] = _v
finally:
shutil.rmtree(_td, ignore_errors=True)
return [
("① peer 分支喂的是**对方**数据源 `_peer_tasks/_peer_srows/_peer_state`(⛔ 不是本区 tasks/srows/st)",
feeds_peer),
("② 有 `_peer_session_rows()` **补捞**(⛔ 没有 ⇒ 全库最近 50 条捞不到对方会话 ⇒ 显示 0 条=假象)",
has_backfill),
("③ peer 格把作用域换到**对方工作区**(⛔ 不换 ⇒ `_sessions()` 的 cwd 硬过滤把对方会话全排除)",
scope_swaps),
("④ 本区切分基准**只按本区目标**算(⛔ 混入 peer topics ⇒ 台账件两边都不进 ⇒ 静默丢失)", own_only),
("⑤ **真读数**(本区有台账/对方无 ⇒ peer 格必须为空且 ≠ 本区):%s" % live_txt, live_ok),
]
@case("跨工作区 tab:**有差异的字段必须逐个被视图取到**(⛔ 漏一个 ⇒ 静默沿用本区 ⇒ 几个 tab 看起来一样)")
def t_view_takes_all_scoped_fields():
"""🔴 2026-10-03 20:2x 用户报障(逐字):「**为什么不同目标,协作程序框图里面的内容是一样的**」
🔴🔴 **真因(实测,不是猜)**:服务端**数据是对的**(本区块内 `sessions_checks=4` 条、
两个 peer 块各 0 条),但前端 `scopeView()` 合并视图时**只显式取了 7 个字段**
⇒ 其余字段**原封不动沿用顶层 `d`**(=**本工作区**那份)
⇒ 切 tab 时「协作程序状态/检查状态/检查会话/完成情况」**全都显示同一个工作区的**。
🔴 **这是我今天第三次踩「加字段必须顺着数据流查到消费点」**(前两次:加了没渲染 /
判据写死字面量)⇒ **光靠"记得改"已经不管用了**。
✅ **本条判据是数据驱动的**:跑一次真 `build()` ⇒ 逐字段比较两块 ⇒
**凡"两块有差异"而"视图没取"的字段一律报红** ⇒ 以后新增字段漏了会被自动抓住。
"""
import re as _re2
_root = os.environ.get("DSH_COLLAB_WS") or os.environ.get("DSH_WS_ROOT") or ""
_pcfg = (Path(_root) / ".workbuddy" / "collab" / "collabd.config.json") if _root else None
if not _pcfg or not _pcfg.is_file():
return [("SKIP:没指到真工作区(跑的时候设 `DSH_COLLAB_WS=<工作区>` 才会真跑)", True)]
_old = {k: os.environ.get(k) for k in ("COLLABD_CONFIG", "DSH_COLLAB_WS")}
try:
os.environ["COLLABD_CONFIG"] = str(_pcfg)
os.environ.pop("DSH_COLLAB_WS", None)
_u = __import__("importlib.util", fromlist=["util"])
_sp = _u.spec_from_file_location("bd_viewchk", HERE / "board.py")
bd = _u.module_from_spec(_sp)
_sp.loader.exec_module(bd)
gs = (bd.build() or {}).get("goals") or []
if len(gs) < 2:
return [("SKIP:本工作区只登记了 1 个目标(⛔ 比不出差异)", True)]
a, b2 = gs[0], gs[1]
_IDENT = {"key", "peer", "ws_root", "active", "life", "life_at", "life_by", "peer_scope"}
# 🔴🔴 2026-10-05 12:0x 归位两个**后端排序键**(实测取证,不是为了让判据变绿而塞进豁免):
# `tab_created`=该块的 `declared_at`(排序键)、`tab_i`=排序后重编的 tab 序号
# —— 二者由 `board.py` 在 `blocks.sort(key=…)` 里**当场用掉**,
# 前端 `board.html` 对它们的**消费点命中数 = 0**(它们只用于后端排序)⇒ **不是视图字段**,
# ⛔ 要求 `scopeView()` 去取,等于要求视图去取一个它根本不渲染的东西。
# ⚠️ 反向自证:判据**仍然有牙** —— 本次真漏的 `ws_name`(渲染点 `renderProject()` L660 读
# `d.ws_name`)**不在**此表内,修它之前判据照红(见变异对照)。
_IDENT |= {"tab_created", "tab_i"}
_diff = [k for k in sorted(set(a.keys()) | set(b2.keys())) if k not in _IDENT
and json.dumps(a.get(k), sort_keys=True, ensure_ascii=False)
!= json.dumps(b2.get(k), sort_keys=True, ensure_ascii=False)]
# 🔴 提取赋值清单前**先剥掉注释** —— ⛔ 否则"把某行注释掉"这种变异**照样匹配得到**
# (本轮实测踩到:注释掉 `v.sessions_checks=` 之后判据仍报绿 ⇒ 判据被注释骗了)。
_html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
_html = _re2.sub(r"/\*.*?\*/", "", _html, flags=_re2.S)
_html = _re2.sub(r"//[^\n]*", "", _html)
_fn = _re2.search(r"function scopeView\(d,g\)\{.*?\n\}", _html, _re2.S)
_taken = set(_re2.findall(r"v\.(\w+)\s*=", _fn.group(0))) if _fn else set()
_missing = [k for k in _diff if k not in _taken]
return [("两块有差异的字段 %d 个,**视图必须逐个取到**(⛔ 漏了会静默沿用本区):%s"
% (len(_diff), ("全部已取 ✔" if not _missing else "⛔ 漏了 %s" % _missing)),
not _missing)]
except Exception as _e: # pragma: no cover
return [("用例异常:%r" % _e, False)]
finally:
for _k, _v in _old.items():
if _v is None:
os.environ.pop(_k, None)
else:
os.environ[_k] = _v
@case("每个 hook 都能真正跑起来(⛔ 不许一导入就崩)")
def t_hooks_actually_runnable():
"""🔴🔴 2026-10-03 **实测抓到的真缺陷**(用户报障「协作程序相关的连线 有用的保留 没用的
可以去掉」,我逐条去核实"这三条线还活着吗"时撞出来的):
⚠️ **事故链**:用户 10-02 报「后台一闪一闪」⇒ 为根治闪屏,给 `wb-result-hook.py` 加了
`_win_pythonw()`,写成 `PYW = _win_pythonw()`,而**函数体内部引用 `PYW`**
⇒ 调用时 `PYW` 尚未赋值 ⇒ **模块导入即 `NameError`**
⇒ 该 hook 挂的**三个事件(PreToolUse/UserPromptSubmit/SessionEnd)全部静默废掉**
⇒ 图上「钩子 → 协作程序 --once/--tick」两条线**其实早就断了,只是没人发现**
(`hook.log` 停在 10-02 21:24、三个 stamp 也停在同一分钟 —— 三处独立证据互相印证)。
🔴 **为什么这条判据必须存在**:⛔ 语法检查(`ast.parse`)**照样通过** ——
`NameError` 是**运行期**错误,语法完全合法。⇒ 「能解析」≠「能跑」,
光靠语法自检永远抓不到这类缺陷。而它造成的后果是**整条链路静默失效**,
看板上的连线会一直画着、看着一切正常。
✅ **本条判据=真的把每个 hook 起一次**(喂最小合法 stdin),断言 `rc=0` 且无 traceback。
⚠️ 起法要点:⛔ 必须用 `sys.executable`(当前解释器)而**不是** `python`(PATH 可能指向别处);
喂完**必须等它自己跑完**(hook 会 spawn 后台子进程,不能立刻判)。
"""
import subprocess as _sp2
import tempfile as _tf2
# 🔴 **只测真 hook**:目录里的 `*.bak-*` 是历史备份,⛔ 不是在跑的钩子
# (它们恰好也都能起来 ⇒ 混进来只是给输出添噪音,且会让人误以为有 12 个钩子)。
hooks = sorted(p for p in (HERE / "hooks").glob("*.py") if ".bak-" not in p.name)
out = []
for hk in hooks:
payload = json.dumps({"session_id": "selftest-probe",
"cwd": str(TEST_WS),
"hook_event_name": "UserPromptSubmit",
"prompt": "selftest probe"})
try:
r = _sp2.run([sys.executable, str(hk)], input=payload.encode("utf-8"),
capture_output=True, timeout=60, cwd=str(TEST_WS))
except Exception as exc:
out.append(("%s 起不来:%r" % (hk.name, exc), False))
continue
err = (r.stderr or b"").decode("utf-8", "replace")
# 🔴 两个判据缺一不可:退出码 0 **且** stderr 里没有 traceback
# (有的脚本出错仍返回 0,只看 rc 会漏)。
out.append(("%s 起得来(rc=%d%s)" % (
hk.name, r.returncode,
",stderr 有 traceback:%s" % err.strip().splitlines()[-1] if "Traceback" in err else ""),
r.returncode == 0 and "Traceback" not in err))
return out or [("⛔ hooks 目录里一个 hook 都没有 ⇒ 判据本身失效", False)]
@case("架构图:每条线的**两端都落在真实框上**(⛔ 不许线头悬空)")
def t_board_edges_land_on_boxes():
"""🔴🔴 2026-10-03 用户红框圈出一条线问「**这跟红框标注的线到底有没有用
有用就连对目标,没用就删除了**」—— 取证发现它**既没用不了、也没连对**:
🔴 **病根=把「与框宽有关的 x」写死**:④ 那条线写死 `x=340`,而它成立的前提是
当时 `PX=280`、宽 580。后来框**收窄到 470** 且改成**跟随画布中心** ⇒ 框变 405..875
⇒ 340 掉到框外**左侧 65px** ⇒ 箭头悬空。
🔴 **同一个病 09-30 修过一次**(当时原话「WorkBuddy 左边那根线是要连到哪里?」)
—— 当时**只改了线的位置、没把它与框宽绑死** ⇒ 框一变就复发。
⇒ 教训:**⛔ 凡是"与可变量(框宽/框位)有关"的坐标,一律现算,别写死。**
✅ **本条判据=把源码里那几个常量取出来重算一遍**,逐条断言:
① ④ 出口 `RX` **必须落在常驻程序框内**(`PX < RX < PX+PW`)—— 这条就是"连对目标";
② `RX` **必须小于 `hkX`** —— 否则会**穿过** Hook 框("连过去却穿框"同样是错);
③ `--once`/`--tick` 两个出口**必须落在 Hook 框顶边之内**;
④ 四个出口**两两不重叠**(重叠=两条线并成一条、看不出谁是谁)。
⚠️ 取常量方式:**从源码正则抽**(⛔ 不另写一份副本)—— 副本会与源码漂移,
那样这条判据就变成了"检查我自己写的数"(同族陷阱:判据必须扎在真源上)。
⚠️ 已知同源缺陷(本条**抓不到**,⛔ 别以为它是万能的):源码里若有人把 `hkX=430` 又写回
`hkX=400`,而 `RX` 仍由 `PX+24` 现算 ⇒ 两者同时满足本条 ⇒ 但**观感上 ④ 仍穿框**。
⇒ 那属于"把条件写复杂了",根治办法是**保持 `RX < hkX` 恒成立**(别靠加更多分支)。
"""
import re as _re3
src = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
# 🔴🔴 2026-10-03 **又踩一次「朴素扫描被注释骗」**(同族坑,见 P0-36):
# ④ 那段注释里**逐字写着** `RXV=PX-40`/`RXH=PX+28` ⇒ 正则先命中注释
# ⇒ 抽出来是一整段散文 ⇒ `eval` 失败 ⇒ 判据整条失效。
# ✅ **正解=先剥注释再抽**(顺序不能反):块注释 → 行注释 → 再找赋值。
_code = _re3.sub(r"/\*.*?\*/", "", src, flags=_re3.S)
_code = _re3.sub(r"(?m)^\s*//.*$", "", _code) # 只吃**整行**注释(行尾注释常跟在代码后)
_code = _re3.sub(r"//[^\n]*", "", _code)
def _n(pat, dflt=None, hay=None):
m = _re3.search(pat, hay if hay is not None else _code)
return float(m.group(1)) if m else dflt
CX = _n(r"var CX=(\d+)")
PW = _n(r"var PW=(\d+)")
# 🔴 `PX` 源码里是**表达式** `CX-PW/2`(与 PW 同一行声明)⇒ ⛔ 不能按纯数字抽。
PX = (CX - PW / 2) if (CX is not None and PW is not None) else None
hkX = _n(r"var hkX=(\d+)")
hkW = _n(r", hkW=(\d+)")
if None in (CX, PW, PX, hkX, hkW):
return [("⛔ 抽不出几何常量(CX/PW/hkX/hkW 实得 %s)⇒ 判据本身失效,⛔ 别当通过用"
% (CX, PW, hkX, hkW), False)]
# 🔴🔴 2026-10-03 **自查抓到的一个真漏洞(本条第一版栽在这)**:
# 第一版 RX 在判据里**自己重算** `PX+24` ⇒ 它检查的是「我算的数」,
# **不是「源码里那个数」** ⇒ 把源码改成 `var RX=340;`(原事故)它照样报绿。
# 🔴 这就是判据的经典自欺:**判据与被检对象各算一遍,必然有一份是假的**。
# ✅ **正解=判据只认源码里的真实写法**:抽 `var ...=` 后面到底是**表达式**还是**字面量**,
# 若是字面量**直接报红**(那正是"写死与框宽有关的坐标"这个病根本身)。
# 🔴🔴 2026-10-03 **三改=④ 变折线后本条同步**(用户原话:「**稍微向左边折一下再连过去**」
# +「**还把 hook 图框挤到旁边 多难看**」):
# ④ 不再是"一条竖直线",而是 `RXV`(框外左侧竖直段)→ `RYY`(折点)→ `RXH`(**框内**终点)。
# ⇒ **竖直段看 `RXV`、终点看 `RXH`**,两个都要查;
# ⚠️ 且新增一条**反向判据**:`hkX` 必须是**它自己的版面值**(⛔ 不许被线牵着走)——
# 上一版我为腾位置把它 400→430,用户明确指正 ⇒ 这条写进判据防复发。
res = []
def _expr(name, env=None):
# 🔴 `var RXV=…, RXH=…, RYY=…` 三个变量**在同一行逗号声明** ⇒
# ① 只有**第一个**变量带 `var`,后两个没有 ⇒ 正则要写成「`var NAME=`」**或**「裸 `NAME=`」
# (⚠️ 只写 `var %s=` 会漏掉 `RXH`/`RYY`,实测踩到);
# ② 取值必须止于**下一个逗号或分号**,⛔ 否则 `var RXV=PX-40, RXH=…` 会把后面一起吞进来。
m = _re3.search(r"\b(?:var\s+)?%s=([^,;]+)" % _re3.escape(name), _code)
if not m:
res.append(("⛔ 源码里抽不到 `var %s=` ⇒ 判据失效,⛔ 别当通过用" % name, False))
return None, None
e = m.group(1).strip()
lit = _re3.fullmatch(r"(\d+(?:\.\d+)?)", e)
if lit:
res.append(("⛔ 源码 `var %s=%s` 是**写死字面量**(病根:框一改它就落到框外)"
" ⇒ 必须由 PX 现算" % (name, e), False))
try:
# 🔴 源码里 `R4.y` / `R4.h` 是**属性访问**写法 ⇒ eval 环境不能只给 dict,
# 否则 `R4.y+(R4.h/2)` 会 `AttributeError`(实测踩到 ⇒ 整条判据失效)。
# ✅ 两种都支持:dict 下标(`R4["y"]`)与属性(`R4.y`)——用一个既支持点又支持下标的类型。
class _Box(dict):
__getattr__ = dict.__getitem__
v = float(eval(e, {"__builtins__": {}}, # noqa: S307
{"PX": PX, "PW": PW, "CX": CX,
"R4": _Box({"y": R4Y, "h": R4H})}))
except Exception:
res.append(("⛔ `var %s=%s` 求值不了 ⇒ 判据失效,⛔ 别当通过用" % (name, e), False))
return None, e
return v, e
R4Y, R4H = _n(r"R4=\{y:(\d+)"), _n(r"h:(\d+)\}, R5")
if R4Y is None or R4H is None:
return res + [("⛔ 抽不到常驻程序行几何 `R4={y:...,h:...}` ⇒ 判据失效", False)]
RXV, _ = _expr("RXV")
RXH, _ = _expr("RXH")
RYY, _ = _expr("RYY")
once_x = hkX + 40
tick_x = PX + PW / 2 + 120
if RXV is None or RXH is None or RYY is None:
return res
res += [
# 🔴🔴 2026-10-03 **判据跟着改口径**(用户原话:「**线连到左边不行 非的连到下边
# 是咋想的**」):④ 终点现在**落在框的左边框上**(`RXH==PX`),
# ⛔ 不再是"框内某点"(旧判据 `PX<RXH<PX+PW` 已不适用 ⇒ 会被误判为红)。
# ✅ 正确口径=**终点必须落在"某个边框上"**(左边/下边/上边/右边皆可),
# 唯独⛔ 不能"绕到左边却从下边进"(那是**无意义的折**)。
("④ 折线**终点 RXH=%.0f 落在框边框上**(左边框 x=%.0f)——⛔ 不是绕左却从下边进"
% (RXH, PX), abs(RXH - PX) < 0.5),
# ✅ 折点 y 必须是**框的垂直中点** ⇒ 两段折线(竖 + 横)一眼看懂。
("④ 折点 RYY=%.0f 落在框的垂直中点 %.0f(⛔ 不贴边、⛔ 不写死)"
% (RYY, R4Y + R4H / 2), abs(RYY - (R4Y + R4H / 2)) < 0.5),
("④ **竖直段 RXV=%.0f 在 Hook 框左边 %.0f 之外**(⛔ 不穿 Hook 框)" % (RXV, hkX),
RXV < hkX),
# 🔴 新增:**⛔ 不许有多余折段**(每段都得有理由;绕左边再从下边进=白绕)。
("④ 折线**只有两段**(⛔ 绕左边却从下边钻进来=那几段横移毫无意义)",
src.count("L'+RXH+' '+(R4.y+R4.h)") == 0),
# 🔴🔴 新增**反向判据**(防"为凑一条线去挪别人的框"):hkX 必须仍是 400。
# 上一版我把它 400→430 来腾位置,用户明确指正「把 hook 图框挤到旁边 多难看」。
("Hook 框左边界 hkX=%.0f **未被连线牵动**(⛔ 框的位置是它自己的版面决策,"
"不许为凑一条线挪框)" % hkX, hkX == 400),
("--once 出口 %.0f 落在 Hook 框顶边 %.0f..%.0f 内" % (once_x, hkX, hkX + hkW),
hkX <= once_x <= hkX + hkW),
# 🔴🔴 2026-10-05:`--tick` 已删 ⇒ 图上那条出口改标 **`--ensure`**(钩子唯一剩余链接
# =顺手续命)。⛔ 坐标不动、只换标签 ⇒ 本条判据**原样有效**(仍在验"线头在框内")。
("--ensure 出口 %.0f 落在 Hook 框顶边 %.0f..%.0f 内" % (tick_x, hkX, hkX + hkW),
hkX <= tick_x <= hkX + hkW),
]
# ④ 两两不重叠:⛔ 重叠=两条线并一条,读者分不出谁是谁(P0-25 同族)
outs = [("④", RXH), ("--once", once_x), ("--ensure", tick_x)]
for i in range(len(outs)):
for j in range(i + 1, len(outs)):
gap = abs(outs[i][1] - outs[j][1])
res.append(("%s 与 %s 间隔 %.0fpx(⛔ 不许并成一条)" % (outs[i][0], outs[j][0], gap),
gap >= 30))
return res
@case("目标完成 ⇒ 真的收工(⛔ 不许只改状态不关进程);阻碍 ⇒ 不关")
def t_goal_done_stops_supervise():
"""🔴🔴 2026-10-03 **接上用户口径第①条**(逐字):
「**目标完成后需要把目标状态改为 完成,关闭后台任务 和 协作程序
(有新目标或之前的目标有调整 在打开)**」;
第②条:「**目标 遇到阻碍** …(**不用关闭后台任务和协作程序**)」。
🔴 **改前取证**:`goal_life_set()` 原来**只写 `goal.json` 四个字段就 `return`**,
全文无 `stop`/`kill` 调用 ⇒ "完成即收工"**在代码里根本没实现**。
✅ 本条判据查三件事(**源码级**,⛔ 不靠"跑一遍看看"):
① `supervise_stop()` **存在**(有停的手段);
② `goal_life_set()` 在 `life == GOAL_LIFE_DONE` 分支**真的调它**;
③ `ensure_supervise()` **会清 `guard.stop`** —— 🔴 这条最容易漏:
停止标志是常驻的退出条件,不清就**永远起不来**("完成"变成不可逆,与口径相反)。
⚠️ 判据一律**剥注释后**匹配(注释里写着"关闭后台任务"等字样,别被骗)。
"""
import ast as _ast
import re as _re4
src = (HERE / "collabd.py").read_text(encoding="utf-8")
# 🔴🔴 2026-10-04 **改用 AST 取函数体**(P0-54 同族第 3 次):
# 原实现用 `re.sub(r"#[^\n]*", "", code)` 粗暴剥注释 ⇒ 会把**字符串里的 `#`**
# 之后内容全删 —— prompt 模板里到处是 `"## 二、…"` ⇒ **函数体被吃掉** ⇒ 假红
# (实测 `ensure_supervise()` 明明在,判据却报"没清 guard.stop")。
# ✅ AST 按 `FunctionDef` 切片,字符串里的 `#` 一律不受影响。
_segs = {}
try:
for _n in _ast.walk(_ast.parse(src)):
if isinstance(_n, _ast.FunctionDef) and _n.name not in _segs:
_segs[_n.name] = _ast.get_source_segment(src, _n) or ""
except Exception as _e:
return [("⛔ 源码解析失败(%s)⇒ 判据没法跑" % _e, False)]
def _fn(name):
return _segs.get(name, "")
fn_stop, fn_life, fn_ens = _fn("supervise_stop"), _fn("goal_life_set"), _fn("ensure_supervise")
res = [
("`supervise_stop()` 存在(没有它就无从关闭常驻程序)", bool(fn_stop)),
("`supervise_stop()` 真的能停:写了停止标志 `guard.stop`", "guard.stop" in fn_stop),
("`supervise_stop()` 有强停兜底(⛔ 只写标志可能停不掉)",
"taskkill" in fn_stop or "SIGKILL" in fn_stop),
("`goal_life_set()` 在**已完成**分支调用 `supervise_stop()`(完成 ⇒ 收工)",
"GOAL_LIFE_DONE" in fn_life and "supervise_stop" in fn_life),
("`goal_life_set()` 对**阻碍/已暂停**明确不调停(用户口径②:阻碍不用关)",
("GOAL_LIFE_BLOCK" in fn_life or "保持运行" in fn_life)
and _re4.search(r"GOAL_LIFE_DONE.*?supervise_stop", fn_life, _re4.S) is not None),
("`ensure_supervise()` **会清 `guard.stop`**(⛔ 不清 ⇒ 完成后再也起不来)",
"guard.stop" in fn_ens and "unlink" in fn_ens),
]
return res
@case("跨区自愈:**只补进行中的区**(⛔ 已完成/阻碍/已收工的一律不拉)")
def t_peer_supervise_sweep():
"""🔴 2026-10-03 补的跨区自愈(实测坐实缺口:本区 `ensure_supervise()` 只看本区心跳,
本区日志搜别区名**零命中** ⇒ 别区常驻被外部收走后**没人知道、没人补**)。
✅ 本条守三条**越界红线**(少一条就可能去动别的区)——源码级断言:
① 目标 `lifecycle` **必须是「进行中」**才拉(用户口径:完成=已收工,阻碍=不用开);
② **⛔ 只读别区心跳**(不得写别区的 `goal.json`/队列 —— 那是别的区主会话的领地);
③ 有 `guard.stop` 的区**⛔ 不拉**(刚被收工的区,拉起来=对抗用户的停)。
⚠️ 另守两条工程约束:**接在主循环里**(⛔ 没接=写了函数也不生效)
+ **有节流**(⛔ 每轮都去 stat 别区是白耗 IO)。
🔴🔴 **2026-10-04 修本体 bug(P0-54 同族第 3 次)**:
原实现用 `re.sub(r"#[^\n]*", "", code)` **粗暴剥注释** ⇒ 它会把
**字符串字面量里的 `#`** 之后的内容**全删掉** —— 而 prompt 模板里到处是
`"## 二、…"`、`"## 三、…"` ⇒ **它后面的函数体整个被吃掉**
⇒ `peer_supervise_sweep()` 明明在源码里(实测 `collabd.py:403`),
判据却报「⛔ 源码里没有」⇒ **假红**。
✅ 正解=**用 `ast` 按函数体切片**(P0-54 早就写了「等宽要按函数体切片」,
这条是同一个病的第 3 次 ⇒ 这次改成 AST,⛔ 不再用正则)。
"""
import ast as _ast
src = (HERE / "collabd.py").read_text(encoding="utf-8")
fn = ""
try:
_tree = _ast.parse(src)
for _n in _ast.walk(_tree):
if isinstance(_n, _ast.FunctionDef) and _n.name == "peer_supervise_sweep":
fn = _ast.get_source_segment(src, _n) or ""
break
except Exception as _e:
return [("⛔ 源码解析失败(%s)⇒ 判据没法跑" % _e, False)]
# 「接在主循环里」要**全文**看(调用点必然在函数体外)
_code = "\n".join(ln for ln in src.splitlines()
if not ln.lstrip().startswith("#"))
if not fn:
return [("⛔ 源码里没有 `peer_supervise_sweep()` ⇒ 跨区自愈不存在", False)]
return [
("跨区自愈函数存在(⛔ 缺它=别区停了没人管)", bool(fn)),
# 🔴🔴 2026-10-04 改:闸① **收窄** —— 目标非「进行中」时**先看队列**,
# 有未完成件仍拉(用户口径「检查程序必然要去处理队列」)。
# ⛔ 旧断言("非进行中一律不拉")已过时,不改它会**红着掩盖新口径**。
("闸①:目标非「进行中」时**改看队列**(有未完成件仍拉 —— 有活没人干)",
"GOAL_LIFE_RUN" in fn and "queue_pending_of(" in fn),
# ⚠️ 判据写准:`read_text` 在函数里**本来就有一处合法用途**(读别区心跳)⇒
# ⛔ 不能断言"函数里没有 read_text"(那会误伤心跳那行 —— 我第一版就是这么写错的)。
# 真正要禁的是「**自己另写一段读 `tasks.json` 的代码**」⇒ 直接查那个特征串。
("闸①:用的是**唯一事实源** `queue_pending_of()`(⛔ 不在这里另写读 tasks 的代码)",
"queue_pending_of(" in fn and "tasks.json" not in fn),
("闸②:⛔ **只读**别区心跳(只 read_text 心跳,⛔ 不写别区 goal.json)",
"supervise-heartbeat.json" in fn and fn.count("write_text") == 0),
("闸③:有 `guard.stop` 的区 ⛔ 不拉(别对抗用户的收工)", "guard.stop" in fn),
("起别区时用**它自己的副本+它自己的 COLLABD_CONFIG**(⛔ 指向本区就串了)",
"COLLABD_CONFIG" in fn and ".workbuddy" in fn),
("有节流 `PEER_SWEEP_EVERY`(⛔ 每轮都碰别区=白耗 IO)", "PEER_SWEEP_EVERY" in src),
("**接在主循环里**(⛔ 写了函数不接=永不生效)", "peer_supervise_sweep()" in _code),
("Popen 带 `creationflags`(⛔ 漏了会闪黑窗,仓内另有判据守这条)",
"creationflags" in fn),
]
def _tail_has_board_note(code: str) -> bool:
"""🔴 2026-10-05:判「末尾『后续协作』段是否说清**本区不起看板**」。
⚠️ **为什么不能全文 `in` 一把梭**(本轮实测踩到):`init_workspace.py` 里
`peer_workspaces` 全文出现 **3 次**、`不起看板` **2 次**,其中**多数在注释里**
⇒ 用 `"不起看板" in src` 判,**把那段 print 整块删掉后判据照样绿**(恒绿假修)。
✅ 正解=**锚定到「✅ 完成。后续协作」之后的那段代码**(调用方传进来的 `code` 已剥注释,
⇒ 注释里的字样不会命中),且**两句 print 都要在**:
· 一句说「本区⛔ 不起看板」(否则用户以为要自己起一份)
· 一句指路「加进主工作区的 `peer_workspaces`」(否则知道不起、却不知道去哪并看)
"""
i = code.find("✅ 完成。后续协作")
if i < 0:
return False
seg = code[i:i + 1500]
return ("不起看板" in seg) and ("peer_workspaces" in seg)
@case("初始化脚本:**指向本区副本**(⛔ 不许再让用户跑技能目录那份)")
def t_init_points_to_own_copy():
"""🔴 2026-10-03 修的误导:`init_workspace.py` 原先 `--ensure-goal-dir` 与末尾
「后续协作」提示**全部指向技能目录的 `collabd.py`/`board.py`**。
🔴 **为什么有害**(不是洁癖):定案口径是**各工作区程序各自独立**(技能目录=源,
`.workbuddy/collab/`=各区副本,`deploy_code.py` 分发)⇒ 让用户跑目录那份
**会拉起第二个常驻、写到技能目录的 `tmp/`**(⛔ 那不是任何工作区的 inbox)
⇒ 状态写到别处:看板读不到、故障排查被带偏。
✅ 判据:`--ensure-goal-dir` 的实参**优先取本区副本**,末尾提示同理;
⚠️ 副本不存在时**允许回落**,但⛔ 必须**打印说明**(静默回落=用户以为在跑副本)。
"""
src = (HERE / "init_workspace.py").read_text(encoding="utf-8")
code = re.sub(r"\"\"\".*?\"\"\"", "", src, flags=re.S)
code = re.sub(r"(?m)^\s*#.*$", "", code)
res = [
("有「取本区副本」这一步(`_own = ws/.workbuddy/collab/collabd.py`)",
".workbuddy" in code and "collabd.py" in code),
("`--ensure-goal-dir` 用的是**本区副本优先**的路径(⛔ 不是硬编码 SK)",
code.count("SK / \"collabd.py\"") <= 2 and "_cd" in code),
("副本缺失时**回落**用技能目录那份(⛔ 硬失败会挡住首次初始化)",
"_fallback" in code),
("回落时**打印说明**(⛔ 静默回落=用户以为在跑副本)",
code.count("print") >= 2 and "回落" in code),
# 🔴 2026-10-05 改判据:末条原来要求 `_bd_show` 存在(=末尾会打印「起看板」命令)。
# ⚠️ 那是**旧口径**:SKILL.md:809 早有 10-03 定案「看板只保留一份(主工作区那份)」,
# 各区⛔ 不起看板 ⇒ 新区末尾**本就不该**再打印 board 启动命令。
# ⇒ 判据跟着改:仍然要求「后续协作提示指向副本」(`_cd_show`),
# 但另一条换成**新口径的事实**——末尾必须说清「本区不起看板 + 要并看走 peer_workspaces」。
# ⛔ 别把 `_bd_show` 塞回去凑数:那会把出错的形态提示又引回来。
("末尾「后续协作」提示也指向副本/带说明(⛔ 别再让人跑目录那份)",
"_cd_show" in code),
("末尾提示**不起本区看板**、要并看走 `peer_workspaces`(2026-10-03 定案:看板只保留一份)",
_tail_has_board_note(code)),
]
return res
@case("自指型判据:**必须被排除**(⛔ 否则 `goal_state()` 只能自己满足自己,永远锁死)")
def t_acc_selfref_excluded():
"""🔴🔴 2026-10-04 用户点破:「**A6 判据自指 ⇒ `goal_state()` 逻辑死锁**」。
🔴 **死锁形状**(不是时序问题、⛔ 也等主会话解决不了):
`goal_state()` 的**路3 要读这条判据**,而这条判据判的内容**就是**「三路并集的结论」
⇒ 它**只能由"自己写自己过"满足** ⇒ 固定点之外永不到达。
🔴 **实测坐实**:把 A6 临时写成「过|…」⇒ `goal_state()` 立刻 pass;还原 ⇒ 立刻回 open。
✅ 修法=**路3 排除自指型判据**,判据查四件事(源码级 + 行为级各一半):
① `_acc_is_selfref()` 存在,且**登记制**(`_ACC_SELFREF_KEYS`)与**文本兜底**(`_ACC_SELFREF_TOKENS`)**两条都在**;
② `goal_state()` 路3 与 `_acc_short()` **两处都剔**(⛔ 只剔一处 ⇒ 唤醒正文仍会念「还差 A6」,
造出「`goal_state()` 已判 pass、这里还在报」的新自相矛盾);
③ **fail-closed**:剔完一条不剩 ⇒ 判「未声明」,⛔ 不算过;
④ **兜底必须窄**(`_ACC_SELFREF_WIN` 有上限)—— 扫整段会**误伤 A2**(它正文里顺口提了一句
`goal_state()` 只读文本)⇒ 悄悄放掉一条真门禁,**比原 bug 更坏**(原 bug 是吵,这个是静默失守)。
⚠️ **源码级那两条必须按 AST 精确定位,⛔ 不可用「函数体里含某串」**(2026-10-04 变异验证实测栽过):
- 🔴 `_acc_is_selfref` 在 `goal_state()` 里出现**两次**(`_real` 的推导式 + `_dropped` 的推导式)
⇒ 判据写成 `"_acc_is_selfref" in fn_gs` 时,**只把 `_real` 那处改掉照样 PASS**(漏网)。
- 🔴 `undeclared` 在 `goal_state()` 尾部 `return "pass" if eff else "undeclared"` 里也有
⇒ 判据写成 `re.search(r"if not _real.*?undeclared", ...)` 时,**把中间那个 `return` 拆掉照样 PASS**(漏网)。
⇒ 一律改成**在 AST 上找指定 Assign / 指定 If 的 body**,别用"整段文本里有没有"。
"""
import ast as _ast5
import re as _re5
src = (HERE / "collabd.py").read_text(encoding="utf-8")
code = _re5.sub(r"\"\"\".*?\"\"\"", "", src, flags=_re5.S)
code = _re5.sub(r"(?m)^\s*#.*$", "", code)
def _fn_src(name):
m = _re5.search(r"\ndef %s\(.*?(?=\ndef )" % _re5.escape(name), code, _re5.S)
return m.group(0) if m else ""
def _fn_node(name):
"""AST 上**唯一定位**某个顶层函数(同名多个 ⇒ 返回 None,⛔ 不许取第一个)。"""
hits = [n for n in _ast5.walk(_ast5.parse(src))
if isinstance(n, _ast5.FunctionDef) and n.name == name
and isinstance(getattr(n, "col_offset", 0), int)]
tops = [n for n in hits if n.col_offset == 0]
return tops[0] if len(tops) == 1 else None
def _unparse(nodes):
return "".join(_ast5.unparse(n) for n in nodes)
fn_sr, fn_gs, fn_as = _fn_src("_acc_is_selfref"), _fn_src("goal_state"), _fn_src("_acc_short")
nd_gs, nd_as = _fn_node("goal_state"), _fn_node("_acc_short")
# --- AST:`_real = [...]` 这个**赋值**的推导式里到底有没有剔 ---
# ⚠️⚠️ 判据写法上的两个坑(2026-10-04 变异验证实测各栽一次):
# ① `_real` 是 **Assign 左侧的名字**,⛔ **推导式的 target 里没有它**
# (`goal_state` 的是 `(_k, _v)`、`_acc_short` 的是 `k`)⇒ 只能在 Assign 上找。
# ② 过滤条件在**生成器的 ifs 子句**里,⛔ 不在 `elt`;且 `elt` 是**单个节点**
# (`goal_state` 那个还是 Tuple)⇒ `list(elt)` 会 TypeError。
def _real_filter_excludes_selfref(node):
if node is None:
return False
for n in _ast5.walk(node):
if not (isinstance(n, _ast5.Assign) and len(n.targets) == 1
and isinstance(n.targets[0], _ast5.Name)
and n.targets[0].id == "_real"):
continue
if not isinstance(n.value, _ast5.ListComp):
continue
scan = list(_ast5.walk(n.value.elt)) + [x for g in n.value.generators
for i in g.ifs for x in _ast5.walk(i)]
if any(isinstance(x, _ast5.Call) and getattr(x.func, "id", "") == "_acc_is_selfref"
for x in scan):
return True
return False
# --- AST:`if not _real:` 这个 If 的 body 里到底有没有 `return "undeclared"` ---
def _not_real_returns_undeclared(node):
if node is None:
return False
for n in _ast5.walk(node):
if not isinstance(n, _ast5.If):
continue
t = n.test
is_not_real = (isinstance(t, _ast5.UnaryOp) and isinstance(t.op, _ast5.Not)
and isinstance(t.operand, _ast5.Name) and t.operand.id == "_real")
if not is_not_real:
continue
for stmt in n.body:
if isinstance(stmt, _ast5.Return) and isinstance(stmt.value, _ast5.Constant) \
and stmt.value.value == "undeclared":
return True
return False
res = [
("`_acc_is_selfref()` 存在(没有它就无从识别自指)", bool(fn_sr)),
("**登记制**在(`_ACC_SELFREF_KEYS`):⛔ 不依赖判据文本,是唯一权威路径",
"_ACC_SELFREF_KEYS" in code),
("**文本兜底**在(`_ACC_SELFREF_TOKENS` + 有窗口上限):补登记漏网的",
"_ACC_SELFREF_TOKENS" in code and "_ACC_SELFREF_WIN" in code),
("窗口**有上限且不为 0**(⛔ 扫整段会误伤 A2 那类真判据 ⇒ 静默失守)",
bool(_re5.search(r"_ACC_SELFREF_WIN\s*=\s*([1-9]\d*)", code))),
("AST:`goal_state()` 里 `_real` 的推导式**真的剔**(⛔ 不看整段文本,见 docstring 变异坑)",
_real_filter_excludes_selfref(nd_gs)),
("AST:`_acc_short()` 里也剔(⛔ 只剔一处 ⇒ 唤醒正文仍念「还差 A6」,造出新自相矛盾)",
_real_filter_excludes_selfref(nd_as)),
("AST:`if not _real:` 的 body 里**真的 `return \"undeclared\"``(fail-closed)",
_not_real_returns_undeclared(nd_gs)),
("⛔ **没放松实质门禁**:`for _k, _v in _real:` 仍逐条判 pass(其余判据照判)",
"for _k, _v in _real:" in fn_gs or "for _k, _v in _real" in fn_gs),
]
# ---- 行为级:真起子进程跑真函数,用真数据量(⛔ 源码含关键字不算数) ----
try:
import subprocess as _sp
probe = r'''
import importlib.util, json, os, sys, tempfile
from pathlib import Path
spec = importlib.util.spec_from_file_location("cd_probe", sys.argv[1])
m = importlib.util.module_from_spec(spec)
sys.modules["cd_probe"] = m
spec.loader.exec_module(m)
sr = m._acc_is_selfref
res = {}
# ① 真自指(测试3 的 A6 原句形状):必须判 True
A6 = "\U0001f534 不过|goal_state() 返回 pass 且 goals_open() 为空(三路全过)"
res["A6 真自指判 True"] = sr("a6 目标状态", A6) is True
# ② 真门禁 A2:正文**深处**顺口提一句 goal_state() ⇒ 必须判 False(⛔ 误伤=静默放掉真门禁)
A2 = ("\U0001f534 不过|" + "A2 快照陷阱说明。" * 40 + " 另注:goal_state() 只读文本、不重验事实。")
res["A2 长正文顺口提及判 False"] = sr("a2 快照", A2) is False
# ③ 登记制:文本完全看不出自指,只要登记过就排除
res["登记制压过文本"] = sr("A6 目标状态", "过|已确认") is True
# ④ 普通真判据:一律不排除
res["普通判据判 False"] = sr("a1 产物", "\U0001f534 不过|产出三份文档") is False
# ⑤🔴 端到端:真造 goal.json 跑**真 goal_state()**(⛔ 这才是死锁的硬证据)
# 🔴🔴 **隔离必须用 `COLLABD_CONFIG`,⛔ 不可用 `DSH_COLLAB_WS`/`COLLABD_WORKSPACE`**:
# 真源 `load_cfg()` 的候选顺序是 ① `COLLABD_CONFIG` → ② `<ws>/.workbuddy/collab/collabd.config.json`
# ⇒ **只设 env 不给 config,会回落读到真工作区的配置**(本判据曾因此把真 `goal.json` 覆盖掉,
# 2026-10-04 已从 `bak-goalctl-20261002/` 恢复)。⇒ 这里**显式写一份临时 config**。
# ⚠️ `TG`(任务图)也由这份 config 的 `taskgraph` 决定 ⇒ 一并指到夹具,⛔ 不读真任务图。
tmp = tempfile.mkdtemp(prefix="gs_")
_cfg = Path(tmp) / "collabd.config.json"
_cfg.write_text(json.dumps({
"workspace": tmp.replace("\\", "/"),
"inbox": "inbox",
"taskgraph": "inbox/taskgraph.json",
"live": "inbox/realtime.md",
}, ensure_ascii=False), encoding="utf-8")
os.environ["COLLABD_CONFIG"] = str(_cfg)
spec2 = importlib.util.spec_from_file_location("cd_probe2", sys.argv[1])
m2 = importlib.util.module_from_spec(spec2)
sys.modules["cd_probe2"] = m2
spec2.loader.exec_module(m2)
# 🔴 自检:**INBOX 必须真落在临时目录**,否则说明隔离没生效 ⇒ 宁可报红也别再写真文件
if str(m2.INBOX).replace("\\", "/").find(tmp.replace("\\", "/")) < 0:
print(json.dumps({"__隔离失效: INBOX=%s 不在临时目录" % m2.INBOX: False}, ensure_ascii=False))
raise SystemExit(0)
inbox = Path(m2.INBOX); inbox.mkdir(parents=True, exist_ok=True)
(inbox / "tasks.json").write_text(json.dumps({"t": {"state": "done"}}), encoding="utf-8")
(inbox / "taskgraph.json").write_text(json.dumps({"nodes": [{"id": "N1", "status": "done"}]}),
encoding="utf-8")
# ⚠️ 判据文本必须用**真源真会写出来的形状**(`过|…`,⛔ 不带 `🔴` 前缀):
# `_acc_is_pass` 取竖线左边那段判,`🔴 过` 的 head 仍带 `🔴` ⇒ 会被判「未过」(假红)。
def _write(acc):
(inbox / "goal.json").write_text(json.dumps(
{"nodes": [{"id": "N1", "status": "done"}], "acceptance_state": acc}),
encoding="utf-8")
# 场景 X:**只有自指判据、且它没过** ⇒ 正确行为=`undeclared`(⛔ 不是 pass)
_write({"a6 目标状态": "\U0001f534 不过|goal_state() 返回 pass 且 goals_open() 为空(三路全过)"})
res["隔离生效: INBOX 落在临时目录"] = True
res["端到端X: 全自指且未过 ⇒ undeclared(不是pass)"] = (m2.goal_state() == "undeclared")
# 场景 Y:自指判据已写「过」 + **一条真判据也过了** ⇒ 正确行为=`pass`
#(⛔ 这是修复前会卡死的那个形状:路3 要读 A6,A6 又判"三路全过")
_write({"a6 目标状态": "过|goal_state() 返回 pass", "a1 产物": "过|三份文档已交"})
res["端到端Y: 自指+真判据全过 ⇒ pass(不再死锁)"] = (m2.goal_state() == "pass")
# 场景 Z:自指 + **一条真判据没过** ⇒ 必须 `open`(⛔ 不许因为剔了自指就把真门禁也放过)
_write({"a6 目标状态": "\U0001f534 不过|goal_state() 返回 pass", "a1 产物": "不过|只交了两份"})
res["端到端Z: 真判据没过 ⇒ open(没放松门禁)"] = (m2.goal_state() == "open")
# 场景 W:自指**已写「过」** + 真判据**没过** ⇒ 仍必须 `open`
#(⛔ 这条专抓"因为剔了自指就整路放行"的写法:自指写过就当全过 ⇒ 静默失守)
_write({"a6 目标状态": "过|goal_state() 返回 pass", "a1 产物": "不过|只交了两份"})
res["端到端W: 自指写过但真判据没过 ⇒ open"] = (m2.goal_state() == "open")
print(json.dumps(res, ensure_ascii=False))
'''
out = _sp.run([sys.executable, "-c", probe, str(HERE / "collabd.py")],
capture_output=True, text=True, timeout=180, encoding="utf-8")
got = json.loads(out.stdout.strip().splitlines()[-1])
for k, v in got.items():
res.append(("真起进程验行为:" + k, v))
except Exception as _e: # ⛔ 起不来就必须报红,不能静默跳过
res.append(("真起进程验行为(⛔ 起不来即报红):" + type(_e).__name__, False))
return res
@case("**单包自包含**(⛔ 只复制这一个技能到别的机器,排版/提报用户/去 AI 味都要能用)")
def t_single_package_selfcontained():
"""🔴🔴 2026-10-04 用户定案:「需要整合到会话技能中,重点是只想复制一个技能到别的机器,
这些功能都可以正常使用」。
🔴 **改前的两处外部依赖**(⛔ 都是"不装另一个技能就静默失效"型):
① `reply-style-guard.py::_core()` 第②级回退指向 **另一个技能**的
`agent-operating-rules/references/回复排版-核心块.md`;
② `hooks/_env.py::_SNIFF = "agent-operating-rules"` —— 它是**技能库定位的识别特征**,
`skills_root()` / `env_check()` 三处都拿`(root)/_SNIFF).is_dir()` 当"库在不在"的判据。
⚠️ **实测过的失效形状(不是推断)**:只搬本包到空目录时,`reply-style-guard` 输出
**空 JSON**(日志里看不出是"机制坏"还是"没配规则")⇒ 排版闸门**整条静默失效**。
✅ 修法:① 把那份核心块**内联**进本包 `references/03-回复排版-核心块.md`(内容守恒、逐字搬),
`_core()` 改成**三级回退**(工作区规则文件 → **包内件** → 外部兜底);
② `_SNIFF` 改成**候选数组**,**第一个 = 本包自己** ⇒ 只拷本包也能定位技能库。
⚠️ 本判据守两件事:件在不在 + **代码真的先找包内**(⛔ 只查文件存在=假绿,
回归过一次:路径上溯少一级时文件在、但读不到,hook 照样静默空输出)。
"""
S = HERE.parent
core = S / "references" / "03-回复排版-核心块.md"
guard = (S / "scripts" / "hooks" / "reply-style-guard.py").read_text(encoding="utf-8")
envp = (S / "scripts" / "hooks" / "_env.py").read_text(encoding="utf-8")
res = [
("包内件 `references/03-回复排版-核心块.md` 在(⛔ 单包自包含的主力件)", core.exists()),
]
if core.exists():
t = core.read_text(encoding="utf-8")
# ⚠️⚠️ 标记必须按 `_extract()` 的**完整字面量**查(带空格与 ` -->`),
# ⛔ 不能查子串 `REPLY-CORE:END` —— 2026-10-04 变异验证实测栽过:
# 把 `<!-- REPLY-CORE:END -->` 改成 `<!-- REPLY-CORE:END(改坏) -->` 后
# 子串仍命中 ⇒ 判据放行,但**真提取器已经认不出**(END 串对不上 ⇒ 静默返回空)。
# ⛔ 判据必须与被仿刻的那个函数用**同一份字面量**。
_B = "<!-- REPLY-CORE:BEGIN"
_E = "<!-- REPLY-CORE:END -->"
res += [
("包内件带**完整** `REPLY-CORE` 标记(⛔ 提取器靠它认,⛔ 少一个就静默返回空)",
_B in t and _E in t),
("包内件非空(⛔ 标记在但块体空 = 注入 0 字符)",
len(t.split(_B, 1)[-1].split(_E)[0].strip()) > 100),
("包内件**不自指外部技能路径**(⛔ 单包环境下那句话会指向不存在的文件)",
"agent-operating-rules/scripts" not in t),
]
res += [
("`_core()` 有**包内件**这一级回退(⛔ 只留外部兜底=没自包含)",
"03-回复排版-核心块.md" in guard),
("🔴 **包内件优先于外部件**(⛔ 顺序反了又变成「看别人脸色」)",
guard.index("03-回复排版-核心块.md") < guard.index("CORE_REL)")),
("🔴 上溯层级对(`hooks` → **两级** → 包根,⛔ 少一级落到 `scripts/` 读不到)",
"os.path.dirname(os.path.dirname(_hooks))" in guard),
("`_env.py` 的识别特征**含本包自己**(⛔ 只认别的技能 ⇒ 单包下库定位失败)",
"_SNIFFS = (" in envp and '"session-mechanism"' in envp),
("`_env.py` 不再**只**靠 `agent-operating-rules` 判库在不在(⛔ 那是隐式耦合)",
"(root / _SNIFF).is_dir()" not in envp and "Path(rep[\"skills_root\"]) / _SNIFF" not in envp),
]
return res
@case("搬入的两块**不许被删掉/漂移**(⛔ 自决策口径 + 任务会话六阶段)")
def t_migrated_docs_intact():
"""🔴 2026-10-04 两件搬入,判据守住它们**在位**且**口径一致**。
① **功能优先协作协议**(原 `dsh-decision/references/01` → 本包 `references/02`)
—— 用户定案「这个属于会话的自决策方法,应该属于会话逻辑」。
⚠️ **2026-10-04 改**:本档**已是权威**(原为 `agent-operating-rules §1.6` 权威 · 本包落地副本)
⇒ 判据查的不只是"在不在",还要查**它声明的是「本档即权威」**(⛔ 指向外部技能当权威 = 单包失效)。
② **执行会话六阶段**(用户定案逐字:需求识别/调研/规划/执行/验证/归档清理
=「正式执行会话执行任务的流程」)⇒ 写在 `references/collab.md §5.5`。
⚠️ 判据**不许只查关键词计数**(P0-45 同族:字符串出现≠机制在);
还要查**接线**(`SKILL.md` 加载段真的挂上了、⛔ 否则档在包里没人读得到)。
"""
S = HERE.parent
fpr = S / "references" / "02-功能优先协作协议.md"
skl = (S / "SKILL.md").read_text(encoding="utf-8")
collab = (S / "references" / "collab.md").read_text(encoding="utf-8")
res = [
("`references/02-功能优先协作协议.md` 在位(搬入的落点)", fpr.exists()),
]
if fpr.exists():
t = fpr.read_text(encoding="utf-8")
res += [
("自决策档:**9 类自决策白名单**在(技术层永不问)", "9 类白名单" in t),
("自决策档:**只准提报用户 3 类**在(功能语义分叉/红线门禁/超边界)", "只准提报用户" in t),
("自决策档:**功能卡 4 问**在(用户侧输入格式,⛔ 此前无任何实体承接)",
"功能卡" in t and "怎样算成功" in t),
("自决策档:**提报用户格式**在(⛔ 不许出现路径/commit/标识符)",
"语言转换表" in t),
("自决策档:**拆包提报用户**在(红线问题⛔ 不得与技术方案捆问)", "拆包提报用户" in t),
("自决策档声明了**本档即权威**(⛔ 单包自包含的关键:⛔ 不能再指向外部技能当权威)",
"本档即权威" in t),
("自决策档**排版指向本包内件**(⛔ 不是外部技能的 §2)",
"references/03-回复排版-核心块.md" in t),
# 🔴 2026-10-04 改造后**这一条被作废**(原文是「声明了三处副本,改判据要同时改三处」)。
# 现在本档**即权威**(单包自包含),⛔ 不再要求同步另两处。
# ⇒ 判据反向:⛔ 那句「同时改三处」必须**已消失**(留着会让单包用户以为还有两份)。
("🔴 自决策档**不再要求「同时改三处」**(⛔ 已作废:本档即权威,留着会误导单包用户)",
"同时改三处" not in t),
]
res += [
# ⚠️⚠️ 查"接线"必须查**段标本身的完整性**,⛔ 不能只查某个子串在不在
# (2026-10-04 变异验证实测栽过:变异把 `**⑦ 我要判断` 改成 `**(已摘除)⑦ 我要判断`,
# 而判据查的正是 `⑦ 我要判断` 这个子串 ⇒ 改完照样命中 ⇒ **假绿**)。
# ⇒ 判据改为查「行首是 `**⑦ 我要判断`」这种**带行首锚定**的形式。
("🔴 **接线**:`SKILL.md` 加载段真挂了第 ⑦ 条(行首锚定,⛔ 不许被改成装饰)",
any(l.startswith("**⑦ 我要判断") for l in skl.splitlines())),
("🔴 **接线**:加载段指到 `references/02-功能优先协作协议.md`",
"references/02-功能优先协作协议.md" in skl),
]
# 六阶段:⛔ 同样不能只查「关键词在文件里」——用户原话那句里也含全部六个词
# (实测:删掉表格里的「归档清理」,原话那句仍在 ⇒ 子串计数不变 ⇒ 假绿)。
# ⇒ 改为查**六阶段表格那一行**(`collab.md §5.5` 的表行,逐行锚定)。
stage_lines = [l for l in collab.splitlines() if l.strip().startswith("| **")]
def _row(kw):
return any(kw in l and "缺了会怎样" not in l and l.strip().startswith("| **") for l in stage_lines)
for _idx, _kw in ((0, "需求识别"), (1, "需求调研"), (2, "方案规划"),
(3, "任务执行"), (4, "结果验证"), (5, "归档清理")):
res.append(("六阶段表行:**%s**(⛔ 查表行不是查关键词,原话里也有这六个词)" % _kw, _row(_kw)))
res += [
("六阶段是**任务会话的标准动作**(⛔ 别只是一张并列的表)",
"任务会话的标准动作" in collab),
("六阶段每行都写了「缺了会怎样」(⛔ 只写做什么=口号)",
"⛔ 后果" in collab and "缺了会怎样" in collab),
("六阶段**接上了自决策口径**(规划阶段 ⇒ 该不该问用户)",
"02-功能优先协作协议.md" in collab),
("⚠️ 标清了**与 `dsh-workflow` 的分工**(⛔ 别当同一件事)",
"dsh-workflow" in collab),
]
# ===== 单包自包含的**文档侧**:⛔ 不能再把外部技能当权威 =====
skl2 = (S / "SKILL.md").read_text(encoding="utf-8")
res += [
("🔴 `SKILL.md` 声明**本包自包含**(⛔ 复制一个就能用)",
"自包含" in skl2 or "已无必需外部依赖" in skl2),
("🔴 `SKILL.md` 不再说「依赖某技能」为**必需**(⛔ 只可作可选增强)",
"**依赖(⛔ 未并入,只声明)**" not in skl2),
# ⚠️ 判「权威归属」⛔ 不能只查"提了本档" —— 改造前那句是「权威在 `agent-operating-rules §1.6`」,
# 改造后是「本档即权威」,两处都提到"本档" ⇒ 只查本档会漏。
# ⇒ 查**反向**表述:不能再出现「权威分工/完整判据已收敛到通用技能/判据实体在 <外部技能>」。
("🔴 自决策档**不再把外部技能当权威**(⛔ 那会让单包用户读到「去别处找判据」)",
not any(k in t for k in ("权威分工", "完整判据已收敛到通用技能",
"判据实体在 `agent-operating-rules"))),
("🔴 自决策档保留了**来路说明**(⛔ 抹掉就查不到它从哪来、为何这样定)",
"来路" in t),
("🔴 排版指向**本包内件**(⛔ 随包,不依赖外部)",
"references/03-回复排版-核心块.md" in t),
# ⚠️ 变异验证实测栽过:只查「`03-回复排版-核心块.md` 在不在」是**假绿** ——
# 文件里它出现**两次**(§5.4 标题行 + L285 的"随包自带"说明行)。
# 把**标题行**改回「`agent-operating-rules §2`」时,L285 那个子串**仍然命中** ⇒ 判据放行。
# ⇒ 改为查**标题行本身**(§5.4 那一行必须带包内路径,且⛔ 不得提外部技能的 §2)。
("🔴 排版**标题行**指向本包内件(⛔ 查子串会假绿:同一文件里它出现两次)",
any("03-回复排版-核心块.md" in l and l.lstrip().startswith("###")
for l in t.splitlines())),
("🔴 排版标题行**不提外部技能 §2**(⛔ 那是「去别处找判据」的形状)",
not any(l.lstrip().startswith("###") and "agent-operating-rules" in l
and "排版" in l for l in t.splitlines())),
]
return res
@case("包体卫生:**不许长回备份/缓存/零引用文件**,且**没有版本控制兜底**(⛔ 删了就是真删)")
def t_pkg_hygiene():
"""🔴🔴 2026-10-04 用户要求「检查技能相关文件是否有可以整合、精简、移除的重复、冗余、多余描述」。
✅ 那轮清掉的(**10.7 MB → 1.9 MB,减 83%**):68 个 `*.bak-*`(8.6 MB)+
`__pycache__`/`.pyc`(255 KB)+ `install.log`(36 KB)+
**零引用**的 `assets/_syntax_probe.js`(109 KB)+ `SKILL.md` 那条 55 KB 变更流水账(压到 2.4 KB,**-96%**)。
🔴🔴 **为什么必须有这条判据(本轮取证到的硬事实)**:
**本技能包不在任何 git 仓库下** —— `.workbuddy/`/`E:/ProgramData/`/`E:/` 三级都无 `.git`;
文档库 `08-skills/` 只收了另外 7 个技能,**没有 session-mechanism**
⇒ **删掉的文件在这台机器上就是唯一副本没了** ⇒ 必须先有"包外快照"才敢删,
也因此**更不能让它重新长回来**(长回来 = 下次清理又要处理一遍 8.6 MB)。
⚠️ 同时盯两件同类退化:① **零引用文件**(`_syntax_probe.js` 就是这么烂在包里的 109 KB);
② 指向已删文件的**死证据引用**(注释写「原文照抄见备份 X」⇒ 备份删了它就是句假话,本轮改掉 6 处)。
"""
import re as _re6
res = []
bak = [p for p in HERE.parent.rglob("*.bak-*") if p.is_file()]
res.append(("⛔ 包内无 `*.bak-*` 历史备份(本轮清掉 68 个 / 8.6 MB)", not bak))
# ⚠️ `__pycache__`/`.pyc` 是**跑一次 selftest 就重新生成**的(`hooks/_env.py` 被导入就产生
# `scripts/hooks/__pycache__/_env.cpython-313.pyc`)⇒ **判它存在=判「永远红」**。
# ⇒ 判据只能是「⛔ 不许把它当能力件进包/入库」,而它**天然会重生** ⇒ 这里改成
# 「删干净后跑一遍 selftest,确认它只重生在 __pycache__ 里、⛔ 不在任何源码目录留下散 .pyc」。
loose_pyc = [p for p in HERE.parent.rglob("*.pyc") if "__pycache__" not in str(p)]
res.append(("⛔ 无散落的 `.pyc`(⛔ 只能在 `__pycache__` 内;它跑一次就重生,不判存在)",
not loose_pyc))
# ⚠️ `install.log` 同理:`install.py:96,340` 每次运行都 append ⇒ 同样只能判"不巨大"。
log = HERE.parent / "install.log"
logsz = log.stat().st_size if log.exists() else 0
res.append(("⛔ `install.log` 不超 64 KB(运行日志,⛔ 不属能力件;本轮清掉 36 KB)",
logsz <= 64 * 1024))
# 零引用文件:assets/ 下每个 .js,⛔ 排掉本身被引用的
dead = []
for p in list((HERE.parent / "assets").glob("*.js")):
stem = p.stem
hit = 0
for q in HERE.parent.rglob("*"):
if not q.is_file() or q == p or "__pycache__" in str(q):
continue
if q.suffix not in (".py", ".md", ".html", ".js", ".sh", ".json", ".example"):
continue
try:
if stem in q.read_text(encoding="utf-8", errors="ignore"):
hit += 1
except Exception:
continue
if hit == 0:
dead.append(p.name)
res.append(("⛔ `assets/*.js` 无零引用文件(`_syntax_probe.js` 烂在包里 109 KB)", not dead))
# 死证据引用:注释/文档里点名某个 *.bak-xxx,⛔ 排除 install.py(它的备份在 .workbuddy/,是活机制名)
# ⚠️⚠️ 还要**排除 selftest.py 自己** —— 本判据的注释里就写了那个串(2026-10-04 实测自我误报)。
# ⚠️⚠️ 以及 `references/pitfalls.md` 的 **P0-45** —— 那一节**专门讲「判据会自我误报」这个坑**,
# 必须举出那个串当例子(2026-10-04 实测:把它当死证据 ⇒ 判据对自己的说明书报红)。
# 判据=「活机制名」还是「死证据」:**看它指的是不是 `.workbuddy/` 下真存在的备份**。
pat = _re6.compile(r"[\w\.\-]+\.bak-[\w\-]+")
dref = []
for p in HERE.parent.rglob("*"):
if not p.is_file() or "__pycache__" in str(p):
continue
rel = p.relative_to(HERE.parent).as_posix()
# ⛔ 排除三个"提到备份名是**机制本身/说明书**"的文件
if rel in ("install.py", "scripts/selftest.py"):
continue
# 🔴🔴 2026-10-05 加:`install.log` = **运行日志**(同文件上一条判据自己写着
# 「运行日志,⛔ 不属能力件」)⇒ 它里面「已备份 → …bak-session-mechanism-…」
# 是**安装时的事实陈述**,⛔ 不是注释里的死证据引用。
# ⚠️ 它是 **append 型**:每跑一次 `install.py --apply` 就多一行 ⇒ 不排除的话
# **判据会随安装次数必然变红**(2026-10-05 实测:装完当场 FAIL 1,与本次改动无关)。
# 📌 判据本意="注释不许指向已删的备份";日志**不承担这个语义**。
if rel == "install.log":
continue
if rel == "references/pitfalls.md":
# ⛔ 只豁免 P0-45 那一节(讲这个坑的),其它节的死证据仍要报
t_all = p.read_text(encoding="utf-8")
i45 = t_all.find("## P0-45")
head, tail = (t_all[:i45], t_all[i45:]) if i45 >= 0 else (t_all, "")
nxt = tail.find("\n## P0-", 4)
sec45 = tail[:nxt] if nxt > 0 else tail
for m in pat.finditer(head):
dref.append("%s:%s" % (p.name, m.group(0)[:40]))
continue # ⛔ P0-45 节内的举例不计入
try:
t = p.read_text(encoding="utf-8")
except Exception:
continue
for m in pat.finditer(t):
dref.append("%s:%s" % (p.name, m.group(0)[:40]))
res.append(("⛔ 无「见备份 X」式死证据引用(备份已删,注释不能再说)", not dref))
# 变更流水账:frontmatter 的 last_change 行不许再超长(曾 55 KB / 全文 66%)
# ⚠️ 判据只查**行体积**;⛔ 不查「本条为准」字样 —— 2026-10-04 实测那个词在新台账 L6 与
# L24/L98/L267(各自成段的历史改法说明)里都合法存在,拿它当判据只会误报。
# 🔴🔴 2026-10-04 **阈值 1 KB → 2 KB**(用户口径:判据守的是**口径**,⛔ 不是守某个数字):
# 病根:1 KB 是照**当年那次**(339 B)定的,属于「按当时观测值钉死」⇒
# 台账只要多一条**同量级**的现行口径(1241 B)就红,而被砍掉的恰恰是
# "为什么这么做"这类**必须留**的内容 ⇒ 判据开始**逼人删有用信息**(本末倒置)。
# 真正要防的是「**一行长成 55 KB 的流水账**」⇒ 那个量级已被下面的 2 KB 正文判据拦死。
# ⚠️ 与下方"正文无超 2 KB"**同阈值**,口径统一(⛔ 不再出现两个数字互相打架)。
skl = (HERE.parent / "SKILL.md").read_text(encoding="utf-8").splitlines()
l6 = skl[5] if len(skl) > 5 else ""
_l6b = len(l6.encode("utf-8"))
res.append(("⛔ `SKILL.md` 末行不超 **2 KB**(当年 55 KB = 全文 66%% ⇒ 它是**台账**不是日记;"
"⚠️ 阈值 2026-10-04 由 1 KB 上调,⛔ 守的是「不许长成流水账」这个口径、不是 339 B 这个数)",
_l6b <= 2048))
#⛔ 真正要盯的是「正文有没有哪一行又长成流水账」(当年就是 L6 一行 55 KB)。
# ⚠️ 两条**该长的**必须豁免,否则判据永远红(2026-10-04 实测):
# ① L3 `description:`(2.2 KB)—— 技能描述本就是长句,⛔ 不该压;
# ② **已标作废**的历史段(如 L234「同工作区 = 四类会话 … 已于 2026-10-03 作废」)——
# 它长是因为**把作废原因写全了**,删掉就丢了"为什么废"。
body = [(i, len(l.encode("utf-8"))) for i, l in enumerate(skl, 1)
if not l.startswith("description:") and "作废" not in l and len(l.encode("utf-8")) > 2048]
res.append(("⛔ `SKILL.md` 正文无超 2 KB 的行(⛔ 当年 L6 单行 55 KB = 全文 66%%;"
"⛔ 已标作废的段与 description 豁免)", not body))
if dref or dead or bak:
res.append(("⚠️ 具体残留:%s" % ((bak[:2] + dead + dref[:2]) or "-"), True))
return res
@case("告警文案:**必须说清后果**(⛔ 不许只说「缺了 A」)")
def t_warn_text_states_consequence():
"""🔴 2026-10-03 用户逐字指正:「**标题里没读类别前缀) 这句话都不知道有什么作用**」。
🔴 **它错在哪**:原文案只陈述"读不到类别前缀"这个**现象**,
⛔ **没说缺了会怎样** ⇒ 读者不知道该做什么(看板上它跟正常文案长得一样,
只是短一点 —— 这种"只描述现象"的告警=**没有行动价值的废话**)。
🔴🔴 2026-10-03 **我自己犯了这条**(用户当场抓住):第一版我把它改成
「⚠ 无类别 ⇒ **不会被派活**」—— 🔴 **那个后果是我编的**。
取证:`collabd.py::queue_pending()` 读 **`tasks.json` 台账**,`topic` 在派活链路上
**根本没出现**;而这排 `cells` **已经是** `role==='任务会话'` 筛出的**现存任务会话**,
⛔ 不是"待派活清单" ⇒ "不会被派活"=**说了个不存在的后果**。
✅ **正确口径=只说能证实的后果**:`⚠ 归不出类别 · 不计入分工`
(依据:`board.py` 归线判据 `topic == 线 ∨ cwd_tail == 线` ⇒ topic 空即不归任何线)。
📌 **本条判据升级后的红线**:⚠️ **写"会怎样"之前必须先取证那个后果真的存在**
—— 否则"讲清后果"会变成"**编一个后果讲得更像回事**",比原句更坏。
✅ 本条判据是**通用红线**,不是这一句话的专属:
凡"⚠/缺了/没读"类文案,**必须同时出现一个后果词**(会…/不会…/导致…)。
"""
import re as _re6
html = (HERE.parent / "assets" / "board.html").read_text(encoding="utf-8")
# 剥掉注释 ⇒ 只查**真正渲染给用户看的文案**(注释里的原话留档不算)
code = _re6.sub(r"/\*.*?\*/", "", html, flags=_re6.S)
code = _re6.sub(r"(?m)^\s*//.*$", "", code)
code = _re6.sub(r"//[^\n]*", "", code)
# ⚠️ 只看**纯文案字面量**(`tx(...)` / `chip(...)` 这类调用的**第一个字符串参数**)
# ⛔ 不用"整段代码里抓 ⚠ 段"那种粗法 —— 实测它会**跨语句边界**把
# `(⛔ 不因此判完成)` 之类切到下一个字面量里 ⇒ **8 条里 7 条是误报**。
# ✅ 锚定 `tx(` / `chip(` 之后的第一个引号串,才是用户真正看到的句子。
lits = []
for m in _re6.finditer(r"(?:tx|chip|lbl)\(\s*(['\"])(.*?)\1", code, _re6.S):
s_txt = m.group(2)
if "⚠" in s_txt:
lits.append(s_txt)
# ⚠️ 后果词表**必须包含「不…」式否定** —— 例「⚠ 未声明验收判据(⛔ 不因此判完成)」
# 它**已经**把后果说清了(不判完成),⛔ 只因原表里只有"会/不能/无法"而漏判。
# 📌 教训:写判据时**先拿真实文案跑一遍**(本次就是这样才发现自己错,而不是去改好文案)。
bad = [w for w in lits
if not _re6.search(r"会|导致|漏管|不能|无法|勿|别|不因此|不算|勿当", w)]
return [
("渲染文案里**没有**「只说现象不说后果」的告警(查到 %d 条可疑:%s)"
% (len(bad), bad[:3] or "无"), not bad),
# 🔴🔴 2026-10-03 **用户直接指示:那一行整个删掉**(逐字:「越写越看不懂
# 还是删除了把」)⇒ 这里从"要求它写对"改成"**不许它回来**"。
# 📌 两次踩坑留下的结论:① 原文案只说现象(啰嗦)② 我"改成讲后果"又编了后果
# ⇒ **第三次选择是删掉**:这行信息量不足,⛔ 不值得占版面、更不值得编文案。
("⛔ 任务会话格**不许**再出现类别/前缀相关文案(用户已指示删除)",
("归不出类别" not in code) and ("没读类别前缀" not in code)
and ("类别:'+W.topic" not in code)),
("⛔ 那句编出来的后果也不许回来(`不会被派活`)", "不会被派活" not in code),
]
def _worker_label_from_source() -> str:
"""**AST 抽** `board.py::_ROLE_LABEL['worker']` 的字面量(唯一真源)。
🔴 2026-10-03 23:4x 新增:标签从「协作目标」改名成「执行会话」时,
判据里**写死**的那份字面量**当场报红**(这是它该做的),
但也意味着**每次改名都得手改判据** ⇒ 漏一处就假红。
✅ 正解=**判据从真源派生**:抽出来用 ⇒ 改名只改 `_ROLE_LABEL` 一处,判据自动跟随。
⚠️ 抽不到 ⇒ 返回空串(让下游断言**报红**,⛔ 绝不猜一个词填上去)。
"""
import ast as _ast
try:
t = _ast.parse((HERE / "board.py").read_text(encoding="utf-8"))
except Exception:
return ""
for n in t.body:
if not isinstance(n, _ast.Assign):
continue
tgt = n.targets[0]
nm = getattr(tgt, "id", None) or getattr(tgt, "attr", None)
if nm != "_ROLE_LABEL":
continue
for k, v in zip(n.value.keys, n.value.values):
if getattr(k, "value", "") == "worker" and isinstance(v, _ast.Constant):
return str(v.value)
return ""
@report("产物落点:**每个目标一个独立文件夹**(⛔ 代码与其他路径除外)|**报告型**")
def t_artifacts_land_in_goal_dir():
"""🔴 2026-10-04(用户要求逐字):「**检查是否每个工作区 进行 执行任务时每个目标都有独立的
文件夹 保存生成的内容(要求处理其他路径下的文件 代码 等除外)**」
🔴 **为什么补这条**(实测坐实的缺口):机制本身有(`goal_dir_name()`/`_ensure_goal_dir()`/
`t_goal_dir_mechanism`)⇒ ⛔ 但**那条只守"机制在不在",不守"产物真的落进去了吗"**。
实测反例:测试3 根目录长出 `新环境验证/`、`a6-final-check/`(2 份 S2/S3 取证 +
6 个临时修库脚本)**散在目标文件夹之外** ⇒ 机制在、结果照样乱。
✅ 本条判据=**逐个真工作区**(⛔ 不造夹具,因为要验的就是真磁盘):
① 目标文件夹**存在**(按 `goal_dir_name()` 现算,⛔ 不写死目录名);
② **产物型 `.md` 必须落在目标文件夹或 `交付物/` 内**;
③ ⛔ **豁免**:`CODEBUDDY.md`/`AGENTS.md`/`README.md`(规则与说明,不是产物)、
`docs/`、`归档/`、`脚本/代码`(用户口径:「**处理其他路径下的文件 代码 等除外**」)。
📌 **豁免写清楚,否则这条判据会假红**:一个连"什么是产物"都没定义的检查=没用的检查。
⚠️ 本条只**报告与提示**,⛔ **不删不移任何文件**(清理是用户的事,且可能是别人写的)。
"""
# 产物型文档:⛔ 排除规则类/说明类文件名(它们天生在根目录,不是产物)
RULE_FILES = {"CODEBUDDY.md", "AGENTS.md", "README.md", "MEMORY.md", "state.py"}
# 🔴 `inbox`=**机制自己的状态/信号文件**(NEXT/digest/queue/advance…),
# ⛔ **不是产物** —— 第一版没豁免它 ⇒ 报出一堆假散落(实测四个区都报)。
EXEMPT_DIRS = ("docs", "归档", "scripts", "脚本", "tmp", ".workbuddy", "done",
"__pycache__", "inbox")
# 🔴🔴 2026-10-04 **第三版才算对**:`imp()` 及其它用例会**永久改写** `DSH_COLLAB_WS`
# 指向**测试夹具**且不还原(实测 `selftest.py:184`)⇒ **跑到本用例时读它必是夹具**。
# ⇒ 真工作区只能用**模块加载时定下的 `WS`**(第 57 行,在 env 被改之前取的)。
# 📌 纪律:**判据里凡要"真工作区",一律用 `WS`,⛔ 不许现读 `os.environ`**。
ws = Path(WS)
if not ws.is_dir():
return [("SKIP:没指到真工作区(设 `DSH_COLLAB_WS=<工作区>` 才会真跑)", True)]
res = []
# ① 目标文件夹存在
# 🔴🔴 2026-10-04 **前两版都算错(留档)**:
# ① 第一版:直接 `exec_module` 但没设 `COLLABD_CONFIG` ⇒ `INBOX` 回落 ⇒ 报「未命名目标」。
# ② 第二版:设了 env,但**加载的是 `HERE/collabd.py`(技能目录那份)** ⇒ 它的
# `_cfg_candidates()` 里 `DSH_COLLAB_WS` 已被 `imp()` 指向**测试夹具** ⇒ 又回落。
# ⚠️ 更根子的一条:判据里**改 env 后再 `exec_module`**,而 `selftest` 顶层
# 已经跑过 `imp()`(它把 env 设为夹具)⇒ **我的设值与它的设值打架,谁最后生效不确定**。
# ✅ **正解=开独立子进程**(⛔ 不与 selftest 的 env 抢):真区自己的副本 + 干净 env
# ⇒ 拿到的是**真磁盘读数**,这才是本条要验的东西(同 P0-39:判据不许读错世界)。
import subprocess as _sb3
_own = ws / ".workbuddy" / "collab" / "collabd.py"
if not _own.is_file():
return [("⛔ 该工作区没有自己的 collabd.py 副本 ⇒ 判据无法验真实部署", False)]
_py = sys.executable
_code = (
"import importlib.util,sys,json\n"
"sp=importlib.util.spec_from_file_location('c',%r)\n"
"m=importlib.util.module_from_spec(sp)\n"
"try: sp.loader.exec_module(m)\n"
"except SystemExit: pass\n"
"print(json.dumps({'inbox':str(m.INBOX),'dir':m.goal_dir_rel()},ensure_ascii=False))\n"
% str(_own))
_env = {"COLLABD_CONFIG": str(ws / ".workbuddy" / "collab" / "collabd.config.json"),
"COLLABD_WORKSPACE": str(ws)}
try:
_r = _sb3.run([_py, "-c", _code], env={**os.environ, **_env},
capture_output=True, timeout=60, cwd=str(ws))
_j = json.loads((_r.stdout or b"{}").decode("utf-8", "replace") or "{}")
except Exception as _e:
return [("⛔ 独立子进程取真值失败:%r" % _e, False)]
_gdir = ws / (_j.get("dir") or "")
res = [("目标文件夹存在:%s(独立进程按 `goal.json` 算出,INBOX=%s)"
% (_j.get("dir"), _j.get("inbox")), bool(_j.get("dir")) and _gdir.is_dir())]
# ② 产物型 md 落点
# 🔴🔴 2026-10-05:目标文件夹收进 `执行会话/` 一层(用户定案「<工作区根>/执行会话/目标-xxx/ 改成这样」)
# ⇒ 落点判据必须**跟着机制走**(同族第 N 次:判据没跟上改动 ⇒ 假红/假绿)。
# ⚠️ 判据从「`parts[0].startswith('目标-')`」扩成**两级**:
# `执行会话/目标-xxx/…` 与**裸 `目标-xxx/…`(存量旧目录)都要认**
# —— ⛔ 硬切到新形态会把历史产物全判成"散在别处"(那是**误报**,不是事实)。
# 📌 只认 `执行会话/` 下一层的 `目标-*`,⛔ 不认 `执行会话/` 里别的杂物
# (否则往 `执行会话/` 随便丢个 md 就能骗过判据)。
stray, inside = [], 0
for p in ws.rglob("*.md"):
rel = p.relative_to(ws)
parts = rel.parts
if parts[0] in EXEMPT_DIRS or str(rel) in RULE_FILES:
continue
if p.name in RULE_FILES or p.name.startswith("CODEBUDDY"):
continue
# 只看「顶层 目标-*|执行会话/目标-*|交付物|其他一层目录」这一层,⛔ 不递归进目标文件夹内部
if len(parts) > 1 and parts[0].startswith("目标-"):
continue
if (len(parts) > 2 and parts[0] == "执行会话" and parts[1].startswith("目标-")):
continue
if len(parts) == 1:
stray.append(str(rel)); continue
if parts[0] == "交付物":
inside += 1; continue
stray.append(str(rel))
res.append(("产物型 md 都落在「执行会话/目标-*/ 或 目标-*/ 或 交付物/」内(⛔ 代码/规则/归档除外);"
"已在目标文件夹或交付物=%d,散在别处=%d %s"
% (inside, len(stray), ("→ " + str(stray[:5])) if stray else ""),
not stray))
return res
@case("🔴🔴 `ensure_supervise` spawn 孙进程**必须传 `COLLABD_CONFIG`**(否则起后即退)")
def t_ensure_passes_config_to_child():
"""🔴 2026-10-04 **实测坐实**(`vibe-product` 停 16.4 h 的真因)。
症状:`--ensure` 报 `{"ok": false, "spawned": true}` ⇒ **看起来起好了,其实秒退**;
`supervise.out.log` 里只有 spawn 记录、无异常;`tasklist` 查 pid **已不在**。
病根(`collabd.py::ensure_supervise()` 的 `subprocess.Popen`):
· `cwd` =**副本目录**(`<ws>/.workbuddy/collab`)
· `load_cfg()` 找配置顺序=`COLLABD_CONFIG` → **`<cwd>/.workbuddy/collab/collabd.config.json`**
· ⛔ **spawn 时没传 `env`** ⇒ 孙进程拿不到 `COLLABD_CONFIG`
⇒ 回落 `<副本目录>/.workbuddy/collab/…` ⇒ **不存在** ⇒ `CFG_MISSING` ⇒ 秒退。
⚠️ 为什么难查:`spawned=True` + 日志无异常 + 我**自己也用这个有缺陷的 `--ensure` 去补起**
⇒ 它报"起好了"我信了 ⇒ 一次也没验到真活(今天第 N 次「工具报成功我照抄」)。
"""
src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
R = []
R.append(("`ensure_supervise` 的 `Popen` **带 `env=`**(⛔ 不传 ⇒ 孙进程丢 `COLLABD_CONFIG`)",
re.search(r"Popen\(\[[^\]]*PYW[^\]]*--supervise[^\]]*\][\s\S]{0,400}?env=_env",
src) is not None))
R.append(("`env` 取自**真来源全局 `CFG_USED`**(⛔ 不是 `C` 里的键 —— `C` 是配置内容字典)",
re.search(r'if not _env\.get\("COLLABD_CONFIG"\) and CFG_USED:', src) is not None))
R.append(("⛔ 代码里**没有编造的 `_path_used` 键**(我第一版就写了它 ⇒ 取不到 ⇒ 修复等于没做)",
"_path_used" not in src))
R.append(("源码里留着**「起后即退」的成因注释**(⛔ 防后人再踩)",
"起后即退" in src and "CFG_MISSING" in src))
# 行为判据:⛔ 真起一条,看它**活过第一轮**(⛔ 不看返回的 ok/spawned,那些不可信)
try:
import importlib.util
env2 = dict(os.environ)
env2["COLLABD_CONFIG"] = str(HERE.parent.parent).replace("\\", "/")
_sp = importlib.util.spec_from_file_location("_cd_ens", HERE / "collabd.py")
m = importlib.util.module_from_spec(_sp); _sp.loader.exec_module(m)
import time as _t
_t.sleep(0.3)
cfg_src = m.CFG_USED
R.append(("本进程真读到配置(`CFG_USED` = %s)⇒ 有得传给孙进程"
% (os.path.basename(str(cfg_src)),), bool(cfg_src) and not m.CFG_MISSING))
except Exception as e:
R.append(("⛔ 读 collabd 失败:%r" % (e,), False))
return R
@case("🔴 钩子真接上了:工作区从 stdin.payload.cwd 取(⛔ 不是 DSH_WS_ROOT)")
def t_hook_reads_ws_from_stdin_cwd():
"""🔴🔴 2026-10-04 用户问「为什么 vibe 会话说技能没有调整」时挖出的**第二个**真缺陷。
病根链(三层,每层都"看起来没问题"):
① 我上一版 `supervise-ensure-hook.py` 只读 `os.environ["DSH_WS_ROOT"]`;
② `settings.json` 的 hook 条目**没有 env 字段** ⇒ 宿主 env 里该变量
**实测三级全空**(Process/User/Machine)⇒ 第①步就 `_noop()` 返回;
③ 而它的表现是「**零输出、rc=0**」—— 与"正确沉默"**完全一样**
⇒ 我据此报了"钩子已挂上、修好了",**它一次都没生效过**。
⚖️ 同源判据的既有正确写法(⛔ 必须对齐,不能自创):
`wb-result-hook.py` / `lock-guard-hook.py` 都是**先读 stdin 的
`payload.cwd`**,env / `roots.env` 只作兜底。
🔴 判据要点:⛔ **不许只查"读没读 cwd"**——还要钉住
① stdin **只读一次**(`sys.stdin.buffer.read()` 是一次性的)
② `os.environ` 必须被**真改写**(`env = dict(os.environ)` 对
`exec_module` 里的 `load_cfg()` **无效**)
③ 加载后必须**核对 `CFG_USED` 真的指向本区**,否则会验错对象。
"""
H = HERE / "hooks" / "supervise-ensure-hook.py"
R = []
R.append(("钩子文件存在", H.is_file()))
if not H.is_file():
return R
src = H.read_text(encoding="utf-8", errors="replace")
R.append(("🔴 主路径读 **stdin 的 `payload.cwd`**(⛔ 不能只读 `DSH_WS_ROOT`)",
'get("cwd")' in src and "_stdin_payload" in src))
R.append(("`DSH_WS_ROOT` 只作**兜底**(⛔ 不能当唯一来源)",
'_ws_root() or (os.environ.get("DSH_WS_ROOT")' in src))
R.append(("🔴 **stdin 载荷有缓存**(⛔ `sys.stdin.buffer.read()` 一次性,"
"读两次第二次必空 ⇒ 零输出假绿)",
"_PAYLOAD" in src and "if _PAYLOAD is None" in src))
R.append(("🔴 `COLLABD_CONFIG` 改的是 **`os.environ`**(⛔ `env = dict(...)` "
"对 `exec_module` 无效 ⇒ `load_cfg()` 读真进程环境)",
'os.environ[cfg_env_key] = _cfg_path' in src))
R.append(("🔴 加载后**核对 `CFG_USED` 指向本区**(⛔ 不核对=验错对象=假绿)",
"CFG_USED" in src and "_used" in src))
R.append(("起不来时**必须报出来**(10-04 用户原话「起不来就先报错」,⛔ 不许静默)",
"常驻没起来" in src and "先修好这个再往下做" in src))
R.append(("`--ensure` 参数名正确(⛔ 没有 `--ensure-supervise`)",
'"--ensure"' in src))
# 🔴🔴 2026-10-04 改判据:原来钉的是 `"capture_output=True, timeout=40"`
# ⇒ 收超时隐患(40→15)后**判据自己红了**,成了"守着旧值"的绊脚石。
# ⛔ 判据的职责是**守住"不看返回码"这个口径**,⛔ 不是守住某个超时数字。
# ✅ 正解=判**形状**:`subprocess.run(...)` 的实参里**没有** `check=`,
# 且结果**没被赋给任何变量/没取 `.returncode`**(=真的没看返回码),
# 同时回验走 `supervise_alive()`(唯一可信判据)。
# ⚠️ 这样收超时值(乃至换回验次数)都⛔ 不会再假红。
import re as _re_en
_m_en = _re_en.search(r"subprocess\.run\(\[PY, str\(COLLABD\), \"--ensure\"\](.*?)\)\n",
src, _re_en.S)
_args = _m_en.group(1) if _m_en else ""
R.append(("⛔ 不看 `--ensure` 的返回码(⛔ `spawned:true` 会骗人 ⇒ 等活再问)",
bool(_m_en)
and "check=" not in _args
and "returncode" not in src.split("_ENSURE_SUBPROC_TIMEOUT")[-1][:1200]
and "supervise_alive()" in src))
# —— 行为判据(⛔ 这才是能分辨 A/B 的那一条)
R.append(("⛔ 行为:**喂真 payload(`cwd` 指向一个只有 cfg+goal 的合成区)"
"⇒ 必须注入「已自动补起」", *_hook_behaves()))
return R
@case("🔴 T 表 §1.6:起不来先报错(10-04 用户原话「必须首先启动好在执行」)")
def t_t16_stop_when_supervise_fails():
"""🔴 这条红线**当天说了两次没落进技能** ⇒ 补上并钉住。
用户原话:「**加个规则 后台任务和检查程序如果不能启动,就先报错解决了问题
在执行完成目标的任务**」「**必须首先启动好在执行**」。
⚠️ 之前只停在**工作区日志**里(⛔ 换会话读不到 = 没立)。
"""
S = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace")
R = []
R.append(("SKILL.md 有 **§1.6 起不来先报错** 这一节",
"1.6 起不来 = **先报错、先解决,再执行目标**" in S))
R.append(("逐字锚用户原话「必须首先启动好在执行」",
"必须首先启动好在执行" in S))
R.append(("写明**顺序**(在位才允许往下 / 不在位先起)",
"在位" in S and "才允许往下建会话" in S))
R.append(("写明**起不来时三条不许**(不许边起边干/不许只说起好了/不许编原因)",
"不许\"边起边干\"" in S or "不许“边起边干”" in S
or "边起边干" in S))
R.append(("写明**三条边界**(没目标不起也不报/只管在位这一件事/判据只有 pid∧心跳)",
"基础会话方式" in S and "只在位这一件事" in S.replace(",", ",")
or "只管「常驻在不在位」这一件事" in S))
R.append(("指明**由钩子执行**、⛔ 不是靠我记得(能力已接线)",
"supervise-ensure-hook.py" in S and "不是靠我记得" in S))
return R
def _hook_behaves():
"""真喂 payload 跑一次钩子,验它**看得见工作区**(不是只查代码文本)。
🔴 为什么必须真跑:本次两个缺陷**代码文本全对**(第一版也确实读 `cwd`),
栽在「stdin 读两次」+「env 没进真进程」⇒ **纯文本判据会假绿**。
⚠️ 用**合成区**而不是真实区:真实两区常驻都在位 ⇒ 零输出,
而零输出同时可能是「看见了+在位」或「⛔ 根本没看见」⇒ 分不出来。
"""
import shutil
import subprocess
import tempfile
PY = sys.executable
H = HERE / "hooks" / "supervise-ensure-hook.py"
ws = Path(tempfile.mkdtemp(prefix="sm_hooktest_"))
try:
(ws / ".workbuddy" / "collab" / "logs").mkdir(parents=True, exist_ok=True)
(ws / "tmp" / "supervise-inbox").mkdir(parents=True, exist_ok=True)
(ws / ".workbuddy" / "collab" / "collabd.config.json").write_text(
json.dumps({"schema": 1, "workspace": str(ws)}), encoding="utf-8")
(ws / "tmp" / "supervise-inbox" / "goal.json").write_text(
json.dumps({"short": "hooktest", "title": "hook selftest",
"lifecycle": "进行中", "topics": ["自检"]}), encoding="utf-8")
pl = json.dumps({"cwd": str(ws).replace("\\", "/"),
"prompt": "继续完成目标"}, ensure_ascii=False)
p = subprocess.run([PY, str(H)], input=pl.encode("utf-8"),
capture_output=True, timeout=90)
out = p.stdout.decode("utf-8", "replace")
ctx = ""
try:
ctx = (json.loads(out).get("hookSpecificOutput") or {}).get("additionalContext") or ""
except Exception:
pass
ok = "已自动补起" in ctx
# 收尾:合成区起出来的常驻必须收掉,⛔ 不留进程
try:
hb = ws / ".workbuddy" / "collab" / "logs" / "supervise-heartbeat.json"
pid = json.loads(hb.read_text(encoding="utf-8-sig")).get("pid")
if pid:
subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"],
capture_output=True, timeout=30)
except Exception:
pass
return (ok,)
except Exception as e:
return ("⛔ 行为判据跑不起来:%r" % (e,),)
@case("🔴 T 表:状态 → 我该做什么(2026-10-04 用户口述 · ⛔ 不许 AI 推断)")
def t_state_to_action_table():
"""🔴 这条是**「一天栽三次」的直接对策**。
2026-10-04 我三次把**自己推断的**东西当成需求/欠项:
① 「开机自启/计划任务」⇒ 用户:「从来没说过什么开机自启」
② 「三类会话要建齐」 ⇒ 那口径 10-02 就作废了
③ 「两类会话要建齐」 ⇒ 现行是「要落事才显式建」
三次的共同根因:**技能只有「你要干什么 → 去看哪篇」(知识组织),
没有「现在什么状态 → 下一步做什么」(行动组织)**
⇒ 查到一条知识就以为「知道该做什么了」。
✅ 解法=把这张表**落进 SKILL.md**(⛔ 落在 `references/` 里没人先看到),
并且**三条红线里最要紧的那条必须钉死**:不许把「实体里存在」读成「该有」。
"""
S = (HERE.parent / "SKILL.md").read_text(encoding="utf-8", errors="replace")
R = []
R.append(("SKILL.md 有 **T 表**(状态 → 我该做什么)",
"T 表 · 状态 → 我该做什么" in S))
R.append(("ⓘ 写明它**存在的原因**(⛔ 防后人当成又一份知识文档删掉)",
"知识组织" in S and "行动组织" in S))
# —— 逐字锚用户原话(⛔ 今天的第 12 次教训:锚点必须逐字,⛔ 不能凭记忆重写)
R.append(("写明**会话生命周期**(用户原话逐字:主会话才建一轮,后续无意外走检查进程)",
"才建立一轮执行会话" in S and "后续无意外都是 检查进程创建" in S))
R.append(("写明**只有第①步是主会话建任务会话**,②主会话收手",
"只有这一步是「主会话建执行会话」" in S and "主会话到这一步就收手" in S))
# —— 「这轮结束了」的判据
R.append(("写明**靠投递判完成**(用户原话:执行会话完成会往检查程序队列投递)",
"向检查程序队列投递执行情况" in S))
R.append(("写明 **`done` 必须带 `--artifact`**(10-03 用户逐字要求,⛔ 拒收无产物的 done)",
"必须**带 `--artifact`**" in S or "必须带 `--artifact`" in S))
R.append(("写明**台账是唯一权威** + ⛔ 别读快照"
"(⚠️ `acceptance_state` 记着 10-02 的 pid 8024,实际已是 19424)",
"唯一权威" in S and "8024" in S))
# —— 检查会话不能固定席位
R.append(("写明**检查会话不能固定席位**(用户原话:没办法固定,有记录吗)",
"没办法固定" in S or "不许预先摆一条「固定检查会话」" in S))
# 🔴 钉**唯一位置**(T 表那一行),⛔ 不全文搜两个词
# —— 旧写法 `台账空 in S and 不许**建任务会话 in S` 被变异 V3 打不穿:
# 改表格那一行,两处关键词在**别处**(§1 甘特图行)仍命中 ⇒ 恒绿。
import re as _re5
_row = [ln for ln in S.splitlines()
if ln.strip().startswith("| 台账空")]
R.append(("⛔ T 表「台账空」那一行写明**不许建任务会话**(⛔ 不许造需求;"
"⚠️ 必须钉**这一行**,⛔ 全文搜会被别处的同词打穿)",
bool(_row) and "不许" in _row[0] and "建任务会话" in _row[0]))
# —— 🔴 前置条件层(2026-10-04 用户补充:「如果主会话下没有 后台任务 或
# 检查程序 还需要启动常驻程序」⇒ 三层串起来,⛔ 不许跳过中间那层)
R.append(("写明**三层是串起来的**(常驻 → 检查会话 → 那一轮任务会话)",
"1.5 前置条件" in S and "唯一载体" in S))
R.append(("写明**常驻是「建检查会话」那条腿的唯一载体**(⛔ 跳层=建了也接不上)",
"没有常驻 ⇒ 检查会话永远建不出来" in S))
R.append(("写明**真顺序 0-a/0-b/0-c**(⛔ 顺序不能颠倒)",
"0-a" in S and "0-b" in S and "0-c" in S))
R.append(("0-a 量的是 **`supervise_alive()`**(pid 活 ∧ 心跳新鲜)⛔ 不看快照",
"supervise_alive()" in S and "心跳新鲜" in S))
# 🔴🔴 2026-10-04 改:闸① **收窄**(用户口径「检查程序必然要去处理队列」)
# ⇒ 旧断言「闸① 生命周期必须「进行中」」**已经过时**,不改它会**红着掩盖新口径**。
# ✅ 新断言=钉**收窄后的口径**:只对 `queue-empty` 生效,`sessions-ended` 不受目标状态限制。
R.append(("0-b 点明**闸① 只对 `queue-empty` 生效**(代码五道闸之①,2026-10-04 收窄)",
"进行中" in S and "`queue-empty` 那条不动" in S))
R.append(("⛔ 写明**跳过常驻直接建会话 = 接不上**(检查会话只能靠排期开)",
"跳过常驻直接建会话" in S))
# 🔴 钉**状态表那两行**(唯一位置),⛔ 不全文搜
# —— 「基础会话方式」全文出现 3 处,V1 变异只改一处 ⇒ 全文搜**恒绿**(今天第 16 次)
_r0b = [ln for ln in S.splitlines()
if ln.strip().startswith("| **0-b**")]
_r0t = [ln for ln in S.splitlines()
if ln.strip().startswith("| **没有目标**")]
R.append(("📌 **0-b 那一行**写明「没有目标 ⇒ 基础会话方式,⛔ 那不是故障」"
"(⛔ 必须钉**这一行**)",
bool(_r0b) and "基础会话方式" in _r0b[0] and "不是故障" in _r0b[0]))
R.append(("📌 **状态表「没有目标」那一行**判它是**正常**(⛔ 什么都用不起)"
"(⛔ 钉**这一行**)",
bool(_r0t) and "正常" in _r0t[0] and "什么都不用起" in _r0t[0]))
R.append(("⛔ 状态表里「**没有目标**」那一行写明**⛔ 不许说成缺目标/缺配置**",
"不许**说成「缺目标/缺配置」" in S or "不许**说成「缺目标" in S))
# —— 🔴 状态表里「**目标已完成**」那一档(10-04 补:原来表里只有进行中/等待/没有目标)
# ⛔ 必须**钉那一行**(唯一位置)—— 全文搜「已完成」会命中几十处 ⇒ 恒绿
_rdone = [ln for ln in S.splitlines()
if ln.strip().startswith("| **目标 `lifecycle=已完成`**")]
R.append(("🔴 状态表有「**目标 lifecycle=已完成**」那一档,且写明**含带后缀**形态"
"(⛔ 钉这一行)",
bool(_rdone) and "带后缀" in _rdone[0] and "已完成(机器可判部分)" in _rdone[0]))
# 🔴🔴 2026-10-04 改:旧断言写「**⛔ 不再建任何检查会话**(闸①不过=设计)」——
# 那句「这是设计,不是故障」正是**被用户当场否掉**的结论(P0-56)⇒ 断言必须跟着翻。
# ✅ 新口径=**先看台账**:还有未完成件 ⇒ 照建 `[结果检查]`;全 done/空 ⇒ 收工。
R.append(("🔴 那一档写明 **先看台账**:还有未完成件 ⇒ **照建 `[结果检查]`**(闸① 不拦它)",
bool(_rdone) and "还有活没人干" in _rdone[0] and "[结果检查]" in _rdone[0]))
R.append(("🔴 那一档写明 **全 done/空 ⇒ 收工**(⛔ 不再一律收工 —— 那是旧口径)",
bool(_rdone) and "收工" in _rdone[0] and "不再建任何检查会话" not in _rdone[0]))
R.append(("🔴 那一档写明 **⛔ 不许把「已完成」说成「等待」**(语义相反,实测栽过)",
bool(_rdone) and "语义相反" in _rdone[0]))
# —— 🔴 0-b 那条硬约束:**读数必须问代码,⛔ 不许肉眼读 goal.json 原文**
R.append(("🔴 写 0-b 时**必须问代码**(⛔ 不许肉眼读 `goal.json` 原文比对)——"
"正是被带偏的那个入口",
"不要自己读 `goal.json` 的原文再肉眼比对" in S))
R.append(("⛔ 明写**⛔ 不许我代他设目标**(想让它有 ⇒ 用户开)",
"不许我代设目标" in S or "不许**代他设目标" in S))
# —— 行为判据:现读本区 lifecycle,验闸① 真的过不了
try:
import json as _j2
gp = WS / "tmp" / "supervise-inbox" / "goal.json"
life = None
if gp.is_file():
life = str((_j2.loads(gp.read_text(encoding="utf-8")) or {}).get("lifecycle") or "")
# 🔴 验的是「**读数可判**」,⛔ **不预设它该是什么** ——
# 原写法硬编码 `life != "进行中"`(那是**本区当时**的状态 None)
# ⇒ 同一份判据在副本区(lifecycle='进行中')报红 ⇒ **验错对象**(今天第 15 次)。
# 🔴🔴 口径订正(用户 2026-10-04:「没有目标就说明 当前用户是用的是基础会话方式」):
# ⛔ **不许**把「非进行中」写成需要修的缺口 —— 那不是故障,是**用户在用基础会话方式**。
# ⇒ 判据只验「**读得到、且能据此判**」,⛔ 不断言「本区该不该有目标」。
R.append(("现读本区 `lifecycle`(=%r)—— 能据此判「是不是基础会话方式」即可"
"(⛔ **不预设该是什么**)" % life,
# 🔴🔴 2026-10-04 修:原来是一份**枚举白名单**,结果 `vibe-product`
# 的 `'已完成(机器可判部分)'`(**带后缀**)不在名单里 ⇒ 副本区报红。
# ⛔ 那是**判据自己枚举不全**,不是真故障 —— 又一次「验错对象」。
# ✅ 正确判法=只验「**读得到、且非空或明确为 None**」,
# ⛔ 不断言它是哪个值(值由各区用户自己定,机制无权预设)。
(life is None) or (isinstance(life, str))))
except Exception as e:
R.append(("⛔ 读 lifecycle 失败:%r(**不假装通过**)" % (e,), False))
# —— 🔴🔴 行为判据:`goal_life()` 必须**剥括号后缀再判**(2026-10-04 真缺陷)
# 病根(`vibe-product` 实测):`goal.json` 写 `"已完成(机器可判部分)"`,
# 而 `goal_life()` 只做**精确匹配** ⇒ 静默回落成 `等待`
# ⇒ 「已完成」被读成「等待」(**语义相反**)⇒ 日志/主会话全被带偏。
# ⚠️ 这条判据**必须在真模块上跑**(⛔ 不做文本 grep):
# 文本判据只能证明"写了 split('(')",证明不了"真的能读出 已完成"。
try:
# ⚠️ 本函数后面还有 `import json`(局部)⇒ 在它之前用 `json` 会 `UnboundLocalError`
# ⇒ 这里**显式局部导入**,⛔ 不依赖外层作用域(实测就是这么挂掉的)。
import json as _j_gl
import re as _re_life
def _load_mod():
import importlib.util as _iu
_sp = _iu.spec_from_file_location("cb_life", HERE / "collabd.py")
_mm = _iu.module_from_spec(_sp)
_sp.loader.exec_module(_mm)
return _mm
# ① 代码里确有剥后缀(文本层,只作辅助)
_src = (HERE / "collabd.py").read_text(encoding="utf-8")
R.append(("🔴 `goal_life()` 有**剥括号后缀**的分支(全角/半角各一条)",
"(" in _src and '.split("(", 1)[0]' in _src
and '.split("(", 1)[0]' in _src))
# ② 行为层:直接喂三种带后缀的字面量,断言读出**标准值**
# ⚠️ 用**临时改 goal.json 再还原**(字节级),⛔ 不猜、不 mock
# 🔴🔴 **必须改"本模块实际读的那个文件"**,⛔ 不许拿常量硬拼凑路径。
# 2026-10-04 实测两个坑(两个都栽过,判据因此**假红**过一整轮):
# 坑1:写 `WS / "tmp" / "supervise-inbox"` —— `WS` 是**跑 selftest 的那个区**
# (`DSH_WS_ROOT` 或 `cwd`),⛔ 不是被检模块的区。
# 坑2:写 `Path(_m_probe.INBOX) / "goal.json"` —— 看着"从模块取"很稳,
# 但 `INBOX` 是**模块加载那一刻算出来的默认值**:
# 没配 `COLLABD_CONFIG` 时它回落成 `<技能目录>/tmp/supervise-inbox`
# (见实测:`scripts/tmp/supervise-inbox/goal.json`,`is_file=False`)
# ⇒ 写进去的是**没人读的空目录**,`goal_life()` 照样读不到 ⇒ 永远 `等待`
# ⇒ **判据假红**(测的是"文件不存在"这个副作用,⛔ 不是被测行为)。
# ✅ 正解=**用与被测函数同一条路径公式**:
# `路径 = Path(root) / "tmp" / "supervise-inbox" / "goal.json"`,
# 而 `root` 取 `WS`(模块自己算出来的区)。
# ⛔ 这样即使实现改了公式,也是**实现与判据一起改**(同步暴露),
# 不会出现"判据偷偷指向第三处"的假红/假绿。
_m_probe = _load_mod()
_gp = Path(_m_probe.WS) / "tmp" / "supervise-inbox" / "goal.json"
_gp.parent.mkdir(parents=True, exist_ok=True)
_bak = _gp.read_bytes() if _gp.is_file() else None
try:
_base = _j_gl.loads(_bak.decode("utf-8")) if _bak else {}
_cases = [("已完成(机器可判部分)", "已完成"),
("已完成(机器可判部分)", "已完成"),
("进行中(第2棒)", "进行中"),
("已完成", "已完成")]
_okall = True
_det = []
for _lit, _want in _cases:
_base["lifecycle"] = _lit
_gp.write_bytes(_j_gl.dumps(_base, ensure_ascii=False,
indent=2).encode("utf-8") + b"\n")
_m_gl = _load_mod() # 重新加载,⛔ 不靠缓存
_got = _m_gl.goal_life()
_det.append("%s→%s" % (_lit, _got))
if _got != _want:
_okall = False
R.append(("🔴 **行为**:带后缀字面量必须读成标准值(%s)" % ";".join(_det),
_okall))
finally:
if _bak is not None:
_gp.write_bytes(_bak) # 字节级还原(⛔ 不碰行尾)
# ③ ⛔ 认不出时**必须留痕**(不许静默回落 —— 否则"读错"与"本来没有"长得一样)
R.append(("⛔ `goal_life()` 认不出字面量时**写日志**(⛔ 不许静默回落)",
"目标状态字面量认不出" in _src
and "log_throttled(" in _src))
# ④ 🔴🔴 **单一事实源**:判定算法只许有一份(用户口径「禁止重复判定标准」)
# 病根(2026-10-04 实测):`peer_supervise_sweep()` 自己抄了一遍
# `str(g.get("lifecycle")).strip()` + 精确比较(**不剥后缀**)
# ⇒ 与 `goal_life()` 对**同一个文件**给出**两个答案**。
# ✅ 正解=抽 `goal_life_of(root)`,`goal_life()` 只作薄封装。
# 判据=① 存在 `goal_life_of(root)` 定义;② `goal_life()` 的函数体**不含**
# `get("lifecycle")`(自己判就是又抄了一遍);③ 跨区扫描**调** `goal_life_of(`
# 而**不再**自己读 `lifecycle`。
_src_life = _src # 同一份源码
_body_gl = _re_life.search(r"^def goal_life\(\) -> str:(.*?)(?=^def )",
_src_life, _re_life.S | _re_life.M)
_body_sweep = _re_life.search(r"^def peer_supervise_sweep\((.*?)(?=^def )",
_src_life, _re_life.S | _re_life.M)
def _code_only(txt: str) -> str:
"""⛔ 只留**代码行**(去掉整行注释)。
⚠️ 必须这么做:本文件的注释里**大量引用**旧写法当反面教材
(如「原来这里自己判 `get("lifecycle")`」)⇒ 裸串搜会把注释当代码
⇒ 判据**假红**(实测:`peer_supervise_sweep` 那条就这么红的)。
"""
return "\n".join(ln for ln in txt.splitlines()
if not ln.lstrip().startswith("#"))
R.append(("🔴🔴 **单一事实源**:判定算法只有 `goal_life_of(root)` 一份"
"(⛔ `goal_life()` 只作薄封装、自己不许再读 `lifecycle`)",
_re_life.search(r"^def goal_life_of\(root\) -> str:", _src_life, _re_life.M)
is not None
and bool(_body_gl)
and 'get("lifecycle")' not in _code_only(_body_gl.group(1))))
R.append(("🔴 跨区扫描 `peer_supervise_sweep()` **调** `goal_life_of(`"
"(⛔ 不许自己抄一遍 lifecycle 判定)",
bool(_body_sweep)
and "goal_life_of(" in _code_only(_body_sweep.group(1))
and 'get("lifecycle")' not in _code_only(_body_sweep.group(1))))
except Exception as e:
R.append(("⛔ 跑 `goal_life()` 后缀判据失败:%r(**不假装通过**)" % (e,), False))
# —— 三条红线
R.append(("红线①**不在表里的状态 ⇒ 问用户**(⛔ 不许推断后当需求)",
"凡不在上表的状态 ⇒ 问用户" in S))
R.append(("红线②**「实体里存在」⛔ 不等于「该有」**(当天栽的就是这条)",
"不等于「该有」" in S))
R.append(("红线③**⛔ 不许把推断摊成欠项/风险/待办**(凭空造需求)",
"不许把自己以为该有的东西摊成「欠项」" in S))
R.append(("红线③里**留了当天三次的实证**(开机自启/三类/两类 ⇒ ⛔ 空头红线不算数)",
"开机自启" in S and "三类会话" in S and "两类会话" in S))
# —— 行为判据:拿真台账验「这轮结束了没有」能被判出来
import json
# 🔴 路径**实测**得来,⛔ 不凭猜:测试台账在 `TEST_WS/inbox/`(`inbox` =配置键名);
# ⛔ `supervise-inbox/` 是**生产**工作区的落点(今天凭猜写这个 ⇒ FileNotFoundError)
tk = TEST_WS / "inbox" / "tasks.json"
try:
d = json.loads(tk.read_text(encoding="utf-8"))
ts = d if isinstance(d, list) else (d.get("tasks") or [])
states = [str(t.get("state") or t.get("status") or "") for t in ts]
R.append(("真读台账**判得出「这轮结束了没有」**(`tasks.json` 可解析,%d 条)" % len(ts),
True))
R.append(("台账里 **`done` 必带 `artifact`**(⛔ 判据:空的 done = 假完成)",
all(t.get("artifact") for t in ts
if str(t.get("state") or t.get("status")) == "done") or
"done" not in states))
except Exception as e:
R.append(("⛔ 台账读不出(%r)⇒ 这一条无法验(**不假装通过**)" % (e,), False))
return R
@case("📇 文档索引:改流程/改机制前**必读三篇**在第一屏 + 文档四条规则在位")
def t_doc_index():
"""🔴 落它的起因(用户原话):「**实在文件多看不过来 就建立一个索引**」。
⚠️ **不是"整理归档",是"防复发"**:10-03 立的「⛔ 写『会怎样』前先取证那个后果」
只躺在**工作区日志**(197 KB)⇒ 换会话读不到 ⇒ 10-04 又编了一次后果。
⇒ 索引的真正作用:**让"下次必须做到的事"落在会被自动加载的地方**。
📌 本判据量的四件事(⛔ 少一件都等于索引没建):
① 索引档在位 ② SKILL.md **第一屏**挂「必读三篇」 ③ 文档四条规则逐字在位
④ ⛔ 别让它烂掉(新文档**必须**登记在索引里 —— ⛔ 没登记=别人找不到=白写)
"""
G = HERE.parent
IDX = G / "references" / "01-文档索引.md"
R = []
R.append(("📇 索引档在位 `references/01-文档索引.md`", IDX.is_file()))
if not IDX.is_file():
return R
idx = IDX.read_text(encoding="utf-8", errors="replace")
skill = (G / "SKILL.md").read_text(encoding="utf-8", errors="replace")
# ① 必读三篇:⛔ 必须是**三篇具体文件名**(⛔ 写"相关文档"等于没写)
for f in ("00-动手前必过.md", "rules.md", "manifest.md"):
R.append(("索引里点名必读 `%s`" % f, f in idx))
R.append(("索引里有**「改流程/改机制前」这个入口**(⛔ 索引最常见的用法就是它)",
"改流程" in idx and "改机制" in idx))
R.append(("⛔ 明写**别通读**(⛔ 索引的意义就是不用全看)", "别通读" in idx))
# ② SKILL.md **第一屏**:⛔ 埋在下面=没人会看到(今天已经为「红线埋在末尾」栽过一次)
# 🔴 量法修正:⛔ **不用 `skill[:2000]` 切片** —— frontmatter 就 3000+ 字符
# ⇒ 索引明明在第 24 行却被判「不在第一屏」(**假红**,今天第 7 次同形状)。
# ⇒ 真「第一屏」=**标题之后的第一个非空块**。
_after = skill.split("# session-mechanism", 1)[-1].lstrip("\n")
head = _after[:1200]
R.append(("SKILL.md **第一屏**就有「改流程/改机制前先看这三篇」",
"改流程" in head and "00-动手前必过.md" in head))
R.append(("SKILL.md 第一屏**指向索引档**",
"01-文档索引.md" in head))
# ③ 文档四条规则(用户 10-04 逐字给定)—— 逐条在位
for key, why in (("分类索引", "新文档必须登记,否则白写"),
("结论在最前", "读者要的是现在是什么样,不是改了几轮"),
("新的在前面", "历史倒排:追加只能往前插"),
("简明扼要有效", "单条上限,防流水账")):
R.append(("四条规则之一「**%s**」在位(%s)" % (key, why), key in idx))
R.append(("⛔ 四条规则里**「单条 ≤6 KB」写明上限**(⛔ 只说「简明」没有上限=没约束)",
"6 KB" in idx))
R.append(("⛔ 明确**追加只能往前插,不许接在末尾**(历史倒排的可执行说法)",
"不许接在末尾" in idx))
R.append(("⚠️ 同时写着**判据要点一个不许丢**(⛔ 精简过头=假绿,比啰嗦更坏)",
"判据要点" in idx and "假绿" in idx))
# ④ 索引不许烂掉:⛔ 「存档不等于读得到」那节必须在(今天栽的根因)
R.append(("索引里写着**「存档 ≠ 读得到」**那节(10-04 实证的根因)",
"读得到" in idx))
R.append(("⛔ 明写「只写进工作区日志的规矩 = 没立」"
"(⛔ 这是今天复发的那一条的直接对策)",
"没立" in idx))
# 行为判据:⛔ 索引点名的每个 references 文件**必须真存在**(防"登记了但文件没了")
import re as _re3
# 🔴 正则**要认带不带 `references/` 前缀两种写法**(索引里多数写作 `00-动手前必过.md`)
# ⛔ 还要**修双写**:`r"\\w"` 在 Python 里是匹配**字面 `\w`**,⛔ 不成字符类
# (实测:写成 `\\w` ⇒ 匹配到 0 个 ⇒ 这条行为判据**空转恒绿**)
named = set(_re3.findall(r"`(?:references/)?([\w\u4e00-\u9fff\-]+\.md)`", idx))
bad = [n for n in named
if not ((G / n).is_file() or (G / "references" / n).is_file())]
R.append(("索引点名的 %d 个 references 文件**全部真存在**(⛔ 登记了但文件没了=索引在说假话)"
% len(named), named and not bad))
return R
@case("🔴 红线:⛔ 说「会怎样」前必须取证那个后果(10-03 立 · ⛔ 10-04 又犯)")
def t_no_invented_consequence():
"""🔴 这条是**跨会话翻车**的实证:
10-03 23:20 立了「⛔ 写『会怎样』前必须先取证那个后果真的存在」,
⛔ **但它只躺在工作区日志里**(`.workbuddy/memory/2026-10-03.md`,197 KB)
⇒ 换会话/换技能**读不到** ⇒ 10-04 我又编了一次
(说「常驻挂在会话进程树上、会话一关就被带走」;实测它连跑 24.2 h,
父进程已退出=**孤儿进程** ⇒ **不受会话结束影响**)。
⚠️ **真正的根因不是「忘了」,是「存档放错地方」**:
· 工作区日志 197 KB = 只有在那个工作区翻才看得到
· 技能(跨会话唯一会自动加载的)**当时没写**
· 技能仓库 **10-04 11:22 才首次入库** ⇒ 昨天成果压根没进版本库
⇒ 本判据量的就是「它**进技能了没有**」——⛔ 还在日志里就等于没存档。
"""
doc = (HERE.parent / "references" / "00-动手前必过.md").read_text(
encoding="utf-8", errors="replace")
R = []
import re as _re2
# 🔴 序号后面还跟着 emoji(`## ⓿ 🔴🔴 …`)⇒ 取**第一个字符**(⛔ 用 \S+ 会抓成 `⓿`+emoji 之外的整块)
_heads = [(m.start(), m.group(1)[:1]) for m in _re2.finditer(r"^##\s*(\S)", doc, _re2.M)]
_first = _heads[0][1] if _heads else ""
_red = doc.find("取证那个后果真的存在")
_red_sec = next((h for pos, h in _heads if pos < _red), "")
R.append(("红线在**动手层第一段**(⛔ 埋在末尾 = 不会先被看到)",
# 🔴 真量「红线**落在第几个标题区间**」⛔ 不比序号字符(变异实测两种写法都打不穿:
# ① 改标题序号、位置不动 ② 整段挪到末尾、序号不变 ⇒ 恒绿假判据,今天第 6 次同形状)
# ⇒ 判据:红线落在**第一个标题区间**内(`_heads[0][0] <= 红线 < 第二个标题起点`)
bool(_heads) and len(_heads) >= 2
and _heads[0][0] <= _red < _heads[1][0]))
R.append(("明写「编一个后果、说得更像回事」=**比原句更坏**",
# 🔴 量「**出现在第一段里**」⛔ 不量全文出现过(V2 变异实测:文档里有**两处**,
# 改一处另一处还在 ⇒ 恒绿 ⇒ 判据必须钉**唯一位置**)
_red > 0 and "说得更像回事" in doc[_red:_red + 700]
and "比原句更坏" in doc[_red:_red + 700]))
R.append(("明写**假情报**这个词(⛔ 后果会被当真话用)", "假情报" in doc))
R.append(("带**当天的实证**(24.2 h / 8695 轮 / 孤儿进程)⛔ 不留空头红线",
"24.2" in doc and "8695" in doc and "孤儿" in doc))
R.append(("点明「正确答案早写在文档里」+「把它读反了」"
"(⛔ 防的是「没查」与「读反了」两种)",
"早写在" in doc and "读反" in doc))
R.append(("第二条同源红线也在:**改到第三轮说不清就问要不要留**"
"(10-03 与 10-04 各撞一次)",
"第三轮" in doc and "这行本身就不该在" in doc))
R.append(("「报结论前」的清单**含那条取证问**(⛔ 收口时必看)",
"都有读数证明吗" in doc))
return R
@case("🔴 常驻「活多久」的判据:**存活时长 = 发起会话的存活时长**(⛔ 禁编造「活不过当轮」当后果)")
def t_supervise_lifespan_not_invented():
"""🔴🔴 2026-10-04 **当天栽的第 5 次同形状**(用户当场纠正:
「本会话 后台任务 和 程序 不是一直好好的 都跑了2天了」)。
我说了什么:`没载体 ⇒ 常驻挂在当前会话的进程树上 ⇒ 会话一关就被带走`。
🔴 **实测把它推翻**(不许反驳用户,得看数据):
· pid 19424 启动于 **10-03 17:05:36**,到 10-04 17:15 **连续活 24.2 h / 8695 轮**,心跳 4 s 前
· 它的 **父进程 pid 57216 早已不在进程表** ⇒ 它是**孤儿进程**
· 孤儿化之后**独立存活,⛔ 不受会话结束影响** ⇒ 我说的"会被带走"是**编造的后果**
⚠️ 讽刺的是**正确答案早就写在权威文档里**(`supervise-persistence.md` 第 30 行):
「⚠️ **它们现在活着,只因为发起它们的那条会话进程树至今没被回收**
⇒ 「**存活时长 = 发起会话的存活时长**」
⛔ 不能用它论证「这种起法也能长期」」
⇒ **我今天把这条读反了,还编出了它没说的后果。**
⚠️ 为什么单独立判据(⛔ 不是又写条纪律):
今天的两个根因(A 连接未验证/B 验证器自身失败被当结论)**都是"判据"层的**,
这条是**"结论"层的** ⇒ 它得有个自己的门:**凡要说「它会死/活不过」,必须先验父链。**
"""
P = (HERE.parent / "references" / "supervise-persistence.md")
doc = P.read_text(encoding="utf-8", errors="replace")
R = []
# ① 权威文档里那条**实测判据**必须在位(⛔ 它是今天唯一推翻我编造的依据)
R.append(("权威文档写着「**存活时长 = 发起会话的存活时长**」"
"(⛔ 这是唯一能推翻「活不过当轮」的实测依据)",
"存活时长 = 发起会话的存活时长" in doc))
R.append(("文档明写「它们现在活着,只因为…会话进程树至今没被回收」"
"(⛔ 承认**活着的因**,而不把「活着」当成「这种起法可行」)",
"至今没被回收" in doc))
R.append(("文档⛔ 明写「不能用它论证这种起法也能长期」"
"(⛔ 防的正是我今天犯的推断)",
"不能用它论证" in doc))
# ② 🔴 真实验一次**父链**(⛔ 断言必须建立在现读数据上,⛔ 不许复述文档)
import json
import time
hb = None
for cand in (Path(os.environ.get("DSH_WS_ROOT") or ".") / ".workbuddy" / "collab" / "logs"
/ "supervise-heartbeat.json",):
try:
if cand.is_file():
hb = json.loads(cand.read_text(encoding="utf-8"))
break
except Exception:
pass
if not hb:
return R + [("⛔ 本区没有心跳文件 ⇒ 父链这条验不了(**不假装通过**)", False)]
age_h = (time.time() - float(hb.get("started_ts") or 0)) / 3600.0
# 🔴 ⚠️ 时长**只打印、不作断言**(2026-10-04 踩过):本条要反驳的是
# 「活不过当轮」⇒ 只需**能算出时长且 pid 活着**。
# ⛔ 硬要「> 1 小时」⇒ 常驻一重启(pid 19424→65072,活 0.2 h)就**假红**。
R.append(("**能算出常驻存活时长**(本次 %.2f h)+ 起点是 `started_ts`"
" ⇒ ⛔ 反驳「工具算不出、只能靠猜」" % age_h, age_h > 0.0))
R.append(("常驻**此刻活着**(pid 活)—— 这才是「没活不过当轮」的直接反证",
bool(hb.get("pid"))))
R.append(("心跳里带 `started_ts`(⛔ **算「活了多久」的唯一依据** —— "
"⛔ 别用 mtime 猜、⛔ 别用 pid 猜)",
bool(hb.get("started_ts"))))
R.append(("心跳里带 `argv0`(**能证伪「跑的是哪份文件」**)",
bool(hb.get("argv0"))))
src_self = Path(__file__).read_text(encoding="utf-8", errors="replace")
i = src_self.find("def t_supervise_lifespan_not_invented")
j = src_self.find("@case(", i + 10)
seg = src_self[i:j]
import re as _re
age_lines = [ln for ln in seg.splitlines()
if "age_h" in ln and "=" in ln and "append" not in ln]
R.append(("🔴 **算时长的那一行逐字读的是 `started_ts`**"
"(⛔ `pid` 是**身份**不是**时间**;⚠️ 实测把 `pid` 转 float 也能"
"通过『值小于 now』⇒ ⛔ 只验值不验字段 = 白验)",
bool(age_lines) and all('hb.get("started_ts")' in ln for ln in age_lines)))
R.append(("⛔ 算时长的代码**没有**拿 `mtime`/`st_mtime` 当起点"
"(⛔ mtime 会被复制/同步改掉)",
not _re.search(r"age_h\s*=.*mtime", seg)))
return R
@case("副本同步:**残留/待补/待更新三类漂移都抓得到**,且白名单不被动(P0-51 的正解)")
def t_mirror_drift():
"""🔴 为什么要有这条:`vibe-product` 用的是**工作区副本**(整目录拷贝出去的),
全局删掉的文件副本**不知道**,会静默留着 ⇒ 技能改动在副本上"看不到",
而副本才是那个区**真在跑的**代码(实测:`mut_run.py` 差点没补上,
`00-动手前必过.md`/`99-速查清单.md` 一度只存在于全局)。
⚠️ 本条**自建夹具**(⛔ 不拿真副本跑判据):
真副本的漂移是**外部状态**(取决于上次同步到哪)⇒ 拿它当判据,
「副本刚同步完」就必红。**自建**才能对每类漂移做变异验证。
"""
import importlib.util
mp = HERE / "workspace_mirror.py"
if not mp.exists():
return [("⛔ 包内缺 `scripts/workspace_mirror.py` ⇒ 副本漂移没人管", False)]
spec = importlib.util.spec_from_file_location("wm_selftest", mp)
wm = importlib.util.module_from_spec(spec)
spec.loader.exec_module(wm)
# 🔴 夹具要**贴真实结构**:`<夹具>/.workbuddy/skills/<包>/`。
# 理由(实测踩过):备份落点靠「往上找 `.workbuddy`」判定 ⇒ 结构不像 ⇒
# 备份会写进**真工作区的 交付物/**(自测污染真工作区,且判据依赖外部状态)。
fx_root = TEST_WS / "mirror_fixture"
fx_src = fx_root / "src"
fx_cp = fx_root / "ws" / ".workbuddy" / "skills" / "session-mechanism"
shutil.rmtree(str(fx_root), ignore_errors=True)
(fx_src / "sub").mkdir(parents=True, exist_ok=True)
(fx_cp / "sub").mkdir(parents=True, exist_ok=True)
# 夹具:全局侧 3 文件(a/b/c),副本侧 b 篡改 + 残留 r + 缺 c + 白名单 roots.env
(fx_src / "SKILL.md").write_text("# fixture", encoding="utf-8")
(fx_src / "a.py").write_text("A=1\n", encoding="utf-8")
(fx_src / "sub" / "b.py").write_text("B=1\n", encoding="utf-8")
(fx_src / "c.py").write_text("C=1\n", encoding="utf-8")
(fx_cp / "SKILL.md").write_text("# fixture", encoding="utf-8")
(fx_cp / "a.py").write_text("A=1\n", encoding="utf-8")
(fx_cp / "sub" / "b.py").write_text("B=1\n# TAMPER\n", encoding="utf-8") # 待更新
(fx_cp / "r_stale.py").write_text("STALE\n", encoding="utf-8") # 真残留
(fx_cp / "roots.env").write_text("DSH_WS_ROOT=/fixture/ONLY\n", encoding="utf-8") # 白名单
g, c, only_c, only_g, changed = wm.diff(fx_src, fx_cp)
det = [
("抓到**真残留**(全局已删、副本还留着)",
"r_stale.py" in only_c and len(only_c) == 1),
("抓到**待补**(全局有、副本没有)",
"c.py" in only_g and len(only_g) == 1),
("抓到**待更新**(两边都有但内容不同)",
"sub/b.py" in changed and "a.py" not in changed),
("**没把白名单当漂移**(roots.env 不该出现在任一差异里)",
not any("roots.env" in x for x in (only_c + only_g + changed))),
]
# --sync:真跑一遍,验「收口 + 白名单内容没被动 + 幂等」
rc1 = wm.do_sync(fx_src, fx_cp)
g2, c2, only_c2, only_g2, changed2 = wm.diff(fx_src, fx_cp)
det += [
("`--sync` 收口(rc=0)", rc1 == 0),
("同步后**无漂移**", not (only_c2 or only_g2 or changed2)),
("同步后**残留已真删**(⛔ 不能只是「不报」)",
not (fx_cp / "r_stale.py").exists()),
("同步后**待补已真写入**", (fx_cp / "c.py").exists()
and (fx_cp / "c.py").read_bytes() == (fx_src / "c.py").read_bytes()),
("同步后**篡改已被覆盖回全局版**",
(fx_cp / "sub" / "b.py").read_bytes() == (fx_src / "sub" / "b.py").read_bytes()),
("🔴 **白名单内容原样保住**(roots.env 仍写 /fixture/ONLY,"
"⛔ 被冲回全局值就会让副本去读写别人的仓库)",
(fx_cp / "roots.env").exists()
and "/fixture/ONLY" in (fx_cp / "roots.env").read_text(encoding="utf-8")),
("**幂等**:再 sync 一次 rc=0", wm.do_sync(fx_src, fx_cp) == 0),
]
# 🔴 锚在**结构**上:真正拦住"覆盖白名单"的是覆盖循环里那句 `if k in KEEP: continue`。
# ⚠️ 早先只验"写回"验不到 —— 因为 `_files()` 已把 KEEP 排除,写回那步**恒为空转**,
# 把它删掉判据照样全绿(2026-10-04 变异实测)。⇒ 判据必须锚在真作用点上。
wsrc = mp.read_text(encoding="utf-8")
det += [
("覆盖循环里有**白名单守卫** `if k in KEEP: continue`"
"(⛔ 拿掉它 ⇒ roots.env 被全局值覆盖 ⇒ 副本去读写别人的仓库)",
re.search(r"for k in sorted\(g\):\s*\n\s*if k in KEEP:\s*\n\s*continue", wsrc) is not None),
("`--sync` 先**备份再删**(⛔ 破坏性动作前必须留退路)",
wsrc.index("copytree") < wsrc.index("unlink()")),
("破坏前有**预检**(源文件读不出就拒绝动手,⛔ 不许崩在半路留半同步态)",
"拒绝动手" in wsrc),
("覆盖用**临时文件+os.replace**(⛔ 半截文件不能留在副本里)",
"os.replace" in wsrc and ".mirtmp" in wsrc),
]
fx_root2 = TEST_WS / "mirror_fixture2"
fx_src2 = fx_root2 / "src"
fx_cp2 = fx_root2 / "ws" / ".workbuddy" / "skills" / "session-mechanism"
shutil.rmtree(str(fx_root2), ignore_errors=True)
shutil.copytree(str(fx_src), str(fx_src2))
fx_cp2.parent.mkdir(parents=True, exist_ok=True)
shutil.copytree(str(fx_src), str(fx_cp2))
# ⚠️ `roots.env` 是**副本独有**(src 侧没有)⇒ copytree 后天然不存在,
# ⛔ 别再 unlink(早先多这一步 ⇒ FileNotFoundError,用例崩在夹具准备上)
# ② **从来没有过** ⛔ 不该报"丢失"(否则每次 sync 都刷假告警,久了没人看)
# ⚠️ 判据侧踩过两次坑,都记这:
# · ⛔ 不能假设"删了就一定救不回" —— 夹具有备份时工具**正确地**捞回、rc=0,
# 早先断言 rc≠0 ⇒ 把正确行为判成错(假红)。
# · ⛔ 也不能假设"没备份就该报丢" —— ⛔ **分不出来**("从来没有过" vs "丢了"
# 在副本现状里完全同形)⇒ 工具选择**不报**。这是有意的保守,不是缺陷。
import contextlib
import io as _io
_buf = _io.StringIO()
with contextlib.redirect_stdout(_buf):
rc_none = wm.do_sync(fx_src2, fx_cp2)
_out2 = _buf.getvalue()
det += [
("⛔ **从来没有过的白名单不报「丢失」**(`diff` 排除 KEEP + 无历史备份"
"⇒ 分不出「丢」和「没过」⇒ 有意不报,⛔ 不刷假告警)",
rc_none == 0 and "白名单缺失" not in _out2),
("⛔ **代码里明写了这条保守取向**(`副本历史里有过` 才算丢)",
"副本历史里有过" in wsrc),
("⛔ **捞不到时明说不代填**(⛔ 绝不拿全局那份顶替 —— 全局指向别的仓库)",
# 🔴 锚在**结构**上(早先锚措辞/锚 `else:` ⇒ 措辞微调、结构一变就假红,今天各踩一次):
# 三句必须在**同一个 `if keep_lost:` 块**里 ⇒ 兜底与告警走的是同一条路径。
re.search(r"if keep_lost:[\s\S]{0,700}?绝不拿全局那份顶替[\s\S]{0,200}?请人工恢复"
r"[\s\S]{0,80}?不代填", wsrc) is not None),
("⛔ **兜底源只认「历次备份」**(⛔ 不是本次刚建的、⛔ 更不是全局)",
"历次" in wsrc and "BK_PREFIX" in wsrc
and re.search(r"sorted\([\s\S]{0,300}?reverse=True\)", wsrc) is not None),
("⛔ **兜底源只认「历史备份」**(⛔ 不是副本现状、⛔ 更不是全局)",
re.search(r"兜底源=\*\*历史备份", wsrc) is not None),
]
# ③ 有备份时**能**捞回:造一次"先 sync(留备份)→ 再删 → 再 sync"
(fx_cp2 / "roots.env").write_text("DSH_WS_ROOT=/fixture/BACKUPED\n", encoding="utf-8")
(fx_cp2 / "sub" / "b.py").write_text("B=1\n# DIFF\n", encoding="utf-8") # 制造漂移 ⇒ 触发备份
wm.do_sync(fx_src2, fx_cp2) # ← 这次备份里有 roots.env
(fx_cp2 / "roots.env").unlink()
rc_bk = wm.do_sync(fx_src2, fx_cp2)
det += [
("🔴 **有备份时,白名单被删后 `--sync` 能捞回**"
"(`roots.env` 丢失 ⇒ 副本按位置推导去读写**别的仓库**,静默且难查)",
rc_bk == 0 and (fx_cp2 / "roots.env").exists()
and "/fixture/BACKUPED" in (fx_cp2 / "roots.env").read_text(encoding="utf-8")),
]
shutil.rmtree(str(fx_src2.parent), ignore_errors=True)
shutil.rmtree(str(fx_src.parent), ignore_errors=True)
return det
@case("副本同步:`roots.env` 白名单**只按文件名**判(⛔ 换副本不许靠「我记得跳过哪个」)")
def t_mirror_keep_by_name():
import importlib.util
spec = importlib.util.spec_from_file_location("wm_selftest2", HERE / "workspace_mirror.py")
wm = importlib.util.module_from_spec(spec)
spec.loader.exec_module(wm)
return [
("白名单含 `roots.env`(⛔ 覆盖它 ⇒ 副本读写到别的仓库)", "roots.env" in wm.KEEP),
("白名单含 `副本使用说明.md`(副本独有的口径文档)", "副本使用说明.md" in wm.KEEP),
("白名单含 `install.log`(副本自己的安装历史)", "install.log" in wm.KEEP),
("白名单是**元组**(⛔ 散着写容易漏)", isinstance(wm.KEEP, tuple)),
("**`__pycache__` 等自身目录不算漂移**(否则每次跑都报一堆假漂移)",
"__pycache__" in wm.SELF_DIRS),
]
@case("🔴 常驻载体形态:**旧看守已彻底清除 + 不许回潮**(2026-10-05 收敛)")
def t_keeper_legacy_ps1_form_gone():
"""🔴🔴 2026-10-05 **形态收敛的守卫**(取代此前 `t_keeper_tpl_found_in_copy_layout`)。
**为什么换掉旧的**:旧用例判「副本形态下找不找得到 `assets/start-supervise.ps1.tpl`」,
而该模板与「旧 看守」这套形态**已整套废弃** ——
任务动作从「壳脚本 -File」改成 **`pythonw.exe` + `supervise-launch.py`**
(GUI 子系统 ⇒ 零 conhost ⇒ **不闪黑窗**,用户 2026-10-05 报「又弹了窗口」的直接修复)。
⇒ 旧用例连**被测对象都不存在了** ⇒ 恒红、且无法修复 ⇒ **必须删,换成本条**。
🔴 本条的职责是**守形态**(防回潮),五条一起判:
① 代码里**不许再出现**读那个 `.tpl` 的路径拼接;
② 任务动作**必须是** `pythonw.exe` + 启动器;
③ ⛔ 动作里不许再把壳脚本当看守(⚠️ 登记任务本身仍走 PowerShell,
那是在**登记**、不是在**当看守**,两者别混);
④ `CREATE_NO_WINDOW` 在位;
⑤ `_find_keeper_tpl` 死代码已清。
"""
# 🔴🔴 2026-10-06 **扩到三个文件**(本轮实测抓到的探照灯打偏):
# 原版只扫 `collabd.py` ⇒ 而**同族的 `init_workspace.py` 整份没被扫到**,
# 它揣着 `start-supervise.ps1.tpl` 路径拼接 + `-File start-supervise.ps1` 动作
# 活到今天(`_register_keeper_task` 又写了一份建任务代码 = 第二套起法)。
# ⚠️ 教训:**判据只扫"我以为哪个文件"就等于没扫** —— 凡是"这一族文件都不许有 X",
# 必须**枚举整族**,⛔ 不许只挑一个代表。
_FAMILY = ["collabd.py", "init_workspace.py", "collabctl.py"]
res = []
_codes = {}
for _n in _FAMILY:
_p = HERE / _n
if not _p.is_file():
res.append((f"⛔ `{_n}` 存在(同族文件缺了 ⇒ 判据无从施加)", False))
continue
_s = _p.read_text(encoding="utf-8")
_codes[_n] = (_s, _strip_comments_and_docs(_s))
# ① 整族:⛔ 谁也不许再去找那份废模板
_bad = [n for n, (_, c) in _codes.items() if "start-supervise.ps1.tpl" in c]
res.append(("⛔ 全族已无「读 `start-supervise.ps1.tpl`」的路径拼接(形态废弃 ⇒ 不该有人再找它)",
not _bad))
# ② 整族:⛔ 谁也不许把壳脚本当看守
_bad = [n for n, (_, c) in _codes.items() if "-File start-supervise.ps1" in c]
res.append(("⛔ 全族动作里**不再有** `-File start-supervise.ps1`(⛔ 壳脚本当看守=闪黑窗)",
not _bad))
# ③ 建任务者:动作必须是 pythonw + 启动器
_mk = {n: c for n, (_, c) in _codes.items() if "New-ScheduledTaskAction" in c}
_ok = bool(_mk)
_ok = _ok and all("supervise-launch.py" in c for c in _mk.values())
_ok = _ok and all("pythonw.exe" in _codes[n][0] for n in _mk)
res.append(("🔴 建任务者(%s)动作=`pythonw.exe` + 启动器 `supervise-launch.py`"
% "/".join(sorted(_mk)), bool(_ok)))
# ④ 建任务者:`CREATE_NO_WINDOW` 在位
_bad = [n for n, c in _mk.items() if "0x08000000" not in c]
res.append(("🔴 建任务者 `CREATE_NO_WINDOW`(0x08000000)在位(⛔ 缺了每次登记都闪一下)",
not _bad))
# ⑤ ⭐⭐ **任务名口径必须唯一**(本轮抓到的病:`collabd.py` 建 `collabd-supervise-*`,
# 而 `collabctl.py` 建 `collabd-keepalive-*` ⇒ 前者在 `SCHED_TASKS` 禁用名单里
# ⇒ **自愈起的常驻会被下一次 `off` 当旧形态干掉**)
_names = set()
for n, c in _mk.items():
# ⚠️ **先剥掉「禁用名单」块再扫** —— `SCHED_TASKS` 里是**故意**列着旧名去**禁用它**的,
# 不剥就会把"它自己的否定句"当成违规。
# 🔴 这是本项目**第三次**踩同一个坑(前两次:查"PRD 有没有视觉词"命中免责声明、
# 查"有没有六份旧产出"命中 PRD 里的禁令文本)⇒ 判据凡扫文本,先问"这句是不是它的否定句"。
c2 = re.sub(r"SCHED_TASKS\s*=\s*\[[^\]]*\]", "", c)
_names |= set(re.findall(r'"collabd-(?:supervise|keepalive)-', c2))
res.append(("⭐⭐ 任务名口径唯一(建任务者只许用 `collabd-keepalive-`;"
"⛔ 出现 `collabd-supervise-` 会被 `off` 当旧形态禁掉)|实测命中:%s"
% (sorted(_names) or "无"),
_names == {"\"collabd-keepalive-"}))
# ⑥ init_workspace 侧:死代码已清
_iw = _codes.get("init_workspace.py", ("", ""))[1]
_dead = [d for d in ("def _register_keeper_task(", "def _keeper_task_state(",
"def _ps_check(", "def _find_keeper_tpl(") if d in _iw]
res.append(("`init_workspace.py` 旧形态死代码已清(⛔ 留着会再招一批过期判据)|实测残留:%s"
% (_dead or "无"), not _dead))
return res
@case("🔴 看板渲染:**真跑 `renderProject()`**(⛔ 不是查字符串 —— 实发过 `acc is not defined`)")
def t_board_render_runs_clean():
"""🔴🔴 2026-10-05 用户报障逐字:**「快照读取失败:acc is not defined」**。
**病症**:页面白屏 + 一句「快照读取失败…服务还开着吗?重跑 board.py」
—— 那句文案把**渲染期 ReferenceError** 说成**服务挂了** ⇒ 用户去重跑服务(白费力)。
**真因**:`renderProject(d)` 里引用了**从未声明的标识符**:
· `acc` —— 验收判据其实在 `d.goal.acceptance`(实测快照顶层**没有** `acc`);
· `p.*` —— 参数早已改名成 `d`,函数体里 6 处没跟着改(同族,一次改名的漏网)。
⇒ 都属于「**变量名与数据流向不符**」,⛔ 语法检查查不出(原版语法是过的)。
✅ 本判据的做法=**真跑**:用 `assets/board-render-probe.js` 把页面脚本放进
`vm` 的**全局作用域**执行(⛔ 不能用 `new Function` —— 函数会关在局部,
取到 `undefined` ⇒ **假绿**),喂一份**合成快照**,真调 `renderProject()`,
看它抛不抛、`#proj` 真渲染了没。
⚠️ **必须行为级**:只 grep `acc is not defined` 这种字样是**恒真**的判据
(修好之后字符串当然不在)⇒ 测不出"将来又冒出个别名"。
⚠️ 合成快照**不依赖活服务**(自测要在离线/CI 也能跑)。
"""
import json as _json
import subprocess as _sp
import tempfile as _tmp_mod
probe = HERE.parent / "assets" / "board-render-probe.js"
html = HERE.parent / "assets" / "board.html"
res = []
res.append(("探针在位 `assets/board-render-probe.js`", probe.is_file()))
res.append(("探针路径**自定位**(⛔ 不写死作者机器)", "BOARD_HTML" in probe.read_text(encoding="utf-8")))
# 源码级:`acc` 必须**先声明后使用**(⛔ 不能只是"出现过")
# ⚠️ 注释里也会出现 `var acc=`(本轮就是)⇒ **先剔注释**再判,否则判据假红。
src = html.read_text(encoding="utf-8")
i = src.index("function renderProject(d){")
j = src.index("\nfunction ", i + 10)
body = src[i:j]
_code = re.sub(r"/\*[\s\S]*?\*/", "", body) # 剔块注释
_code = re.sub(r"(?m)//.*$", "", _code) # 剔行注释
_decl = _code.find("var acc=")
_use = _code.find("Object.keys(acc)")
res.append(("`renderProject` 里 `acc` **有声明**(`var acc=` 在用它之前,⛔ 剔注释后判)",
_decl >= 0 and _use >= 0 and _decl < _use))
res.append(("`renderProject` 里**没有裸 `p.`**(参数名是 `d`,改名漏网会 ReferenceError)",
not re.search(r"(?<![\w.$])p\.\w", _code)))
# 行为级:真跑(用合成快照,⛔ 不依赖活服务)
if probe.is_file():
# 🔴🔴 2026-10-05 **夹具必须逐字照真快照的层级**(这条漏 ⇒ 上一条 bug 才活了下来)。
# 实测活服务 `/board.json` 顶层键里**没有** `short`/`title`/`criteria`/`id`/`main_sid8`
# ⇒ 它们全在 `project` 下;`goal` 那份只装 `acceptance`/`short`/`title`。
# 旧夹具把 `short`/`title`/`criteria` **放在顶层** ⇒ **喂了一份服务器从不发出的形状**
# ⇒ 代码里裸写 `d.short` 也能"取到值" ⇒ 判据**假绿**,真机上却显示「(未声明主题)」。
# 📌 教训:**离线夹具造假的地方,就是判据瞎的地方** —— 夹具必须对活服务取一次样再写死。
# 🔴🔴 2026-10-05 再补一次(同族第 2 次):夹具**又漏了 `ws_name`** ——
# 而目标行 pill 现在画的正是它 ⇒ 夹具没有该字段时探针报「工作区真值 ⚠️/❌」。
# ⚠️ 更要紧的是**取值必须与 `goal.short` 不同**:若这里也写「自测」,
# "查 ws_name" 与 "查 short" 得到同一结论 ⇒ 判据**区分不出新旧口径**(假绿)。
# ⇒ 取一个**明显不同**的值,⛔ 别图省事抄 `short`。
_snap = {
"key": "selftest",
"ws_name": "selftest-ws",
"goal": {"short": "自测", "title": "自测目标",
"acceptance": {"A1-真": "过(自测)", "A2-假": "待达标(自测)",
"_说明": "说明行 ⛔ 不进分母"}},
"project": {"short": "自测", "title": "自测目标", "id": "st-1",
"main_sid8": "st123456", "workspace": "E:/selftest",
"criteria": ["自测判据"], "topics": ["自测类别"],
"topics_source": {"kind": "declared", "at": "自测", "by": "自测"},
"main_by_topic": {}},
}
_f = None
try:
_f = Path(_tmp_mod.mkdtemp(prefix="rendchk_")) / "snap.json"
_f.write_text(_json.dumps(_snap, ensure_ascii=False), encoding="utf-8")
_node = None
for _c in ("node", "node.exe"):
try:
if _sp.run([_c, "--version"], capture_output=True, timeout=10).returncode == 0:
_node = _c
break
except Exception:
pass
if _node is None:
res.append(("⚠️ 本机没有 node ⇒ 行为级这段**跳过**(⛔ 不算通过)", True))
else:
env2 = dict(os.environ)
env2["BOARD_HTML"] = str(html)
env2["BOARD_SNAP_FILE"] = str(_f) # 离线:读盘上合成快照
env2["BOARD_WANT_PASS"] = "1 / 2 通过" # 分母须剔 `_` 开头说明行
out = _sp.run([_node, str(probe)], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=90, env=env2,
cwd=str(probe.parent))
so = (out.stdout or "")
res.append(("🔴 **真跑 `renderProject()` 零异常**(原版在此抛 `acc is not defined`)",
"render() 抛异常: ✅" in so))
res.append(("合成快照真渲染出「完成情况」(⛔ 不是空产物)",
"产物含 完成情况: ✅" in so))
res.append(("分母剔 `_` 开头说明行 ⇒ 应为 `1 / 2 通过`",
"1 / 2 通过" in so))
# 🔴🔴 2026-10-05 加:**静默错文案**那一族(用户报障「目标(未声明主题)(未声明目标)」)。
# ⛔ "没炸" ≠ "画对了":裸写 `d.short` 不抛异常,只把兜底文案悄悄画上去。
res.append(("🔴 **无静默错文案**(⛔ 兜底「(未声明主题)/(未声明目标)」不许出现)",
"静默错文案 : ✅" in so and "(未声明主题)" not in so))
# 🔴🔴🔴 2026-10-05 **口径已改**(用户逐字:「目标 vibe-product 提取 …
# 改为 vibe-product 目标:提取 …」)⇒ 目标行**不再**渲染 `goal.short`,
# 改为渲染【所属工作区真名 `ws_name`】+【目标全称 `goal.title`】。
# ⚠️ 旧断言查 `"主题真值 : ✅"` —— 那条探针输出**已被改名**(`工作区真值`)
# ⇒ 若这里不跟改,就是"同一判据两处实现"的**又一次**(本包红线):
# 探针改了口径、自测还盯着旧文案 ⇒ 自测**永远红**(与代码对错无关)。
# ✅ 新口径=**双查 + 形态**:pill 画 `ws_name`、标题画 `title`、且顺序对。
res.append(("🔴 **工作区真值渲染出来了**(目标行 pill 必须画出 `ws_name`,⛔ 不是主题简称)",
"工作区真值 : ✅" in so))
res.append(("🔴 **目标行形态正确**(必须是「目标 → 工作区 → 标题」这个顺序)",
"目标行形态 : ✅" in so))
res.append(("🔴 **旧口径已清**(⛔ 不许再有「主题真值」这条断言盯着 `goal.short`)",
"主题真值" not in so))
res.append(("🔴 **目标 id 渲染出来了**(`criteria`/`id` 取错层级时这项会红)",
"目标 id : ✅" in so))
finally:
if _f is not None:
shutil.rmtree(str(_f.parent), ignore_errors=True)
return res
@case("🔴🔴 peer(跨工作区)格**也要认主会话**(⛔ 不许只看本看板的 `WS`)")
def t_peer_main_resolved():
"""🔴🔴🔴 2026-10-05 用户报障逐字:「**peer 工作区的主会话还是找不到对应的吗**」。
**病症**:本区 tab 一切正常;切到 **peer 格**(别的目标工作区)⇒ 每个任务类别 chip
都画成「⚠ 无主会话」,而**那条主会话明明活着**(实测:对方主会话在宿主库里 `status` 正常、
`cwd` 就是对方工作区根)。
**真因**:`_scan_ws_mains()` / `_resolve_main()` **只比对本看板的模块级 `WS`**
(`_same_ws(cwd)` 内部恒取 `WS`),而 peer 格的会话 `cwd` 指向**对方**目录
⇒ `_same_ws()` 恒 `False` ⇒ `cand/named/by_topic` **全空** ⇒ 前端「无主会话」。
⚠️ **同一条数据流上,"会话列表"早就为 peer 修过**(`_sessions(bypass=True)` +
`sc["workspace"] = ws_root`),**但主会话解析没跟上** ——
典型的"改一处漏一处",症状=列表对了、主会话仍空。
✅ **本用例的判据(能改前报红)**:
① `_scan_ws_mains(ws_root=<对方>)` 必须**扫得出** cwd 在对方的那个会话;
② `_resolve_main(..., ws_root=<对方>)` 的 `by_topic` 必须**指向它**;
③ ⛔ 反向:**不传 `ws_root`** 时必须**扫不出**(否则等于"把 peer 认成自己的" ⇒ 投错窗口)。
⚠️ 用**合成会话**(临时建一条真会话行到测试库)而不是赌机器上有没有 peer —— 否则换机必红。
"""
import sqlite3 as _sq
res = []
# ⚠️ 本用例要调的是 **`board.py`** 的函数(`_scan_ws_mains`/`_resolve_main`/`project_scope`),
# ⛔ **不是** `imp()` 装出来的 collabd 模块 —— 那两个模块都有同名函数,
# 拿错模块会 `AttributeError`(2026-10-05 本用例第一版就是这么红的,同族第 2 次)。
import board as m
# ── 合成一条"对方工作区的主会话"(直接写测试库;⛔ 绝不碰生产库)
peer_root = str(TEST_WS / "peerws")
sid = "9e1a" + "0" * 32
db = m._host_db()
if not db:
return [("(无法定位宿主库 ⇒ 本机跳过,⛔ 不算失败)", True)]
con = _sq.connect(str(db), timeout=2.0)
try:
con.execute("pragma busy_timeout=800")
con.execute(
"insert or replace into sessions(id,cwd,user_id,title,custom_title,status,"
"created_at,updated_at,last_activity_at,deleted_at) "
"values(?,?,?,?,?,?,?,?,?,null)",
(sid, peer_root.replace("\\", "/"), "selftest", "peer 主会话", "", "completed",
0, int(time.time() * 1000), int(time.time() * 1000)))
con.commit()
topics = ["界面交互"]
# ① 传 ws_root ⇒ 必须扫得出
sc_peer = m._scan_ws_mains({}, topics, peer_root)
res.append(("① 传 `ws_root` ⇒ 扫出对方主会话候选",
sid in sc_peer.get("cand", [])))
# ② 传 ws_root ⇒ by_topic 指向它
rm_peer = m._resolve_main({}, topics, peer_root)
res.append(("② 传 `ws_root` ⇒ `by_topic` 指向它",
str((rm_peer.get("by_topic") or {}).get("界面交互", {}).get("sid") or "") == sid))
# ③ 不传 ⇒ 必须扫不出(fail-closed:⛔ 不许把 peer 认成本区的)
sc_own = m._scan_ws_mains({}, topics)
res.append(("③ ⛔ 不传 `ws_root` ⇒ **扫不出**(不许把 peer 认成自己)",
sid not in sc_own.get("cand", [])))
# ④ `project_scope` 的 `workspace` 键**必须仍回本区**(那是另一条判据,⛔ 不许被顺手改掉)
# ⚠️ 比对基准取 **`m.WS`(被加载的那个 board 模块自己的 WS)**,⛔ 不是本测试脚本的 `WS`
# —— 两者来源不同(模块来自 `goal.json`/部署配置、脚本来自环境变量)⇒ 拿脚本的比会假红。
ps = m.project_scope({"title": "t", "short": "s", "topics": topics, "id": "g"}, peer_root)
_norm = lambda _v: str(_v or "").replace("\\", "/").rstrip("/").lower()
res.append(("④ `project_scope(ws_root=…)` 的 `workspace` 仍=本区 `WS`",
_norm(ps.get("workspace")) == _norm(m.WS)))
# ⑤ 🔴🔴 **"测装配"而非"测零件"**(P0-55 的同族):
# 上面 ①②④ 都是我**自己把 `ws_root` 递进去** ⇒ 只证明"函数接受这个参数",
# ⛔ **证明不了 `_goal_block()` 真把它传下去了** —— 实测变异 M3(删掉调用点的传参)
# **本条全绿**,而线上 peer 格照样「无主会话」。
# ⇒ 判据必须**静态咬住那条调用点**:`project_scope(...)` 在 peer 分支里必须带 `ws_root`。
_bs = (HERE / "board.py").read_text(encoding="utf-8")
res.append(("⑤ `_goal_block()` 的调用点**真把 `ws_root` 递给 `project_scope`**"
"(⛔ 只测函数体等于没测装配)",
"project_scope(g, ws_root)" in _bs))
finally:
try:
con.execute("delete from sessions where id=?", (sid,))
con.commit()
except Exception:
pass
con.close()
return res
@case("🔴 建目标时**顺手登记主会话**(用户原话:⛔ 不用每次会话都去声明)")
def t_main_registered_on_goal_create():
"""🔴🔴 2026-10-05 用户逐字:
> 「**就是你没有指定 声明目标的时候 指定是哪个会话(sid)**」
> 「**最好是记录会话,这样就不用每次会话都去做声明了**」
**病根**:`role_of()` 三条优先级里第三条 —— **未声明就落成哑档**(当 worker 使)。
实测踩到:本工作区的主会话 `a80f300d` 从没声明过 ⇒ 看板/投递全按 worker 走,
用户手动补 `--declare` 才发现(而且**顶掉了原主会话 `3f43ce71`** —— 那正是
"没指定在哪声明"的代价)。
✅ 正解=**在"创建目标"那一步顺手记**(`_ensure_goal_dir()` 那一刻 `CODEBUDDY_SESSION_ID`
必然已知),⛔ 不新增"要记得声明"的待办。
⚠️ 判据必须**行为级 + 变异有效**:
· 只断言"有 `register_main_session` 这个函数"是**恒真**的 ⇒ 测不出"忘了调";
· 必须真跑 `_ensure_goal_dir()`:**无主会话⇒认本会话**、**已有主会话⇒⛔ 不抢位**。
· 🔴 **不抢位那条是变异关键**:把"只在无主会话时认"这层判断撤掉 ⇒ 该条必红。
"""
import json as _json
m = imp()
res = []
src = (HERE / "collabd.py").read_text(encoding="utf-8")
res.append(("有唯一实现 `register_main_session()`(⛔ 不再两处各写一套)",
"def register_main_session(" in src))
res.append(("`--declare` **复用**它(⛔ 不就地再写一遍 roles)",
src.count("register_main_session(") >= 3))
_st_file = Path(m.STATE)
_bak = _st_file.read_bytes() if _st_file.exists() else None
_old_sid = os.environ.get("CODEBUDDY_SESSION_ID")
try:
# ── ① 本区**无**主会话 ⇒ 建目标时认下本会话
m.STATE.write_text(_json.dumps({"roles": {}, "topics": {}}, ensure_ascii=False),
encoding="utf-8")
os.environ["CODEBUDDY_SESSION_ID"] = "aaaa1111-0000-0000-0000-000000000001"
r1 = m._ensure_goal_dir()
_roles1 = _json.loads(_st_file.read_text(encoding="utf-8")).get("roles") or {}
res.append(("① 本区无主会话 ⇒ **建目标时登记本会话为 main**(⛔ 不用手动声明)",
_roles1.get("aaaa1111-0000-0000-0000-000000000001") == "main"))
res.append(("① 结果**说出口**(`main_reg` 有值,⛔ 不静默)",
bool(str(r1.get("main_reg") or "").strip())))
# ── ② 本区**已有**主会话 ⇒ 🔴 ⛔ 不抢位(补哑档 ≠ 抢位子)
m.STATE.write_text(_json.dumps(
{"roles": {"bbbb2222-0000-0000-0000-000000000002": "main"}, "topics": {}},
ensure_ascii=False), encoding="utf-8")
os.environ["CODEBUDDY_SESSION_ID"] = "cccc3333-0000-0000-0000-000000000003"
m._ensure_goal_dir()
_roles2 = _json.loads(_st_file.read_text(encoding="utf-8")).get("roles") or {}
res.append(("🔴 ② 本区**已有**主会话 ⇒ **⛔ 不抢位**(原 main 保持)",
_roles2.get("bbbb2222-0000-0000-0000-000000000002") == "main"))
res.append(("🔴 ② 也**不把本会话加进来**(⛔ 不许悄悄多出个 main)",
"cccc3333-0000-0000-0000-000000000003" not in _roles2))
# ── ③ 幂等:已经是 main 再登记 ⇒ roles 不变
_st3 = {"roles": {"dddd4444-0000-0000-0000-000000000004": "main"}, "topics": {}}
_r3a = m.register_main_session(_st3, "dddd4444-0000-0000-0000-000000000004", why="自测")
_r3b = m.register_main_session(_st3, "dddd4444-0000-0000-0000-000000000004", why="自测")
res.append(("③ 幂等:已是 main 再登记 ⇒ 无变化、`demoted` 为空",
_r3a.get("demoted") == [] and _r3b.get("demoted") == []
and _st3["roles"]["dddd4444-0000-0000-0000-000000000004"] == "main"))
res.append(("③ 登记新 main ⇒ **旧 main 降级 worker**(一个区一条主会话)",
(lambda _s: (m.register_main_session(
_s, "eeee5555-0000-0000-0000-000000000005", why="自测"),
_s["roles"].get("dddd4444-0000-0000-0000-000000000004") == "worker")[1])(
{"roles": {"dddd4444-0000-0000-0000-000000000004": "main"}})))
finally:
if _old_sid is None:
os.environ.pop("CODEBUDDY_SESSION_ID", None)
else:
os.environ["CODEBUDDY_SESSION_ID"] = _old_sid
if _bak is None:
try:
_st_file.unlink()
except Exception:
pass
else:
_st_file.write_bytes(_bak)
return res
# 🔴🔴 2026-10-05 **已删除 `t_keeper_script_is_published_copy`**。
# 它判的是旧 ps1 看守形态的三件事:`_own_path()` 的存在、`__SCRIPT__` 走 `_own_path()`、
# 「回落必打日志」。而 ps1 看守**整套废弃** ⇒ `__SCRIPT__` 在 `collabd.py` 里只剩注释一处
# ⇒ 该用例**恒红且无法修复**(被测对象不存在了)。
#
# ⚠️ 它真正有价值的那部分(**续命目标 / 身份基准必须走 `_own_path()` = 本区发布物**)
# 已被 `t_ensure_spawns_published_copy` **完整接管**(6 项,含变异对照),没有覆盖缺口。
@case("🔴🔴 常驻**自我繁殖**必须断:续命只许起**本区发布物**(用户「越创建越多」的真因)")
def t_ensure_spawns_published_copy():
"""🔴🔴 2026-10-05 实测事故(**自我繁殖** —— 用户看到「开了7个终端窗口…越创建越多」的直接成因)。
**现场**:清理后 `skills/session-mechanism/scripts/collabd.py --supervise` **反复复现**,
父进程每次都不同且已消失(`11668` / `15924` / `46488`)⇒ 看着像"幽灵自启"。
**真因(日志铁证)**:`_ensure.log` 里那一行 ——
`常驻续命:新起 --supervise pid=69464(原因:pid 12672 在、心跳新鲜,但**身份对不上**)`
① `ensure_supervise()` 起进程时填 `Path(__file__).resolve()` = **"正在跑的那份"**;
若它本身是**技能目录那份** ⇒ 续命又起技能目录那份 ⇒ **互为镜像、生生不息**;
② 同时 `_pid_is_collabd()` 的比对基准**也被技能目录那份顶着**
⇒ **真的本区常驻被判"身份对不上"** ⇒ 每轮 `--ensure` 都认为"没人跑"⇒ 再拉一条。
✅ 正解=**发布物优先**(`_own_path()`),三处共用:续命目标、「身份」基准、keeper 的 `--script`。
🔴 判据必须**变异有效**:把 `str(_own_path())` 换回 `str(Path(__file__).resolve())` ⇒ 本条必红。
"""
src = (HERE / "collabd.py").read_text(encoding="utf-8")
res = []
res.append(("🔴 续命的 Popen 目标走 `_own_path()`(⛔ 不是 `Path(__file__).resolve()`)",
'Popen([PYW, "-u", str(_tgt), "--supervise"]' in src))
res.append(("🔴 `_tgt` 就是 `_own_path()` 的返回值(⛔ 不是就地重算 `__file__`)",
"_tgt = _own_path()" in src))
res.append(("`cwd` 跟着目标走(⛔ 别用 `__file__` 的 parent —— 否则 cwd 与程序不在同一处)",
"cwd=str(_tgt.parent)" in src))
res.append(("⚠️ 发布物缺失回落要打告警(⛔ 静默 ⇒ 用户以为跑的是副本)",
"常驻续命回落到当前这份" in src))
res.append(("🔴 `_pid_is_collabd` 的身份基准也走 `_own_path()`(⛔ 否则真本区常驻被判成「不是我」)",
"_self = str(_own_path())" in src))
res.append(("三处共用同一函数(⛔ 不再各写一份 ⇒ 同一事实三套答案)",
src.count("_own_path()") >= 4))
return res
@case("🔴🔴 一个工作区只能有一个常驻:**启动前必判 + 身份核验**(pid 会复用)")
def t_supervise_singleton_identity():
"""🔴🔴 2026-10-05 用户点破逐字:
> 「**一个工作区只能有一个常驻。启动的时候必须先判断有没有**」
**两个方向的病**(同一句判据缺失的两面):
① **漏补**:`supervise_alive()` 原来只看 `心跳里的 pid 活着 ∧ 心跳新鲜`,
**从不验证那个 pid 到底是不是 collabd** —— 而 **Windows pid 会复用** ⇒
心跳里的号被别的进程占着 ⇒ 判"活着"、而真常驻早死了 ⇒ **永远没人补**(静默瘫)。
② **双起**:心跳没写进去的那几秒,多个 `--ensure` 并发**各自读到"无心跳"**
⇒ 各起一条 ⇒ **两条常驻**(双写 `collabd-state.json` = 脑裂)。
✅ 修法:
· `_pid_is_collabd(pid, argv0)` —— 按 `QueryFullProcessImageNameW` 取真实 exe
+ `argv0` 对照(心跳里**本来就写了 `argv0`**,可此前**只写不读**,白存);
· `--supervise` 加**互斥窗口**:原子抢位 `SUP_PID`(`os.replace`)⇒ sleep 1.5 s ⇒ 回验让位。
🔴 判据必须**变异有效**:⛔ 只断言"有这个函数"是恒真的 ⇒ 测不出"判据又被拆掉"。
"""
m = imp()
src = (HERE / "collabd.py").read_text(encoding="utf-8")
res = []
res.append(("有身份核验 `_pid_is_collabd()`(⛔ 不再只看号还活着)",
"def _pid_is_collabd(" in src))
res.append(("`supervise_alive()` 真的**调**它(⛔ 有函数不调=没接线)",
"if not _pid_is_collabd(" in src))
res.append(("核验取的是**真实 exe 路径**(`QueryFullProcessImageNameW`,⛔ 不靠进程名猜)",
"QueryFullProcessImageNameW" in src))
res.append(("核验**只认 pythonw**(常驻一律 pythonw 起,⛔ python 不算)",
'"pythonw" not in exe' in src))
res.append(("有**互斥窗口**(原子抢位 ⇒ 回验 ⇒ 让位)⛔ 防两条同时启动",
"互斥窗口回验" in src))
res.append(("抢位用 `os.replace` **原子替换**(⛔ 不用文件锁 —— 本机实测会卡死)",
"os.replace(str(_tmp_pid), str(SUP_PID))" in src))
# ── 行为级:拿**本进程**当样本(它显然是活着的 python,但**不是 pythonw 常驻**)
# ⇒ `_pid_is_collabd(自己的 pid)` 必须判 **False**(这是"身份核验真的在工作"的铁证;
# 若实现退化成"只看 pid 活不活"就会恒判 True ⇒ 本条必红)。
try:
_me_alive = m._pid_alive(os.getpid())
_me_collabd = m._pid_is_collabd(os.getpid(), "")
res.append(("行为级:本进程 pid 活着(前提成立)", bool(_me_alive) is True))
res.append(("🔴 行为级:**本进程活着但判不是 collabd**(当前用 python 跑,⛔ 不是 pythonw)"
" ⇒ 证明核验**真在看身份**,⛔ 不是恒 True",
_me_collabd is False))
except Exception as _e:
res.append(("行为级核验跑不起来:%r" % (_e,), False))
res.append(("行为级:pid 空/非法 ⇒ 一律 False(⛔ 不保守判在导致漏起)",
m._pid_is_collabd(0, "") is False and m._pid_is_collabd("x", "") is False))
return res
@case("🔴🔴 换目标(`declare --switch-goal`)⇒ `lifecycle` **自动复位「进行中」**(⛔ 不靠人手补)")
def t_declare_resets_lifecycle():
"""🔴🔴 2026-10-05(用户定案逐字):
> 「一个工作区 同时只能执行一个目标,如果要切换目标,需要用户确认,
> **然后切换和关注检查切换后的目标**」
🔴 **改前事实**:`declare` **从不碰 `lifecycle`** —— 只在末尾打一句
「别忘了同步 `lifecycle`(⛔ `declare` 不碰它)」**让人手补**。
🔴 **为什么必须自动做**:旧目标做完 ⇒ `lifecycle` 停在「已完成」⇒ 换到新目标后
**机制照旧不拉检查会话**(`collabd.py` 的判据:`life != 进行中` 时只在
"队列还有非 done 件"才拉)⇒ **新目标没人检查** = 与用户"切换后要关注检查"**方向相反**。
⚠️ 且靠人手补 = 又一条"要记得"的待办(同族:用户「最好是记录会话,这样就不用每次
会话都去做声明了」—— **凡"要人记"的都会忘**)。
⚠️ **不是破坏性动作**:旧 `goal.json` **整份归档**(同一步做)⇒ 旧值可回查;
且只在**已确认换目标**时做,⛔ 纯细化(只改措辞/补 why/kpi)不触发。
🔴 **判据必须行为级**(⛔ 不许只查源码里有没有那行):
真造一份 `goal.json`(`lifecycle=已完成`)⇒ 真跑 `goalctl.py declare --switch-goal --yes`
⇒ 读回落盘的 `lifecycle` 必须是「进行中」;**对照组**:不带 `--switch-goal` 的
纯细化(标题不变)⇒ `lifecycle` **必须原样不动**(⛔ 证明复位绑的是"换目标"这个事件,
不是"跑了一次 declare")。
⚠️ 隔离一律用**临时 `COLLABD_CONFIG`**(同 `t_acc_selfref_excluded`:
只设 env 不给 config 会回落读到真工作区,曾覆盖真 `goal.json`)。
"""
import json as _j
import tempfile as _tf
import subprocess as _sb
import pathlib as _pl
res = []
gctl = HERE / "goalctl.py"
# 源码级:确认 `_is_switch` 分支里真写了 lifecycle(⛔ 只作辅助,主判在行为级)
src = gctl.read_text(encoding="utf-8")
res.append(("`declare` 的换目标分支里**真写** `new[\"lifecycle\"]`(⛔ 不再只打提示)",
'new["lifecycle"] = "进行中"' in src))
# 🔴🔴 2026-10-05 **误红修正**:第一版直接查 `"别忘了同步" not in src` ⇒ 误红。
# 真因:`goalctl.py` 里那句字面**仍合法保留在注释**中 —— 它是「**改前事实**」的
# 取证记录(写明"改前 `declare` 从不碰 lifecycle"),是有价值的考古证据,
# ⛔ 不该为了过一条断言而删掉。
# ✅ 正解=**剔掉注释行再查**:只判「有没有这句提示出现在**可执行语句**里」。
_code_lines = [ln for ln in src.splitlines()
if not ln.lstrip().startswith("#")]
_code = "\n".join(_code_lines)
res.append(("⛔ **可执行语句**里已无那句「别忘了同步 `lifecycle`」旧提示(注释里的改前取证不算)",
"别忘了同步" not in _code))
res.append(("⛔ 也确认旧的「让人手补」`print` 真被换成自动复位提示",
"自动复位为「进行中」" in src or "自动复位" in _code))
# 🔴🔴 2026-10-05 **护栏自身要被判**(⛔ 别让护栏变成死代码):
# 本用例出过两次"隔离失效 ⇒ 静默写真工作区"的事故 ⇒ 夹具必须**自查落点**。
# ⚠️ 注意查的是**本文件**(`selftest.py`),⛔ 不是 `goalctl.py` —— 第一版查错文件 ⇒ 误红。
_self_src = Path(__file__).read_text(encoding="utf-8")
res.append(("🔴 夹具**自查子进程自报落点**(越界即记红,⛔ 不许静默写到别处)",
"夹具越界" in _self_src and "落点 `" in _self_src))
res.append(("🔴 夹具用对变量:`DSH_COLLAB_WS`(`_resolve_ws()` 的第一顺位)",
'"DSH_COLLAB_WS": tmp' in _self_src))
def _run(tmp, args, goal):
# 🔴🔴 2026-10-05 **隔离夹具的真修(本用例的第 2 个坑)**:
# `goalctl.py` 的 `INBOX` 是**硬编码** `WS/"tmp"/"supervise-inbox"`
# —— ⛔ **不读配置里的 `inbox` 字段**(实测:`{"inbox":"inbox"}` 被无视)。
# 第一版夹具把 `goal.json` 写在 `<tmp>/inbox/` ⇒ 脚本读到「(无)」,
# 写又落到 `<tmp>/tmp/supervise-inbox/` ⇒ **读回当然还是旧值**(看着像"没生效")。
# ✅ 正解=**落点跟着脚本走**:夹具与脚本用**同一个** `WS/tmp/supervise-inbox`。
cfg = _pl.Path(tmp) / "collabd.config.json"
cfg.write_text(_j.dumps({"workspace": tmp.replace("\\", "/"),
"inbox": "inbox", "taskgraph": "inbox/taskgraph.json",
"live": "inbox/realtime.md"}, ensure_ascii=False),
encoding="utf-8")
inbox = _pl.Path(tmp) / "tmp" / "supervise-inbox"
inbox.mkdir(parents=True, exist_ok=True)
(inbox / "goal.json").write_text(_j.dumps(goal, ensure_ascii=False), encoding="utf-8")
# 🔴🔴 2026-10-05 **隔离夹具的真修**(同一课**第四遍**):
# `goalctl.py:_resolve_ws()` 的候选顺序是
# `DSH_COLLAB_WS` → `DSH_WS_ROOT` → cwd(含 `.workbuddy/collab/`)
# —— **⛔ 完全不看 `COLLABD_CONFIG`,⛔ 也不认 `COLLABD_WORKSPACE`**!
# 第一版夹具只设了 `COLLABD_CONFIG` + `COLLABD_WORKSPACE` ⇒ `_resolve_ws()` 两个都不认,
# cwd 又是临时目录(没有 `.workbuddy/collab/`)⇒ **回落到技能包上级目录**,
# 而若那里恰好叠了真工作区 ⇒ **静默写进真 `goal.json`**(上一轮就是这么把真 title 改掉的)。
# ✅ 正解=**设 `DSH_COLLAB_WS`**(第一顺位,显式压倒一切)。
env = {**os.environ, "COLLABD_CONFIG": str(cfg).replace("\\", "/"),
"DSH_COLLAB_WS": tmp.replace("\\", "/")}
r = _sb.run([sys.executable, str(gctl)] + args, env=env, cwd=tmp,
capture_output=True, timeout=90)
# 🔴🔴🔴 2026-10-05 **兜底断言(防再犯)**:子进程**自报**的落点必须在本 tmp 内。
# 为什么要"自报"而不是"我猜":`goalctl.py` 的 `INBOX` 是**硬编码**的,夹具
# 一旦把目录猜错(本用例真犯过两次:`COLLABD_CONFIG` 不认、`inbox` 字段不读),
# 就会**静默写到别处** —— 甚至写到真工作区(上一轮真出过事故)。
# ⇒ 从子进程 stdout 里抠出它自己打印的落点,**核对前缀**;不在 tmp 内 ⇒ 直接记红。
_so = (r.stdout or b"").decode("utf-8", "replace")
_claimed = ""
for _ln in _so.splitlines():
if "落点 `" in _ln and "`" in _ln:
_claimed = _ln.split("落点 `", 1)[1].split("`", 1)[0]
_normp = lambda s: str(s or "").replace("\\", "/").strip().lower().rstrip("/")
_claimed_ok = (not _claimed) or _normp(_claimed).startswith(_normp(tmp))
try:
back = _j.loads((inbox / "goal.json").read_text(encoding="utf-8"))
except Exception as e:
back = {"__err": repr(e)}
if not _claimed_ok:
back = {"__err": "🔴 夹具越界:子进程自报落点 %r 不在 tmp %r 内 ⇒ 已污染别处!"
% (_claimed, tmp)}
return (r.returncode, _so,
(r.stderr or b"").decode("utf-8", "replace"), back)
base = {"title": "旧目标甲", "short": "甲", "lifecycle": "已完成",
"lifecycle_at": "2026-10-01T00:00:00", "acceptance_state": {"a1": "过|x"}}
# 🔴🔴 2026-10-05 **防再犯硬护栏**(上一轮真出过事故:夹具隔离失效 ⇒ 静默改真 `goal.json`):
# 跑任何写动作**前后**都量一遍**真工作区 `goal.json` 的 md5**,必须逐字不动。
# ⛔ 这不是"重复劳动" —— 它把「隔离失效」从**静默事故**变成**当场报红**。
_real_goal = Path(TEST_WS).parent / "supervise-inbox" / "goal.json"
_digest = lambda p: (hashlib.sha1(p.read_bytes()).hexdigest() if p.is_file() else "(无)")
_real_before = _digest(_real_goal)
# ① 换目标(带 --switch-goal)⇒ 必须复位
try:
d1 = _tf.mkdtemp(prefix="gl1-")
rc1, o1, e1, b1 = _run(d1, ["declare", "--title", "新目标乙", "--switch-goal", "--yes"], dict(base))
res.append(("① 换目标:rc=0(实测 rc=%s;err=%s)" % (rc1, (e1 or "")[-120:]), rc1 == 0))
res.append(("① 换目标 ⇒ 标题真换成「新目标乙」(实测 %r)" % b1.get("title"), b1.get("title") == "新目标乙"))
res.append(("🔴 换目标 ⇒ `lifecycle` 复位成「进行中」(实测 %r,改前会是『已完成』)"
% b1.get("lifecycle"), b1.get("lifecycle") == "进行中"))
res.append(("① 复位留痕(`lifecycle_by` 写明自动复位,实测 %r)"
% str(b1.get("lifecycle_by"))[:40], "自动复位" in str(b1.get("lifecycle_by") or "")))
res.append(("① 旧值存进 `lifecycle_why` 可回查(实测 %r)"
% str(b1.get("lifecycle_why"))[:50], "已完成" in str(b1.get("lifecycle_why") or "")))
res.append(("⛔ `acceptance_state` **不许被顺手清掉**(那是上一条目标的判据=证据)",
bool(b1.get("acceptance_state"))))
except Exception as e:
res.append(("① 换目标用例跑不起来:%r" % (e,), False))
# ② 对照:**纯细化**(标题不变,只补 why)⇒ lifecycle 必须原样不动
try:
d2 = _tf.mkdtemp(prefix="gl2-")
g2 = dict(base); g2["title"] = "同名目标"
rc2, o2, e2, b2 = _run(d2, ["declare", "--title", "同名目标", "--why", "补充说明", "--yes"], g2)
res.append(("② 对照·纯细化:rc=0(实测 rc=%s)" % rc2, rc2 == 0))
res.append(("🔴 对照·纯细化 ⇒ `lifecycle` **原样不动**(实测 %r;⛔ 证复位绑的是『换目标』不是『跑过 declare』)"
% b2.get("lifecycle"), b2.get("lifecycle") == "已完成"))
except Exception as e:
res.append(("② 对照用例跑不起来:%r" % (e,), False))
# 🔴🔴 硬护栏收口:真工作区 `goal.json` 必须**逐字未动**
_real_after = _digest(_real_goal)
res.append(("🔴🔴 护栏:**真工作区 `goal.json` 逐字未动**(%s ⇒ %s)"
% (_real_before[:12], _real_after[:12]),
_real_before == _real_after))
return res
def main() -> int:
only = ""
if "-k" in sys.argv:
only = sys.argv[sys.argv.index("-k") + 1]
_prepare()
_snap_prod()
cases = [v for v in globals().values() if callable(v) and hasattr(v, "_case")]
npass = nfail = nrep = 0
print("=" * 66)
print("协作机制 · 回归自测(每改一次必跑) 测试工作区:%s" % TEST_WS)
print("协作机制 · 回归自测(每改一次必跑) 测试工作区:%s" % TEST_WS)
print("=" * 66)
for fn in cases:
if only and only not in fn._case:
continue
try:
checks = fn()
except Exception as exc:
checks = [("用例抛异常:%r" % exc, False)]
bad = [c for c, ok in checks if not ok]
_ro = getattr(fn, "_report_only", False)
if bad:
nfail += 1
print(" ✗ %s" % fn._case)
for c, ok in checks:
print(" %s %s" % ("✓" if ok else "✗", c))
if _ro:
nrep += 1
nfail -= 1 # 🔴 报告型不计入失败(见 `report()` 的说明)
print(" ⓘ 报告型 ⇒ ⛔ 不计入 --verify 成败(存量问题,⛔ 与安装无关)")
else:
npass += 1
print(" ✓ %s (%d 项)" % (fn._case, len(checks)))
print("-" * 66)
print(" 合计:PASS %d / FAIL %d%s"
% (npass, nfail, ("(另有 %d 条报告型,⛔ 不计入)" % nrep) if nrep else ""))
print("=" * 66)
return 0 if nfail == 0 else 1
if __name__ == "__main__":
sys.exit(main())