Why: CI runners lack bwrap, so the real RDM binary refuses to start under the sandbox fail-closed policy (run 2607: serve exits right after launch); the product provides RDM_DISABLE_EXEC_TOOLS as the top-level fallback and lifecycle semantics under test are unchanged. Evidence: local rerun with the env set stays green. Impact: tests only.
208 lines
9.9 KiB
Python
208 lines
9.9 KiB
Python
"""svc.sh 真实组合门禁(U3,2026-10-12 统一部署方案)。
|
||
|
||
用**真实产物**(agent-system 组合二进制 + agent-system-rdm 组合二进制)而非 bash
|
||
桩验证部署生命周期:identity 领养/ensure/verify(真 SQLite schema_migrations +
|
||
agent_system_identity)、裸重启身份稳定、能力探测契约(capabilities v1)、
|
||
RDM 全轨部署(物化→门禁→release 切换→伴生工作台)、真实 PostgreSQL 身份
|
||
(AGENT_SYSTEM_DSN 环境交付,凭据不进命令行)。
|
||
|
||
运行条件(缺一即显式 NOT_RUN,不静默跳过):
|
||
SVC_TEST_REAL=1 —— 总开关
|
||
SVC_REAL_BIN=<agent-system> —— 真实基座/组合二进制(make build 产物)
|
||
SVC_REAL_RDM_BIN=<agent-system-rdm> —— 真实 RDM 组合二进制(make build 产物)
|
||
SVC_RDM_ORIGIN=<RDM 仓远端> —— (可选)全轨部署用;缺省仅做制品级生命周期
|
||
SVC_TEST_PG=1 + AGENT_PG_TEST_DSN —— (可选)真实 PG 身份专项
|
||
桩套件(test_svc*.py)不覆盖真实产物行为——本文件是「声明≠能力」的最后一道门。
|
||
"""
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import shutil
|
||
import sqlite3
|
||
import subprocess
|
||
import tempfile
|
||
import unittest
|
||
import urllib.request
|
||
from pathlib import Path
|
||
|
||
SOURCE = Path(__file__).resolve().parents[1]
|
||
|
||
REAL = os.environ.get("SVC_TEST_REAL") == "1"
|
||
REAL_BIN = os.environ.get("SVC_REAL_BIN", "")
|
||
REAL_RDM_BIN = os.environ.get("SVC_REAL_RDM_BIN", "")
|
||
RDM_ORIGIN = os.environ.get("SVC_RDM_ORIGIN", "")
|
||
PG = os.environ.get("SVC_TEST_PG") == "1" and bool(os.environ.get("AGENT_PG_TEST_DSN"))
|
||
|
||
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
|
||
tsr = importlib.util.module_from_spec(_spec)
|
||
_spec.loader.exec_module(tsr)
|
||
|
||
|
||
@unittest.skipUnless(REAL, "真实组合专项 NOT_RUN:需 SVC_TEST_REAL=1(详见文件头运行条件)")
|
||
class RealComboTests(tsr.SvcAutoRecoverTests):
|
||
def setUp(self):
|
||
super().setUp()
|
||
shutil.copy2(SOURCE / "scripts" / "svc_manifest.py",
|
||
self.root / "scripts" / "svc_manifest.py")
|
||
self.real_bin = Path(REAL_BIN).resolve() if REAL_BIN else None
|
||
if self.real_bin is not None:
|
||
self.assertTrue(self.real_bin.is_file() and os.access(self.real_bin, os.X_OK),
|
||
"SVC_REAL_BIN 不可执行:%s" % self.real_bin)
|
||
|
||
def manifest(self, name):
|
||
p = self.runtime(name) / "manifest.json"
|
||
return json.loads(p.read_text(encoding="utf-8")) if p.exists() else None
|
||
|
||
def identity_row(self, name):
|
||
db = self.runtime(name) / "data" / "agent-system.db"
|
||
con = sqlite3.connect(str(db))
|
||
try:
|
||
return list(con.execute(
|
||
"SELECT instance_id, dbref FROM agent_system_identity"))
|
||
finally:
|
||
con.close()
|
||
|
||
def http_ok(self, port):
|
||
try:
|
||
with urllib.request.urlopen("http://127.0.0.1:%d/" % port, timeout=3) as r:
|
||
return r.status == 200
|
||
except OSError:
|
||
return False
|
||
|
||
def seed_real(self, name, port, binary):
|
||
# CI 运行器无 bwrap:RDM 沙箱 fail-closed 会拒启——硬关 exec 工具
|
||
#(产品提供的最高兜底,RDM_DISABLE_EXEC_TOOLS=1)后生命周期语义不变。
|
||
self.env["RDM_DISABLE_EXEC_TOOLS"] = "1"
|
||
d = self.runtime(name)
|
||
(d / "bin").mkdir(parents=True, exist_ok=True)
|
||
(d / "data").mkdir(exist_ok=True)
|
||
(d / "plugins").mkdir(exist_ok=True)
|
||
shutil.copy2(binary, d / "bin" / "agent-system")
|
||
(d / "svc.env").write_text("".join("%s=%s\n" % kv for kv in [
|
||
("kind", "agent"), ("env", name), ("pid", ""),
|
||
("addr", "127.0.0.1:%d" % port), ("tags", ""),
|
||
("source", "realcombo"), ("version", "real"), ("started", ""),
|
||
("pidcheck", str(d / "bin" / "agent-system")),
|
||
("exec", "--addr 127.0.0.1:%d --data %s --plugins %s --auth auto"
|
||
% (port, d / "data", d / "plugins")),
|
||
]))
|
||
|
||
@unittest.skipUnless(REAL and REAL_BIN,
|
||
"真实基座二进制专项 NOT_RUN:需 SVC_REAL_BIN")
|
||
def test_U3_real_agent_identity_lifecycle_sqlite(self):
|
||
"""真实组合二进制:领养 ensure → verify → 两次裸重启身份/锚点稳定;
|
||
capabilities v1 探测契约。"""
|
||
self.seed_real("rc1", 9471, self.real_bin)
|
||
result = self.svc("rc1", "start")
|
||
self.assertIn("已启动", result.stdout)
|
||
m = self.manifest("rc1")
|
||
self.assertIsNotNone(m, "start 应完成 manifest 领养")
|
||
iid = m["instance_id"]
|
||
self.assertEqual(self.identity_row("rc1"), [(iid, "sqlite")],
|
||
"领养须在真实库建身份行")
|
||
# schema 是真实基座写入的(非桩)
|
||
con = sqlite3.connect(str(self.runtime("rc1") / "data" / "agent-system.db"))
|
||
self.assertTrue(con.execute(
|
||
"SELECT count(*) FROM schema_migrations").fetchone()[0] >= 1)
|
||
con.close()
|
||
self.assertTrue(self.http_ok(9471))
|
||
|
||
# capabilities v1 契约(探测无副作用形态)
|
||
cap = subprocess.run([str(self.runtime("rc1") / "bin" / "agent-system"),
|
||
"identity", "capabilities"],
|
||
capture_output=True, text=True, timeout=15)
|
||
self.assertEqual(cap.returncode, 0, cap.stderr)
|
||
self.assertIn("identity-capabilities: v1", cap.stdout)
|
||
# 错 id verify 拒绝
|
||
bad = subprocess.run([str(self.runtime("rc1") / "bin" / "agent-system"),
|
||
"identity", "verify", "--instance-id",
|
||
"svc-rc1-zzzzzz999999",
|
||
"--data", str(self.runtime("rc1") / "data")],
|
||
capture_output=True, text=True, timeout=15)
|
||
self.assertEqual(bad.returncode, 1)
|
||
|
||
self.svc("rc1", "restart")
|
||
self.svc("rc1", "restart")
|
||
self.assertEqual(self.identity_row("rc1"), [(iid, "sqlite")],
|
||
"裸重启不得换身份")
|
||
self.assertEqual(self.manifest("rc1")["instance_id"], iid)
|
||
self.assertTrue(self.http_ok(9471))
|
||
self.svc("rc1", "stop")
|
||
|
||
@unittest.skipUnless(REAL_RDM_BIN, "真实 RDM 专项 NOT_RUN:需 SVC_REAL_RDM_BIN")
|
||
def test_U3_real_rdm_binary_lifecycle_sqlite(self):
|
||
"""真实 RDM 组合二进制:identity 接线(Backend 公共 CLI)→ serve 门禁 →
|
||
领域层可初始化。"""
|
||
rdm_bin = Path(REAL_RDM_BIN).resolve()
|
||
self.seed_real("rcr", 9473, rdm_bin)
|
||
result = self.svc("rcr", "start")
|
||
self.assertIn("已启动", result.stdout)
|
||
m = self.manifest("rcr")
|
||
iid = m["instance_id"]
|
||
self.assertEqual(self.identity_row("rcr"), [(iid, "sqlite")])
|
||
self.assertTrue(self.http_ok(9473))
|
||
# RDM 未知子命令明确退出(不落服务路径)
|
||
bogus = subprocess.run([str(self.runtime("rcr") / "bin" / "agent-system"),
|
||
"totally-bogus"],
|
||
capture_output=True, text=True, timeout=10)
|
||
self.assertEqual(bogus.returncode, 2, bogus.stderr)
|
||
self.svc("rcr", "stop")
|
||
|
||
@unittest.skipUnless(RDM_ORIGIN, "全轨部署 NOT_RUN:需 SVC_RDM_ORIGIN(RDM 源仓远端)")
|
||
def test_U3_real_rdm_full_deploy_track(self):
|
||
"""全轨:materialize(真实 make build)→ 候选身份门禁 → release 切换 →
|
||
伴生工作台 200 → 裸重启稳定。"""
|
||
env = dict(self.env)
|
||
env["RDM_DISABLE_EXEC_TOOLS"] = "1"
|
||
env.update({"SVC_RDM_SRC_DIR": str(self.root / "rdmcache"),
|
||
"SVC_RDM_ORIGIN": RDM_ORIGIN})
|
||
result = self.svc("rcf", "deploy", "--from", "rdm", "--port", "9474", env=env)
|
||
self.assertIn("已切换 release", result.stdout)
|
||
d = self.runtime("rcf")
|
||
self.assertTrue((d / "current" / "bin" / "agent-system").exists())
|
||
iid = self.manifest("rcf")["instance_id"]
|
||
self.assertEqual(self.identity_row("rcf"), [(iid, "sqlite")])
|
||
# 伴生工作台(真实 web/)
|
||
addr = self.kv("rcfweb", "addr")
|
||
self.assertIsNotNone(addr, "伴生 rcfweb 未登记")
|
||
port = int(addr.rsplit(":", 1)[1])
|
||
self.assertTrue(self.http_ok(port))
|
||
self.assertIn("RDM", (d / "current" / "htdocs" / "index.html").read_text()
|
||
if (d / "current" / "htdocs" / "index.html").exists()
|
||
else "rdm", "工作台内容")
|
||
self.svc("rcf", "restart")
|
||
self.assertEqual(self.identity_row("rcf"), [(iid, "sqlite")])
|
||
self.assertTrue(self.http_ok(9474))
|
||
self.svc("rcf", "stop")
|
||
self.svc("rcfweb", "stop")
|
||
|
||
@unittest.skipUnless(PG and REAL_RDM_BIN,
|
||
"真实 PG 专项 NOT_RUN:需 SVC_TEST_PG=1 + AGENT_PG_TEST_DSN + SVC_REAL_RDM_BIN")
|
||
def test_U3_real_pg_identity_env_delivery(self):
|
||
"""真实 PostgreSQL:AGENT_SYSTEM_DSN 环境交付 ensure/verify(凭据不进
|
||
命令行);错 id fail-closed。"""
|
||
rdm_bin = Path(REAL_RDM_BIN).resolve()
|
||
data = self.root / "pgdata"
|
||
data.mkdir()
|
||
env = dict(os.environ)
|
||
env["AGENT_SYSTEM_DSN"] = os.environ["AGENT_PG_TEST_DSN"]
|
||
env.pop("SVC_STUB_TRACE", None)
|
||
iid = "svc-realcombo-pg-%d" % os.getpid()
|
||
run = lambda *a: subprocess.run(
|
||
[str(rdm_bin), "identity", *a, "--instance-id", iid,
|
||
"--dbref", "pg", "--data", str(data)],
|
||
env=env, capture_output=True, text=True, timeout=60)
|
||
r = run("ensure")
|
||
self.assertEqual(r.returncode, 0, r.stderr)
|
||
r = run("verify")
|
||
self.assertEqual(r.returncode, 0, r.stderr)
|
||
bad = subprocess.run(
|
||
[str(rdm_bin), "identity", "verify", "--instance-id",
|
||
"svc-realcombo-pg-wrong", "--dbref", "pg", "--data", str(data)],
|
||
env=env, capture_output=True, text=True, timeout=60)
|
||
self.assertEqual(bad.returncode, 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|