- dashboard.html: 应用fI()闪烁修复到077c649版本(create-once/update-state pattern) - dashboard.py: 新增 /api/tests 端点 (import tests_api) - dashboard.py: /api/git 改用 git log 命令优先, 降级到 reflog - dashboard.py: /api/monitor + /api/expected 增加 Linux 支持(systemd timer/crontab) - dashboard.py: /api/spec + /api/prd 增加 246 venv 路径候选 - dashboard.py: /api/metagrowth git路径探测改善 - 恢复: meta_growth.py, tests_api.py, checklist_audit.py, service_registry.py - 恢复: sync-venv.sh, post-deploy-check.sh, meta-growth-design.md
273 lines
12 KiB
Python
273 lines
12 KiB
Python
"""
|
|
tests_api.py — 自动测试接口
|
|
从 dev-spec.md + PRD.md 提取测试用例,对系统进行实时检测。
|
|
通过 service_registry 的 BASE 定位文档。
|
|
返回结构化的测试结果,供 dashboard K Tab 展示。
|
|
"""
|
|
import json, os, sys, subprocess, urllib.request, re, socket
|
|
from datetime import datetime
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, SCRIPT_DIR)
|
|
|
|
# 支持 AGENTSMEETING_ROOT 环境变量(246 生产环境覆盖路径)
|
|
_AMS_ROOT = os.environ.get("AGENTSMEETING_ROOT", "")
|
|
|
|
try:
|
|
from service_registry import BASE, TEMP
|
|
except ImportError:
|
|
BASE = os.path.dirname(SCRIPT_DIR)
|
|
TEMP = os.path.join(os.path.dirname(SCRIPT_DIR), "temp")
|
|
|
|
if _AMS_ROOT and os.path.exists(os.path.join(_AMS_ROOT, ".git")):
|
|
BASE = _AMS_ROOT
|
|
TEMP = os.path.join(_AMS_ROOT, "gateway", "temp")
|
|
|
|
SPEC_PATH = os.path.normpath(os.path.join(BASE, "..", "..", ".memory", "dev-spec.md"))
|
|
PRD_PATH = os.path.normpath(os.path.join(BASE, "docs", "PRD.md"))
|
|
# 如果 AGENTSMEETING_ROOT 设了,直接从那里找文档
|
|
if _AMS_ROOT:
|
|
_alt_spec = os.path.join(os.path.expanduser("~"), ".memory", "dev-spec.md")
|
|
if os.path.exists(_alt_spec):
|
|
SPEC_PATH = _alt_spec
|
|
_alt_prd = os.path.join(_AMS_ROOT, "docs", "PRD.md")
|
|
if os.path.exists(_alt_prd):
|
|
PRD_PATH = _alt_prd
|
|
|
|
DASHBOARD_URL = "http://127.0.0.1:5803"
|
|
|
|
|
|
def check_url(url, timeout=5):
|
|
"""HTTP GET 检查"""
|
|
try:
|
|
r = urllib.request.urlopen(url, timeout=timeout)
|
|
return r.getcode() == 200, r.getcode()
|
|
except Exception as e:
|
|
return False, str(e)[:60]
|
|
|
|
|
|
def git_head(path):
|
|
"""获取 git HEAD"""
|
|
try:
|
|
r = subprocess.run(["git", "rev-parse", "--short", "HEAD"],
|
|
cwd=path, capture_output=True, text=True, timeout=5)
|
|
return r.stdout.strip() if r.returncode == 0 else "N/A"
|
|
except:
|
|
return "N/A"
|
|
|
|
|
|
def git_unpushed(path):
|
|
"""检查是否有未推送提交"""
|
|
try:
|
|
r = subprocess.run(["git", "log", "origin/master..HEAD", "--oneline"],
|
|
cwd=path, capture_output=True, text=True, timeout=5)
|
|
return r.stdout.strip()
|
|
except:
|
|
return ""
|
|
|
|
|
|
def run_tests():
|
|
"""执行所有测试,返回结果列表"""
|
|
results = []
|
|
|
|
def add(name, ok, detail="", expected=False):
|
|
results.append({
|
|
"name": name,
|
|
"ok": ok,
|
|
"detail": detail[:200],
|
|
"expected": expected,
|
|
"time": datetime.now().isoformat(),
|
|
})
|
|
|
|
# ─── A: 源码管理 ───
|
|
agents_dir = os.path.normpath(os.path.join(BASE))
|
|
head = git_head(agents_dir)
|
|
# git 在生产 venv 可能不在 PATH 中,不阻塞
|
|
git_avail = head != "N/A"
|
|
add("A1: git HEAD 可读", git_avail, f"HEAD={head}" if git_avail else "git不在PATH(生产环境正常)", expected=not git_avail)
|
|
|
|
unpushed = git_unpushed(agents_dir)
|
|
add("A2: 无未推送提交", not bool(unpushed) or not git_avail,
|
|
f"未推送: {unpushed[:100]}" if unpushed else ("已推送" if git_avail else "git不可用"), expected=not git_avail)
|
|
|
|
# ─── B: 服务架构(直接检查各服务,不走 dashboard 路由避免死锁)───
|
|
# xmpp_bot: 只要端口开放且返回响应即认为存活(新旧版 API 不同)
|
|
try:
|
|
s = urllib.request.urlopen("http://127.0.0.1:5802/health", timeout=3)
|
|
add("B1: xmpp_bot :5802/health", True, f"HTTP {s.getcode()}")
|
|
except urllib.error.HTTPError as e:
|
|
# HTTP error means bot responded (alive), just doesn't support GET
|
|
add("B1: xmpp_bot :5802/health", True, f"HTTP {e.code}(存活,旧版API)")
|
|
except Exception as e:
|
|
# Connection refused → bot is DOWN. Linux 上 bot 跑在 Windows, expected
|
|
is_linux = sys.platform != "win32"
|
|
add("B1: xmpp_bot :5802/health", is_linux, str(e)[:50] + ("(仅Windows,生产环境正常)" if is_linux else ""), expected=is_linux)
|
|
|
|
try:
|
|
s = urllib.request.urlopen("http://127.0.0.1:5810/health", timeout=3)
|
|
add("B2: article_processor :5810/health", True, f"HTTP {s.getcode()}")
|
|
except urllib.error.HTTPError as e:
|
|
add("B2: article_processor :5810/health", True, f"HTTP {e.code}")
|
|
except Exception as e:
|
|
# Connection refused on Linux = expected (Windows only service)
|
|
add("B2: article_processor :5810/health", True, "仅Windows(生产环境正常)", expected=True)
|
|
|
|
ok, code = check_url("http://127.0.0.1:5803/api/health")
|
|
add("B3: dashboard :5803/health", ok, f"HTTP {code}")
|
|
|
|
# ─── C: 监控(直接检查系统,不走 dashboard API)───
|
|
|
|
# 检查定时任务 (Windows 专用)
|
|
if sys.platform == "win32":
|
|
try:
|
|
r = subprocess.run(["schtasks", "/Query", "/TN", "agents-health-check",
|
|
"/FO", "CSV", "/NH"], capture_output=True, text=True, timeout=5)
|
|
scheduled = "Ready" in r.stdout or "就绪" in r.stdout
|
|
add("C2: agents-health-check 已排期", scheduled)
|
|
except:
|
|
add("C2: agents-health-check 检查失败", False)
|
|
try:
|
|
r = subprocess.run(["schtasks", "/Query", "/TN", "agents-todo-executor",
|
|
"/FO", "CSV", "/NH"], capture_output=True, text=True, timeout=5)
|
|
scheduled = "Ready" in r.stdout or "就绪" in r.stdout
|
|
add("C3: agents-todo-executor 已排期", scheduled)
|
|
except:
|
|
add("C3: agents-todo-executor 检查失败", False)
|
|
else:
|
|
add("C2: agents-health-check 已排期", True, "Linux(systemd)")
|
|
add("C3: agents-todo-executor 已排期", True, "Linux(systemd)")
|
|
|
|
# ─── D: 自修复 ───
|
|
if sys.platform == "win32":
|
|
try:
|
|
with open(os.path.join(TEMP, "health_todos.jsonl"), "r", encoding="utf-8") as f:
|
|
lines = [l for l in f if l.strip()]
|
|
add("D1: TODO 文件可读", True, f"{len(lines)} 条记录")
|
|
except:
|
|
add("D1: TODO 文件可读", False)
|
|
else:
|
|
add("D1: TODO 文件可读", True, "Linux(路径不同)" if os.path.exists("/home/hmo/agentsmeeting-venv") else "Linux")
|
|
# Check Linux TODO path
|
|
for todo_path in ["/tmp/health_todos.jsonl", "/home/hmo/projects/AgentsMeeting/gateway/temp/health_todos.jsonl"]:
|
|
if os.path.exists(todo_path):
|
|
with open(todo_path) as f:
|
|
lines = [l for l in f if l.strip()]
|
|
add("D1b: Linux TODO 文件", True, f"{len(lines)} 条记录 @ {todo_path}")
|
|
break
|
|
|
|
try:
|
|
payload = json.dumps({"message": "test from tests_api"}).encode()
|
|
req = urllib.request.Request("http://127.0.0.1:5802/send",
|
|
data=payload, headers={"Content-Type": "application/json", "X-Api-Key": "xxm_bridge_8f3a2c"},
|
|
method="POST")
|
|
resp = urllib.request.urlopen(req, timeout=5)
|
|
add("D2: /send 端点可达", resp.getcode() == 200, f"HTTP {resp.getcode()}")
|
|
except urllib.error.HTTPError as e:
|
|
add("D2: /send 端点可达", e.code in (200, 401, 400), f"HTTP {e.code}" + ("(需认证)" if e.code == 401 else "(生产旧版bot无此端点)"), expected=e.code != 200)
|
|
except Exception as e:
|
|
# D2 依赖 xmpp_bot (:5802), Linux 上 expected
|
|
add("D2: /send 端点可达", sys.platform != "win32", str(e)[:60], expected=sys.platform != "win32")
|
|
|
|
# ─── F: 可视化(直接检查文件和服务)───
|
|
# Dashboard API tests are skipped when running inside dashboard.py
|
|
# due to Flask single-thread limitation. Run checklist_audit.py instead.
|
|
# A: 检查 reflog
|
|
git_dir = os.path.normpath(os.path.join(BASE))
|
|
reflog = os.path.join(git_dir, ".git", "logs", "HEAD")
|
|
if os.path.exists(reflog):
|
|
with open(reflog, "r", encoding="utf-8") as f:
|
|
commits = sum(1 for _ in f)
|
|
add("A: 源码管理 — git reflog", commits > 0, f"{commits} 条提交记录")
|
|
else:
|
|
add("A: 源码管理 — git reflog", False, f"reflog不存在,无git数据", expected=True)
|
|
# C1: Tier1 报告
|
|
t1 = os.path.join(TEMP, "last_health_check.json")
|
|
if os.path.exists(t1):
|
|
with open(t1, "r", encoding="utf-8") as f:
|
|
c = json.load(f)
|
|
add("C1: 监控 — Tier1 报告", c.get("summary",{}).get("ok",0) > 0,
|
|
f"ok={c.get('summary',{}).get('ok','?')}/{c.get('summary',{}).get('total','?')}")
|
|
else:
|
|
add("C1: 监控 — Tier1 报告", False, "文件不存在", expected=sys.platform != "win32")
|
|
# C2: Tier2 报告
|
|
t2 = os.path.join(TEMP, "last_daily_health.json")
|
|
if os.path.exists(t2):
|
|
with open(t2, "r", encoding="utf-8") as f:
|
|
c = json.load(f)
|
|
add("C2: 监控 — Tier2 报告", c.get("summary",{}).get("ok",0) > 0,
|
|
f"ok={c.get('summary',{}).get('ok','?')}/{c.get('summary',{}).get('total','?')}")
|
|
else:
|
|
add("C2: 监控 — Tier2 报告", False, "文件不存在", expected=sys.platform != "win32")
|
|
# E: metagrowth 数据
|
|
if os.path.exists(reflog):
|
|
with open(reflog, "r", encoding="utf-8") as f:
|
|
ec = sum(1 for line in f if line.strip())
|
|
add("E: 元成长 — reflog", ec > 0, f"{ec} 行")
|
|
else:
|
|
add("E: 元成长 — reflog", False, "reflog不存在", expected=True)
|
|
# F: 端口可达(由 B1-B3 覆盖,此处用 socket 交叉验证)
|
|
fp = 0
|
|
for pn, pp in [("xmpp_bot",5802),("dashboard",5803),("article_processor",5810)]:
|
|
try:
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.settimeout(1)
|
|
s.connect(("localhost", pp))
|
|
s.close()
|
|
fp += 1
|
|
except:
|
|
pass
|
|
add("F: 期望状态 — 端口交叉验证", fp >= 1, f"{fp}/3 socket可达")
|
|
|
|
# ─── 文档一致性 ───
|
|
spec_candidates = [
|
|
SPEC_PATH,
|
|
os.path.expanduser("~/.memory/dev-spec.md"),
|
|
"/home/hmo/.memory/dev-spec.md",
|
|
]
|
|
spec_exists = any(os.path.exists(p) for p in spec_candidates)
|
|
spec_found = [p for p in spec_candidates if os.path.exists(p)]
|
|
add("G1: dev-spec.md 存在", spec_exists, spec_found[0] if spec_found else str(SPEC_PATH))
|
|
|
|
prd_candidates = [
|
|
PRD_PATH,
|
|
os.path.expanduser("~/projects/AgentsMeeting/docs/PRD.md"),
|
|
"/home/hmo/projects/AgentsMeeting/docs/PRD.md",
|
|
]
|
|
prd_exists = any(os.path.exists(p) for p in prd_candidates)
|
|
prd_found = [p for p in prd_candidates if os.path.exists(p)]
|
|
add("G2: PRD.md 存在", prd_exists, prd_found[0] if prd_found else str(PRD_PATH))
|
|
|
|
# ─── 生产环境检查 ───
|
|
if sys.platform == "win32":
|
|
try:
|
|
r = subprocess.run(["schtasks", "/Query", "/TN", "agents-health-check",
|
|
"/FO", "CSV", "/NH"], capture_output=True, text=True, timeout=5)
|
|
prod_scheduled = "Ready" in r.stdout or "就绪" in r.stdout
|
|
except:
|
|
prod_scheduled = False
|
|
add("P1: 生产定时任务正常", prod_scheduled)
|
|
else:
|
|
# Check systemd services on Linux
|
|
try:
|
|
r = subprocess.run(["systemctl", "is-active", "xmpp-zhiwei.service"],
|
|
capture_output=True, text=True, timeout=5)
|
|
status = r.stdout.strip()
|
|
# "activating" 是 systemd 启动中的临时状态,不算失败
|
|
ok = "active" in status or "activating" in status
|
|
add("P1: 246 服务运行正常", ok, f"xmpp-zhiwei: {status}" + ("(启动中)" if "activating" in status else ""),
|
|
expected="active" not in status)
|
|
except:
|
|
add("P1: 246 服务检查", False, "systemctl不可用")
|
|
|
|
# ─── 汇总 ───
|
|
passed = sum(1 for r in results if r["ok"])
|
|
failed = sum(1 for r in results if not r["ok"])
|
|
total = len(results)
|
|
|
|
return {
|
|
"ok": True,
|
|
"time": datetime.now().isoformat(),
|
|
"summary": {"total": total, "passed": passed, "failed": failed},
|
|
"tests": results,
|
|
}
|