Files
Agent-System-Dev/tests/test_svc_realcombo.py
T
weichen e7b969a905 Retain existing-store lifecycle tests during main synchronization
Why: Main D1 now shares the same explicit fixture initialization contract.

Evidence: Both real SQLite and temporary PostgreSQL initialization paths
were independently replayed; capabilities JSON assertions are preserved.

Impact: Resolve duplicate PG setup through the tested helper; preserve
all main lifecycle changes and the rollback directory protection.

Refs: Dev #483/#485; D1 existing-store identity contract
2026-10-12 15:57:05 +00:00

296 lines
14 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.
"""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 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"
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", "on"],
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()