feat(session-mechanism): 图模式补全自动化测试并接入技能入口

- graph/test_graphctl.py:新增 49 个用例,覆盖建图 / 校验硬错与警告 / 就绪集 /
  状态流转 / 非法操作 / 边界 / 写入范围隔离;实跑 Ran 49 tests OK(27.5s)
- graph/graphctl.py:修测试暴露的问题(+50/-10)
- graph/README.md:补「怎么跑测试」一节(+27 行)
- SKILL.md:目录树与入口指针登记 graph/(新增 4 行文档,机制行为零改动)
- 隔离性核对:collabd.py / goalctl.py / 两个看板 / 全部钩子 diff 为空;
  state.py 闸门读数绿、常驻心跳 7 秒、看板 20100 在听
This commit is contained in:
admin committed 2026-10-19 08:18:57 +08:00
1 parent 41752fd1f3
commit 4866ad8d37
4 files changed
+601 -10

No files matched your search

+41 -9
View File
@@ -42,7 +42,15 @@
## 就绪的判据(四条全中)
① 依赖全部 done ② 自己还是 pending
③ 域不与任何 running 节点重叠 ④ 不超 max_parallel
③ 域不与任何 running 节点重叠,也不与本批已选节点重叠
④ 不超 max_parallel(running 占名额)
## 状态的走法
pending --start--> running --report done--> done
|--report blocked--> blocked --start--> running(重试,attempts 已累加)
`start` 只拒 `running`(已在跑)与 `done`(已完成);`blocked` 可重开。
## 域的用法
@@ -174,7 +182,8 @@ def cmd_check(a) -> int:
if d not in nodes:
errs.append("%s 的依赖 %s 不存在" % (nid, d))
if not _norm_dom(n.get("domain")):
warns.append("%s 没有声明域 ⇒ 只能串行,不能与其他节点并行" % nid)
warns.append("%s 没有声明域 ⇒ 域比较视为不冲突,可与任意节点同批;"
"保守做法是让它单独跑" % nid)
if str(n.get("status")) not in STATUSES:
errs.append("%s 的状态非法:%r" % (nid, n.get("status")))
if not (n.get("accepts") or []):
@@ -220,12 +229,24 @@ def _ready(g: dict) -> dict:
ready.append(nid)
slots = max(0, cap - len(running))
picked = ready[:slots]
for nid in ready[slots:]:
waiting.append({"node": nid, "why": "并发上限 %d 已占满" % cap})
picked, backlog = [], []
for nid in ready:
if len(picked) >= slots:
backlog.append(nid)
waiting.append({"node": nid, "why": "并发上限 %d 已占满" % cap})
continue
dom = _norm_dom((nodes.get(nid) or {}).get("domain"))
clash = [p for p in picked
if dom and _norm_dom((nodes.get(p) or {}).get("domain"))
and _dom_overlap(dom, _norm_dom((nodes.get(p) or {}).get("domain")))]
if clash:
# 同批也不能撞域:check 已警告「永不同批」,ready 必须兑现
waiting.append({"node": nid, "why": "域与本批已选节点重叠:%s" % ", ".join(clash)})
continue
picked.append(nid)
return {"gid": g.get("gid"), "running": running, "ready": picked,
"backlog": [x for x in ready[slots:]], "waiting": waiting,
"backlog": backlog, "waiting": waiting,
"cap": cap, "slots": slots}
@@ -241,8 +262,11 @@ def cmd_start(a) -> int:
n = (g.get("nodes") or {}).get(a.node)
if not n:
raise SystemExit("没有这个节点:%s" % a.node)
if str(n.get("status")) != "pending":
raise SystemExit("节点 %s 当前是 %s,不能开工" % (a.node, n.get("status")))
if str(n.get("status")) == "running":
raise SystemExit("节点 %s 已在跑,不能重复开工" % a.node)
if str(n.get("status")) == "done":
raise SystemExit("节点 %s 已完成,不能开工" % a.node)
# pending 与 blocked 都可开工:blocked 是「这轮没过」,重试时 attempts 保留
n["status"] = "running"
_save(a.ws, a.gid, g)
_event(a.ws, a.gid, {"node": a.node, "to": "running", "by": a.by or ""})
@@ -293,7 +317,15 @@ def cmd_init(a) -> int:
nodes = {}
for i, n in enumerate(spec.get("nodes") or []):
nid = str(n.get("id") or "N%d" % (i + 1))
raw = n.get("id")
nid = str(raw).strip() if raw not in (None, "") else ""
if not nid:
k = i + 1
while ("N%d" % k) in nodes: # 自动编号要避开已占用的 id
k += 1
nid = "N%d" % k
if nid in nodes: # 显式重复 ⇒ 硬错,⛔ 不静默覆盖前一个
raise SystemExit("节点 id 重复:%s" % nid)
nodes[nid] = {
"title": str(n.get("title") or nid),
"deps": [str(x) for x in (n.get("deps") or [])],