Merge pull request 'Require the real lifecycle gate and fault rollback' (#495) from feature/svc-d3-lifecycle-gate into main
ci / svc-realcombo (push) Successful in 1m57s
ci / test-pg (push) Successful in 2m51s
ci / test (push) Successful in 4m56s

Why: keep fault rollback and the real lifecycle gate on main.
Evidence: GATE run 2784 jobs 4032/4033/4034 success on c197b7565b.
Impact: test-pg runs make test-lifecycle. Login stays NOT_RUN.
Refs: #495
This commit was merged in pull request #495.
This commit is contained in:
2026-10-13 01:29:05 +00:00
7 changed files with 423 additions and 28 deletions
+12 -2
View File
@@ -409,15 +409,25 @@ jobs:
GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3
unset SUBMODULE_TOKEN
make -C rdm build
- name: Real-combination identity gates over real PostgreSQL (U3, required)
- name: Install Node 22
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz
tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz"
- name: Build the real agent-system binary
run: make build
- name: Real-combination lifecycle gate (make test-lifecycle, required)
env:
AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable
run: |
set -euo pipefail
# 路径变量在脚本内解析(YAML env: 不做 shell 展开)。
# 不设 SVC_RDM_ORIGIN:全轨 git 部署由 svc-realcombo 作业覆盖。
export SVC_TEST_REAL=1 SVC_TEST_PG=1
export SVC_REAL_BIN="$PWD/bin/agent-system"
export SVC_REAL_RDM_BIN="$PWD/rdm/bin/agent-system-rdm"
python3 -m unittest tests.test_svc_realcombo -v
export SVC_RDM_SRC_DIR="$PWD/rdm"
make test-lifecycle
# U3(统一部署方案):真实 Dev/RDM 制品的 svc.sh 生命周期门禁——桩套件
# (test_svc*.py)只证明脚本逻辑;本作业证明「声明≠能力」的最后一道:真实
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -113,7 +113,7 @@ test-backend-pg:
cd $(BACKEND) && go test -count=1 ./...
# 真实 Dev/RDM + PostgreSQL 生命周期门禁。缺环境直接失败,不静默跳过。
# 登录入口未就绪时套件保持失败(NOT_RUN),不把 D3 报成完成。
# 登录单独标 NOT_RUN(依赖账户方案 U7/U5),不把其余真实组合场景记成失败。
test-lifecycle:
@test -n "$${AGENT_PG_TEST_DSN}" || { echo "test-lifecycle: AGENT_PG_TEST_DSN 未设置(真实 PostgreSQL 必需,不静默跳过)"; exit 1; }
@test -n "$${SVC_RDM_SRC_DIR}" || { echo "test-lifecycle: SVC_RDM_SRC_DIR 未设置(RDM 检出必需,不静默跳过)"; exit 1; }
+1 -1
View File
@@ -41,7 +41,7 @@ U2 起实施与部署分别评审。
- [服务重启与全量数据恢复方案 r3.1](modules/documents/docs/proposals/2026-10-12-service-restart-data-recovery.md):
svc 启停、异常进程/主机退出后的依赖、配置/数据身份、任务/HITL/副作用、文件与组合备份恢复;
r2 已批准。r3.1 增补 D1 实例身份收尾、D2 可回滚切换、D3 真实组合门禁,并记录 2026-10-12 rdmcand 数据归属:会话不在 dev 库的 rdmcand 窗口内,领域事件为空,不跨实例搬迁。
D1/D2/D3 按实施计划推进到可合入;r3 其余未批条款仍待人工评审。
D1 与 D2 已合入 Backend #184、RDM #291、Dev #485。D3 的 `make test-lifecycle` 跑真实组合用例;登录仍是 NOT_RUN,依赖账户方案 U7/U5,不在本轮公共化账户 CLI。r3 其余未批条款仍待人工评审。
- [svc 统一部署与一次性登录 Key 方案 r1](modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md):
二进制/源码统一部署、环境准备、无初始密码开户与账户恢复;一次性 Key 仅兑换设密受限会话;
+1
View File
@@ -2074,6 +2074,7 @@ with open(tmp, "w") as f:
f.write(json.dumps(j, ensure_ascii=False) + "\n")
os.replace(tmp, out)
PYJ
svc_fault_check "$phase"
}
svc_journal_get() { # KEY → 值(journal 缺失/损坏 → 空)
+36 -24
View File
@@ -1,9 +1,11 @@
"""真实 Dev/RDM 组合门禁。由 make test-lifecycle 调用。
缺 PostgreSQL、RDM 源码或制品时直接失败,不静默跳过。
RDM 登录依赖账户 CLI 公共化(账户方案 U7/U5),当前标 NOT_RUN,本套件因此不能报完成。
登录依赖账户 CLI 公共化(账户方案 U7/U5),单独标 NOT_RUN,
不因此把已经跑过的真实组合场景记成失败。
"""
import os
import subprocess
import sys
from pathlib import Path
@@ -15,32 +17,42 @@ def main():
missing = []
if not os.environ.get("AGENT_PG_TEST_DSN"):
missing.append("AGENT_PG_TEST_DSN")
if not os.environ.get("SVC_RDM_SRC_DIR") or not Path(os.environ.get("SVC_RDM_SRC_DIR", "")).is_dir():
src = os.environ.get("SVC_RDM_SRC_DIR", "")
if not src or not Path(src).is_dir():
missing.append("SVC_RDM_SRC_DIR")
if not (ROOT / "bin" / "agent-system").is_file() and not os.environ.get("SVC_DEV_BIN"):
missing.append("Dev 制品(make build 或 SVC_DEV_BIN)")
dev = os.environ.get("SVC_REAL_BIN") or str(ROOT / "bin" / "agent-system")
rdm = os.environ.get("SVC_REAL_RDM_BIN", "")
if not Path(dev).is_file():
missing.append("Dev 制品(SVC_REAL_BIN 或 bin/agent-system)")
if not rdm or not Path(rdm).is_file():
missing.append("RDM 制品(SVC_REAL_RDM_BIN)")
if missing:
print("test-lifecycle 失败。缺:%s" % ", ".join(missing))
return 1
print("组合清单")
print("pin", PIN.read_text().strip())
print("login NOT_RUN:RDM 二进制没有 user/apikey,工作台登录依赖账户 CLI 公共化")
scenarios = [
"双向混合批次更新",
"无 manifest 各入口一致",
"legacy/缺行/不匹配停服前拒绝",
"两次裸重启与换端口换版本换目录",
"构建失败与启动失败回滚",
"中断对账",
"PG 绑定与跨实例错绑拒绝",
"凭据不进 cmdline/日志",
"RDM 页面、rdm.ping、升级前领域数据仍在",
"登录",
]
for name in scenarios:
state = "NOT_RUN" if missing or name == "登录" else "PENDING"
print("%s %s" % (state, name))
if missing or True:
# 登录依赖未就绪时保持未完成。缺环境同样失败。
print("test-lifecycle 未完成。缺:%s" % (", ".join(missing) if missing else "登录入口"))
return 1
print("rdm_src", src)
print("dev_bin", dev)
print("rdm_bin", rdm)
print("将运行 tests.test_svc_realcombo:SQLite 身份、RDM 二进制生命周期、PostgreSQL 身份环境交付")
if os.environ.get("SVC_RDM_ORIGIN"):
print("将运行全轨部署:SVC_RDM_ORIGIN 已设置")
else:
print("全轨部署 NOT_RUN:未设置 SVC_RDM_ORIGIN(由 svc-realcombo 作业覆盖)")
print("登录 NOT_RUN:RDM 二进制没有 user/apikey,工作台登录依赖账户方案 U7/U5")
env = os.environ.copy()
env["SVC_TEST_REAL"] = "1"
env["SVC_TEST_PG"] = "1"
env["SVC_REAL_BIN"] = dev
env["SVC_REAL_RDM_BIN"] = rdm
proc = subprocess.run(
[sys.executable, "-m", "unittest", "tests.test_svc_realcombo", "-q"],
cwd=str(ROOT), env=env)
if proc.returncode != 0:
print("test-lifecycle 失败:真实组合用例未通过")
return proc.returncode
print("test-lifecycle 可跑场景通过;登录与未设 origin 的全轨部署保持 NOT_RUN")
return 0
+47
View File
@@ -229,6 +229,53 @@ class U2ReleaseTests(tsi.IdentityTests):
# ---- T5:legacy 布局未迁移 → 裸 start/restart 仍走原路径(兼容) ----
def test_U2_fault_after_switch_rolls_back_on_next_start(self):
self.seed_agent("u2f", 9466)
self.set_makefile(self.stub)
self.svc("u2f", "deploy", "--from", "local")
v1 = self.cur_release("u2f")
self.svc("u2f", "stop")
env = dict(self.env)
env["SVC_TEST_FAULTS"] = "1"
env["SVC_FAULT_AT"] = "switched"
result = self.svc("u2f", "deploy", "--from", "local", check=False, env=env)
self.assertEqual(result.returncode, 86, result.stdout)
self.assertIn("故障注入于 switched", result.stdout)
result = self.svc("u2f", "start")
self.assertIn("已回退上一版本", result.stdout)
self.assertEqual(self.cur_release("u2f"), v1)
self.assertTrue(self.running("u2f"))
self.svc("u2f", "stop")
def test_U2_health_timeout_rolls_back(self):
self.seed_agent("u2g", 9467)
self.set_makefile(self.stub)
self.svc("u2g", "deploy", "--from", "local")
v1 = self.cur_release("u2g")
good = self.active_bin("u2g").read_bytes()
self.set_makefile(self.hang_stub())
result = self.svc("u2g", "deploy", "--from", "local", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("健康检查未通过", result.stdout)
self.assertIn("已回退上一版本", result.stdout)
self.assertEqual(self.cur_release("u2g"), v1)
self.assertEqual(self.active_bin("u2g").read_bytes(), good)
self.assertTrue(self.running("u2g"))
self.svc("u2g", "stop")
def hang_stub(self):
"""identity 可用、serve 挂起且不监听,用来触发健康检查超时。"""
p = self.root / "stub-agent-system-hang"
if not p.exists():
stub = tsr.STUB
head, sep, tail = stub.partition(" serve)")
assert sep, "STUB 缺 serve 分支锚点"
_, sep2, tail2 = tail.partition(' wait "$child"\n ;;')
assert sep2, "STUB 缺 serve 分支结尾锚点"
p.write_text(head + " serve)\n shift\n sleep 60\n exit 0 ;;" + tail2)
p.chmod(0o755)
return p
def test_U2_legacy_layout_bare_lifecycle_still_works(self):
self.seed_agent("u2e", 9465)
self.start_ok("u2e")