Files
dsh_ai1net_server/归档/技能-退役-20261001/multi-session-collab/scripts/selftest.py
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

1256 lines
72 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
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 r"E:/ProgramData/AIProject/ai1net-dsh-server")
TEST_WS = WS / "tmp" / "selftest"
TIN = TEST_WS / "inbox"
CFG = TEST_WS / "collabd.config.json"
PY = sys.executable
RESULTS = []
def _mk_env() -> dict:
env = dict(os.environ)
env["COLLABD_CONFIG"] = str(CFG)
env["DSH_COLLAB_WS"] = str(TEST_WS)
return env
def _prepare() -> None:
TIN.mkdir(parents=True, exist_ok=True)
for _f in ("wake.lock", "guard.stop", "goal.pending.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 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",
"下载 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),
("🔴 collabd 的主会话候选判据=**按角色排**(⛔ 不再只排 `[协作]`)",
'_role not in ("worker", "waker")' in sa),
("🔴 board 同款", '_role_of_title(t) not in ("worker", "waker")' in sb),
]
@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("目标三态:『没有判据』⛔ 不等于『全过』(防『读不到被当成已完成』)")
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("台账四态: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 只写一行、--tick ⛔ 不投递(防『人为停掉被读成机制坏了』)")
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` 经钩子**注入每个会话每一轮**。
⇒ 本用例钉死两件事:① 已停时**只留一行"已停"** 并把告警投影清掉 ② 已停时 `--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"))
rc2, out2 = run_cli(["--tick"])
ok_no_wake = wakeups_n() == w0
ok_tick = ("paused" in out2)
# 恢复 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),
("已停时 --tick ⛔ 不投递(%d→%d)" % (w0, wakeups_n()), ok_no_wake and rc2 == 0),
("--tick 明确印 paused(⛔ 不静默)", 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():
m = imp()
sid = os.environ.get("CODEBUDDY_SESSION_ID") or ""
if not sid:
return [("SKIP:环境里取不到会话 id", True)]
# 🔴 2026-09-30 修(**同族假红**):本用例的前提是"主会话**此刻**在 `working`"——
# 而主会话在**空闲回落**时 `sessions.status='completed'`(实测:`_session_status(fe146dd9)='completed'`)
# ⇒ 前提不成立 ⇒ 我原来会判**假红**。⇒ 与"对账/幂等"两条同族处理:**前提不成立就 SKIP**
# (⛔ 不假装通过、也⛔ 不判假红)—— ✅ 正确的红是"目标在跑却没被延后"。
_st_now = m._session_status(sid)
if _st_now != "working":
return [("SKIP:主会话当前 `%s`(≠working)⇒ target-busy 前提不成立" % (_st_now or "?"), True)]
st = {"roles": {sid: "main"}}
r = m._deliver_str("自测:不该被投出", "selftest", st)
return [("skipped==target-busy", r.get("skipped") == "target-busy")]
@case("投递:跨进程互斥锁存在且会被释放")
def t_lock_released():
m = imp()
m._deliver_str("自测", "selftest", {"roles": {}})
return [("wake.lock 未残留", not (TIN / "wake.lock").exists())]
# ── 🔴 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 _fake_cfg(sizes: dict, day_offset: int = 0) -> Path:
"""在**测试区内**造一份假 `logs/<日期>/sdk/conversations/`(⛔ 生产日志目录一个字节不动)。
`_deaf_sids()` 只读 `CODEBUDDY_CONFIG_DIR/logs/...` ⇒ 换掉配置根即可**完全离线**复现本坑。
用 `truncate` 建**稀疏文件**:秒级拿到 10 MiB 的**体积读数**(`getsize`),⛔ 不真写 10 MiB。
`day_offset`:0=今天(默认)、1=昨天 —— 🔴 **按天分目录**正是"永久判哑"那个坑的土壤。
"""
_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():
with open(root / (_sid + ".log"), "wb") as f:
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():
"""🔴 这条钉住的正是**假绿**:旧实现向已哑会话投递**仍记 `ok:true · http:200`**。
判据三件:① 判读是 `target-deaf` ② 投递台账**零增量** ③ 已把"该点哪一下"落到 `NEED-USER.md`。"""
DEAF = "deadbeef-0000-4000-8000-000000000001"
cfg = _fake_cfg({DEAF: DEAF_MIB + 100_000})
w0 = wakeups_n()
restore = _with_fake_cfg(cfg)
try:
m = imp()
# `roles` 登记成 main ⇒ 第一道闸(按登记目标)即应命中(⛔ 不必等网关那一遍)
r = m._deliver_str("自测:哑会话不该被投", "selftest-deaf", {"roles": {DEAF: "main"}})
_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)
rc, out = run_cli_env(["--tick"], env)
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("投递(新):--tick 在位、可跑、且**只由它投递**")
def t_tick_wired():
m = imp()
known = "--tick" in {"--where", "--once", "--supervise", "--tick", "--report", "--reqs"}
rc, out = run_cli(["--tick"])
src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
hook = Path(r"E:/ProgramData/AIProject/ai1net-dsh-server/.workbuddy/tools/wb-result-hook.py")
hook_ok, hook_note = True, "(钩子文件不在,跳过)"
if hook.exists():
hs = hook.read_text(encoding="utf-8", errors="replace")
hook_ok = ("--tick" in hs) and ("maybe_run_supervisor_tick" in hs)
hook_note = "钩子里有 `--tick` 调用"
return [("`--tick` 属已知参数", known),
("`--tick` 可跑 rc==0", rc == 0),
("投递方唯一:`--tick` 用 mutate=True 调用 supervise", "deliver=True)" in src or "deliver=True, mutate" in src or "supervise(st, deliver=True)" in src),
(hook_note, hook_ok)]
@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("结构:投递方唯一(只有 --tick 能推进队列)+ 旧投递路径已停用")
def t_deliver_switch():
src = (HERE / "collabd.py").read_text(encoding="utf-8")
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),
("`--tick` 是唯一投递方(supervise(st, deliver=True))",
re.search(r"supervise\(st,\s*deliver=True\)", src) is not None),
("wake_round 已标注停用", "已停用" in src)]
@case("主会话:**解析出来的**(不只是读登记)+ ⛔ 拒绝盲投(防『投错窗口』回潮)")
def t_main_resolution():
"""用户 2026-09-30:「协作机制能发现 主会话 换了吗」——旧答案是**不能**(登记静态+路径依赖,
且看板与投递共用同一份登记 ⇒ 一起钉在旧 id 上)。本用例把修法钉住。"""
src = (HERE / "collabd.py").read_text(encoding="utf-8")
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-01 从「只排 `[协作]`」扩到**按角色排**)"
"—— 接续会话/唤醒会话都是干活的棒,⛔ 不是主会话候选(否则接续棒冒充主会话 ⇒ 通知投给它自己)",
'_role not in ("worker", "waker")' 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),
("🆕 投递侧已接 `resolve_mains`(⛔ 不再是「唯一那一条」)",
re.search(r"_rms = resolve_mains\(st,", src) is not None),
("🆕 投递目标经 `main_for_topic()` **按类别**选", "main_for_topic(_rms, topic)" in src),
("🆕 `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),
("🆕 上报投递**带上件所属类别**(`topic=` 取台账条目的 `line`)",
'topic=str((v or {}).get("line") or "")' in src),
("🆕 归属判据能吃**类别集合**(`_in_project` 的 main_sid / short 都走 `_as_list()`)",
"def _as_list(" in src and "for _t in _as_list(short):" in src),
("🆕 收尾确认**排除全部主会话**(⛔ 否则别类别的主会话被算成「在跑」⇒ 永远拦住)",
'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_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("看板:任务类别**来源**必须说得出口(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]
return [
("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),
]
@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))
# ② board.py 源码零项目串
src = (SK / "board.py").read_text(encoding="utf-8")
proj_pat = re.compile(r"(dsh-worker|dsh-client|ai1net|手机接入|20090|bdf89014)")
hits = []
for i, line in enumerate(src.splitlines(), 1):
s = line.strip()
if s.startswith("#"): # 注释里可以讲"为什么这样分层",但代码里不行
continue
if proj_pat.search(line):
hits.append("board.py:%d" % 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 代码里零项目串(命中:%s)" % (hits[:5] or "无"), not hits),
("看板不回读已废弃的项目键(残留:%s)" % (stale or "无"), not stale)]
@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)]
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 = 0
print("=" * 66)
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]
if bad:
nfail += 1
print(" ✗ %s" % fn._case)
for c, ok in checks:
print(" %s %s" % ("✓" if ok else "✗", c))
else:
npass += 1
print(" ✓ %s (%d 项)" % (fn._case, len(checks)))
print("-" * 66)
print(" 合计:PASS %d / FAIL %d" % (npass, nfail))
print("=" * 66)
return 0 if nfail == 0 else 1
if __name__ == "__main__":
sys.exit(main())