"""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= —— 真实基座/组合二进制(make build 产物) SVC_REAL_RDM_BIN= —— 真实 RDM 组合二进制(make build 产物) SVC_RDM_ORIGIN= —— (可选)全轨部署用;缺省仅做制品级生命周期 SVC_TEST_PG=1 + AGENT_PG_TEST_DSN —— (可选)真实 PG 身份专项 桩套件(test_svc*.py)不覆盖真实产物行为——本文件是「声明≠能力」的最后一道门。 """ import importlib.util import json import os import shutil import socket import sqlite3 import subprocess import tempfile import time import unittest import urllib.error 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", "") # CI 通道(可选):无 SSH key 的运行器上,把受信基址 + URL 级 token 头注入 # svc.sh 派生的 git 子进程(cache 克隆与 pins 物化的 submodule 取件走 SSH URL # 会被 insteadOf 改写)。token 只经环境传递,不进文件/命令行/输出。 CI_SERVER = os.environ.get("SVC_CI_GIT_SERVER", "").rstrip("/") CI_TOKEN = os.environ.get("SVC_CI_GIT_TOKEN", "") def ci_git_env(): if not (CI_SERVER and CI_TOKEN): return {} scheme = CI_SERVER.split("://", 1)[0] return { "GIT_CONFIG_COUNT": "4", "GIT_CONFIG_KEY_0": "protocol.%s.allow" % scheme, "GIT_CONFIG_VALUE_0": "always", "GIT_CONFIG_KEY_1": "http.%s/.extraheader" % CI_SERVER, "GIT_CONFIG_VALUE_1": "AUTHORIZATION: token " + CI_TOKEN, "GIT_CONFIG_KEY_2": "url.%s/weichen/.insteadOf" % CI_SERVER, "GIT_CONFIG_VALUE_2": "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/", "GIT_CONFIG_KEY_3": "core.hooksPath", "GIT_CONFIG_VALUE_3": "/dev/null", } 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")), ])) self.initialize_store(binary, d / "data") def initialize_store(self, binary, data, env=None): """Create the fixture schema through normal startup before identity CLI. identity ensure/verify operate on an existing store and deliberately do not create a missing database or migrate an uninitialized one. """ init_env = dict(self.env if env is None else env) init_env["RDM_DISABLE_EXEC_TOOLS"] = "1" init_env.setdefault("SUPER_ADMIN_OWNER", "superadmin") init_env.setdefault("SUPER_ADMIN_PASSWORD", "realcombo-super-admin-password") if env is None: init_env.pop("AGENT_SYSTEM_DSN", None) with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] log_path = self.root / ("initialize-%s.log" % data.name) with log_path.open("wb") as log: proc = subprocess.Popen( [str(binary), "serve", "--addr", "127.0.0.1:%d" % port, "--data", str(data), "--auth", "auto"], cwd=self.root, env=init_env, stdout=log, stderr=log) try: for _ in range(200): self.assertIsNone(proc.poll(), "fixture startup exited; see %s" % log_path) try: urllib.request.urlopen( "http://127.0.0.1:%d/api/health" % port, timeout=1).close() break except urllib.error.HTTPError as response: status = response.code response.close() if status == 401: break # Authenticated health returning 401 is ready too. self.fail("fixture health returned %d; see %s" % (status, log_path)) except OSError: time.sleep(0.1) else: self.fail("fixture startup timed out; see %s" % log_path) finally: proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() proc.wait(timeout=10) @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) doc = json.loads(cap.stdout) self.assertEqual(doc["protocol"], 1) self.assertEqual(doc["identity"]["open"], "existing-no-migrate") self.assertIn("ensure", doc["identity"]["ops"]) self.assertIn("verify", doc["identity"]["ops"]) # 错 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 → 裸重启稳定。""" # 硬关须覆盖本测试全部调用(deploy/start/restart/stop 的 serve 都要), # 不只 deploy 一处——CI 无 bwrap 时 fail-closed 拒启。 self.env["RDM_DISABLE_EXEC_TOOLS"] = "1" env = dict(self.env) env.update(ci_git_env()) 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, check=False) if result.returncode != 0: log = self.runtime("rcf") / "svc.log" tail = log.read_text(encoding="utf-8", errors="replace")[-2000:] \ if log.exists() else "(no svc.log)" self.fail("deploy failed:\n%s\n--- svc.log tail ---\n%s" % (result.stdout, tail)) 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" plugins = data / "plugins" plugins.mkdir(parents=True) env = dict(os.environ) env["AGENT_SYSTEM_DSN"] = os.environ["AGENT_PG_TEST_DSN"] env["RDM_DISABLE_EXEC_TOOLS"] = "1" env.pop("SVC_STUB_TRACE", None) self.initialize_store(rdm_bin, data, env) 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()