Merge authoritative main into the task branch

Why: include the current integration baseline before verification.

Evidence: fetched origin/main=719bfe945f46d43a22d7ac7025d2313faa4bf0e4.

Impact: integrate upstream changes without rewriting task history.

Refs: task-sync
This commit is contained in:
2026-10-13 05:02:14 +00:00
8 changed files with 1268 additions and 26 deletions
+41 -5
View File
@@ -387,7 +387,7 @@ jobs:
# 凭据纪律与 Checkout 块同源:经 GIT_CONFIG_* 环境注入 URL 级 token 头,
# 不落文件/命令行;allowlist 扩项 = Agent-System-RDM 及其 pins(同一
# 组件集合),由本 PR 评审背书。SSH pins URL 以 insteadOf 改写到受信
# https 基址后再取。
# https 基址后再取。只追加 pin 的三个 40 位 hex,不 source pin 文件。
SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
scheme="${GITHUB_SERVER_URL%%://*}"
export GIT_CONFIG_COUNT=4
@@ -400,15 +400,45 @@ jobs:
export GIT_CONFIG_KEY_3="core.hooksPath"
export GIT_CONFIG_VALUE_3="/dev/null"
git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm
# 凭据环境须保持到 pins 子模块取完(insteadOf + token 头覆盖嵌套 SSH URL),
# 之后再 unset。
git -C rdm submodule update --init --recursive
mapfile -t pins < <(python3 - <<'PY'
import pathlib
got = {}
for line in pathlib.Path("tests/integration/lifecycle/rdm.pin").read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key not in ("sha", "prev_sha", "legacy_sha") or len(value) != 40:
raise SystemExit("rdm.pin rejected")
if any(c not in "0123456789abcdef" for c in value):
raise SystemExit("rdm.pin rejected")
got[key] = value
if set(got) != {"sha", "prev_sha", "legacy_sha"}:
raise SystemExit("rdm.pin rejected")
print(got["prev_sha"])
print(got["legacy_sha"])
print(got["sha"])
PY
)
[ "${#pins[@]}" -eq 3 ]
# depth=1 会把三个 pin 变成互不相关的浅根,update 会把 prev→sha 误判成分叉。
# 当前 first-parent 距离是 39,200 留余量,仍不拉全史。
git -C rdm fetch --depth=200 origin "${pins[@]}"
for rev in "${pins[@]}"; do
git -C rdm checkout --detach "$rev"
git -C rdm submodule update --init --recursive
done
echo "RDM commit: $(git -C rdm rev-parse HEAD)"
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \
GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \
GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3
unset SUBMODULE_TOKEN
make -C rdm build
: > rdm/pin-build-digests.txt
for rev in "${pins[@]}"; do
git -C rdm checkout --detach "$rev"
make -C rdm build
sha256sum rdm/bin/agent-system-rdm | awk -v rev="$rev" '{print rev, $1}' >> rdm/pin-build-digests.txt
done
- name: Install Node 22
run: |
set -euo pipefail
@@ -419,14 +449,20 @@ jobs:
- 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
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# 路径变量在脚本内解析(YAML env: 不做 shell 展开)。
# 不设 SVC_RDM_ORIGIN:全轨 git 部署由 svc-realcombo 作业覆盖。
# pins 物化没有 SSH known_hosts。token 只经 SVC_CI_GIT_TOKEN 进
# ci_git_env 的 URL 级头,不写文件。
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"
export SVC_RDM_SRC_DIR="$PWD/rdm"
export SVC_CI_GIT_SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
export SVC_CI_GIT_TOKEN="$SUBMODULE_TOKEN"
unset SUBMODULE_TOKEN
make test-lifecycle
# U3(统一部署方案):真实 Dev/RDM 制品的 svc.sh 生命周期门禁——桩套件
@@ -1,6 +1,6 @@
# feature/u7-release-roadmap 任务卡
任务主记录:<PR 链接,交付前填写>;状态:进行中;owner:weichen
任务主记录:PR #501 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/501(merge d40d1c2e5d);状态:已完成(回填);owner:weichen
## 基线与授权
@@ -52,7 +52,7 @@
| 正式测试与验收 | 1248227cb | 通过(连续授权) | 用户 | 2026-10-13T03:26Z | `make test` exit 0(SELF,证据表)。首次跑因 18795 被无关临时进程占用而失败,已记录;重跑通过。CI GATE 待 PR HEAD |
| 代码 Review | dd77d27b57 | 通过 | 智能体审阅 | 2026-10-13T03:32Z | PR #501;纯文档,证据等级未抬高,无 blocker |
| 合入 | 3090dd903b | 通过(连续授权) | 智能体 | 2026-10-13T03:52Z | 运行 2821 第二次尝试三项 success,执行器 home-runner,SHA 一致 |
| 合入核验与收口 | 待定 | 未评审 | — | — | — |
| 合入核验与收口 | d40d1c2e5d | 通过(连续授权) | 智能体 | 2026-10-13T04:21Z | 合并提交与 HEAD 72763c9 同树;收口按授权执行 task-finish 与 task-close |
## 探索试验与迭代
@@ -72,7 +72,7 @@
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|---|---|---|---|---|---|
## 结果与验证(完成后填写)
## 结果与验证
- 结果:规格已写入 `docs/u7-release-replay.md`、`docs/u7-release-ci-feasibility.md`、`docs/u7-release-c3-host.md`,检查单增加「下一步规格」且证据等级未改。
- 验证:远端 PR 与 CI 运行的读取为 PASS(证据表,SELF)。重放、CI job、双主机均为 NOT_RUN。第一次核验命令因 Documents 目录 API 404 退出 1,同树结论以随后退出码 0 的那次为准。`make task-sync` 与 `make test` 的输出在证据表(SELF);`make test` 首次因端口 18795 被无关临时进程占用失败,释放后重跑 exit 0。权威测试结论仍是 PR HEAD 的 CI。
@@ -93,7 +93,7 @@
- Review 记录:智能体审阅 approve,无 blocker。对象 base `3fb8210f6495a78e70a67af3661bdfe9c00fb9de`、head `dd77d27b579d827705a43bab9456251cb0349341`(PR #501)。核对:相对 main 只有 4 份文档与任务卡;三份规格均标明未执行,检查单证据等级保持 SELF 或未开始;无凭据值;`.gitea/workflows/ci.yaml`、`policy/facts.md` 与 Documents 仓未改。需求符合性:R1–R4 的产物都在,重放、CI job、双主机仍为 NOT_RUN。问题闭合:无。重放依据:文档任务,审阅方式为 diff 与规则一致性核对,无业务重放。
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
- CI 记录:HEAD `3090dd903b720915b7aa3c3f224b64d8344e5dfc` 的 [运行 2821](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/actions/runs/2821) 第二次尝试(jobs 4117 test、4118 test-pg、4119 svc-realcombo)均为 success,执行器 `home-runner`(runner id 3),head_sha 与该 HEAD 一致(GATE)。第一次尝试的 job 4107(test)失败:`test_legacy_exec_dsn_survives_consecutive_restarts` 报「进程启动后即退出」,与本次文档改动无关;同一时段相邻 PR 的运行也有不同用例的同类时序失败,重跑通过。合入前 `make task-sync` 确认分支含 origin/main `3fb8210f`(SELF,证据表)。
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
- 合入记录:PR [#501](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/501) 合入 main,合并提交 `d40d1c2e5d29bb0ce429edf2221d9c4e4bcb0dcb`。被核验 HEAD `72763c9c6b12a43b8a5d3aeda2907a613949732e` 的 [运行 2832](https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/actions/runs/2832) 中 test(job 4140)、test-pg(job 4141)、svc-realcombo(job 4142)均为 success,执行器 `home-runner`(GATE)。本地 `git rev-parse ^{tree}` 显示合并提交与该 HEAD 同树(`3cf49f39`)。合入前分支已并入 origin/main `268ef29`(登录恢复 #504)。授权范围见基线段。第二实例写流量仍不放行。
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
@@ -516,3 +516,10 @@
"receipt_sha256": "c7b4f31bb5e8b7fb8f49d22509e371227c77334c6a03f9959deb7a38a0629391"
}
```
## 闭合记录(card-autoclose 回填)
- 依据:merge d40d1c2e5d29bb0ce429edf2221d9c4e4bcb0dcb(PR #501)已包含于 origin/main,分支成果已落盘;本段为合入后回填。
- 合并主题:Record the U7 release-gate specs
- 归档:随闭合移入 .task/archive/;任务执行期的过程证据见原任务记录与 PR。
- 范围:只闭合本卡。合入提交的主题未带默认的 "from <branch> into main" 尾注,task.sh close 的主题匹配因此未选中本卡,故按 card_backfill 的同一回填手工归档。
File diff suppressed because one or more lines are too long
+16 -6
View File
@@ -1435,8 +1435,9 @@ svc_launch_serve() {
}
# svc_lifecycle_child ACTION [extra args...]:用清空的环境拉起本脚本的
# __lifecycle。白名单:PATH/HOME/USER/LOGNAME/LANG/LC_*/TZ/TMPDIR、代理、SVC_*。
# 从不传入 AGENT_SYSTEM_*。
# __lifecycle。白名单:PATH/HOME/USER/LOGNAME/LANG/LC_*/TZ/TMPDIR、代理、SVC_*、
# GIT_CONFIG_*(运行器把 SSH pin 改写成受信 HTTPS,token 只在环境里)。
# 从不传入 AGENT_SYSTEM_*。serve 仍是 env -i,不继承这些变量。
svc_lifecycle_child() {
local -a args=(env -i "PATH=${PATH:-/usr/bin:/bin}" "HOME=${HOME:-/tmp}"
"USER=${USER:-}" "LOGNAME=${LOGNAME:-${USER:-}}" "LANG=${LANG:-C.UTF-8}"
@@ -1450,7 +1451,7 @@ svc_lifecycle_child() {
while IFS= read -r k; do
[ -n "$k" ] || continue
args+=("$k=${!k}")
done < <(compgen -e | grep -E '^(SVC_|LC_)' || true)
done < <(compgen -e | grep -E '^(SVC_|LC_|GIT_CONFIG_)' || true)
if [ -n "${RDM_DISABLE_EXEC_TOOLS:-}" ]; then
args+=("RDM_DISABLE_EXEC_TOOLS=$RDM_DISABLE_EXEC_TOOLS")
fi
@@ -3358,7 +3359,6 @@ static_do_deploy() {
wt="$(materialize_dev_commit "$SRC_FULL" "$SRC_SHA" frontend)" \
|| { hist_event deploy fail "$(read_pid)" "物化/构建失败($SRC_LABEL)"; return 1; }
src_dist="$wt/modules/frontend/dist"
recycle_worktree "$SRC_SHA"
;;
release)
c_err "[$ENV_NAME] release 制品内嵌前端(无独立 dist);static 请用 --from dev|local 或 --dir"
@@ -3372,10 +3372,20 @@ static_do_deploy() {
return 1
;;
esac
[ -d "$src_dist" ] || { c_err "[$ENV_NAME] 前端产物缺失:$src_dist"; hist_event deploy fail "$(read_pid)" "dist 缺失"; return 1; }
if [ ! -d "$src_dist" ]; then
[ "$SRC_KINDSRC" = dev ] && recycle_worktree "$SRC_SHA"
c_err "[$ENV_NAME] 前端产物缺失:$src_dist"
hist_event deploy fail "$(read_pid)" "dist 缺失"
return 1
fi
mkdir -p "$ENV_DIR"
rm -rf "${ENV_DIR:?}/htdocs"
cp -r "$src_dist" "$ENV_DIR/htdocs" || { hist_event deploy fail "$(read_pid)" "dist 复制失败"; return 1; }
cp -r "$src_dist" "$ENV_DIR/htdocs" || {
[ "$SRC_KINDSRC" = dev ] && recycle_worktree "$SRC_SHA"
hist_event deploy fail "$(read_pid)" "dist 复制失败"
return 1
}
[ "$SRC_KINDSRC" = dev ] && recycle_worktree "$SRC_SHA"
write_static_reg "$ENV_DIR/htdocs"
c_ok "[$ENV_NAME] 前端产物就位:$ENV_DIR/htdocs(源 $SRC_LABEL)"
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
+66 -9
View File
@@ -1,18 +1,77 @@
"""真实 Dev/RDM 组合门禁。由 make test-lifecycle 调用。
缺 PostgreSQL、RDM 源码或制品时直接失败,不静默跳过。
登录依赖账户 CLI 公共化(账户方案 U7/U5),单独标 NOT_RUN,
登录与 rdm.ping 依赖账户 CLI 公共化,单独标 NOT_RUN,
不因此把已经跑过的真实组合场景记成失败。
"""
import hashlib
import os
import subprocess
import sys
from pathlib import Path
from urllib.parse import urlparse
ROOT = Path(__file__).resolve().parents[3]
PIN = Path(__file__).resolve().parent / "rdm.pin"
def sha256_file(path):
digest = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def print_inventory(src, dev, rdm):
print("组合清单")
head = subprocess.run(["git", "-C", str(ROOT), "rev-parse", "HEAD"],
capture_output=True, text=True)
print("dev_sha", head.stdout.strip() if head.returncode == 0 else "NOT_RUN")
modules = subprocess.run(
["git", "-C", str(ROOT), "submodule", "status", "--recursive"],
capture_output=True, text=True)
if modules.returncode == 0:
for line in modules.stdout.splitlines():
print("submodule", " ".join(line.split()[:2]))
else:
print("submodule NOT_RUN")
print("pin")
for line in PIN.read_text().splitlines():
if line.strip() and not line.startswith("#"):
print(" ", line.strip())
print("dev_bin", dev, sha256_file(dev))
print("rdm_bin", rdm, sha256_file(rdm))
digests = Path(src) / "pin-build-digests.txt"
if digests.is_file():
for line in digests.read_text().splitlines():
print("pin_build", line)
else:
print("pin_build NOT_RUN:本进程没有预构建三个 pin")
print("pg_version", pg_version())
print("登录 NOT_RUN:RDM 二进制没有 user/apikey,工作台登录依赖账户方案 U7/U5")
print("rdm.ping NOT_RUN:需要已登录会话,本轮不公共化账户 CLI")
def pg_version():
parsed = urlparse(os.environ.get("AGENT_PG_TEST_DSN", ""))
if not parsed.hostname:
return "NOT_RUN"
env = os.environ.copy()
env.update({
"PGHOST": parsed.hostname,
"PGPORT": str(parsed.port or 5432),
"PGUSER": parsed.username or "",
"PGPASSWORD": parsed.password or "",
"PGDATABASE": parsed.path.lstrip("/"),
})
proc = subprocess.run(["psql", "-tAc", "SHOW server_version"],
env=env, capture_output=True, text=True)
if proc.returncode != 0:
return "NOT_RUN"
return proc.stdout.strip() or "NOT_RUN"
def main():
missing = []
if not os.environ.get("AGENT_PG_TEST_DSN"):
@@ -30,29 +89,27 @@ def main():
print("test-lifecycle 失败。缺:%s" % ", ".join(missing))
return 1
print("组合清单")
print("pin", PIN.read_text().strip())
print("rdm_src", src)
print("dev_bin", dev)
print("rdm_bin", rdm)
print("将运行 tests.test_svc_realcombo:SQLite 身份、RDM 二进制生命周期、PostgreSQL 身份环境交付")
print("将运行 tests.integration.lifecycle.test_lifecycle_real:pin 上的真实候选与切换")
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"],
[sys.executable, "-m", "unittest",
"tests.test_svc_realcombo",
"tests.integration.lifecycle.test_lifecycle_real", "-q"],
cwd=str(ROOT), env=env)
print_inventory(src, dev, rdm)
if proc.returncode != 0:
print("test-lifecycle 失败:真实组合用例未通过")
return proc.returncode
print("test-lifecycle 可跑场景通过;登录与未设 origin 的全轨部署保持 NOT_RUN")
print("test-lifecycle 可跑场景通过;登录与 rdm.ping 保持 NOT_RUN")
return 0
+4 -2
View File
@@ -1,4 +1,6 @@
# RDM 制品钉扎。三行都是 40 位 hex。升级走 PR,不在 CI 里执行候选文件。
sha=68de2aa41022c8a77206ea30c2164bec33632ee3
prev_sha=00ae1494f5b45ae0b4ed955ee82debdfe830a1c3
# legacy 没有 version --json。prev 是身份能力合入(#291)。sha 与 prev 钉同一
# backend(0f192a4),schema 兼容,但产品树不同,切换能在 release 名上看到。
sha=c6304224f995747ea3ffcb8d0e81f3ad46a96546
prev_sha=3484ec225b0087a9065466ed436ea97a768691ef
legacy_sha=72bb1fec79d64c2af5e49416640712320816c3b4
@@ -0,0 +1,632 @@
"""真实 RDM pin 的部署场景。
由 make test-lifecycle 调用。未设置 SVC_TEST_REAL 时整组跳过,普通 make test
不因此失败。登录与 rdm.ping 不在这里实现:RDM 没有 user/apikey。
"""
import hashlib
import importlib.util
import json
import os
import shutil
import socket
import sqlite3
import subprocess
import time
import unittest
import urllib.error
import urllib.request
from urllib.parse import urlparse, urlunparse
from pathlib import Path
from tests.test_svc_realcombo import ci_git_env
from urllib.parse import urlparse, urlunparse
from tests.test_svc_realcombo import ci_git_env
SOURCE = Path(__file__).resolve().parents[3]
PIN = Path(__file__).resolve().parent / "rdm.pin"
_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)
REAL = os.environ.get("SVC_TEST_REAL") == "1"
SRC = os.environ.get("SVC_RDM_SRC_DIR", "")
REAL_BIN = os.environ.get("SVC_REAL_RDM_BIN", "")
def load_pin():
out = {}
for line in PIN.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
out[key] = value
return out
@unittest.skipUnless(REAL and SRC and REAL_BIN,
"真实 pin 场景 NOT_RUN:需 SVC_TEST_REAL=1、SVC_RDM_SRC_DIR、SVC_REAL_RDM_BIN")
class LifecycleRealTests(tsr.SvcAutoRecoverTests):
"""候选与切换都走 scripts/svc.sh。已装制品用 SVC_REAL_RDM_BIN,候选用 rdm@<pin>。"""
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()
self.assertTrue(self.real_bin.is_file() and os.access(self.real_bin, os.X_OK))
self.pin = load_pin()
self.env["SVC_RDM_SRC_DIR"] = SRC
self.env["RDM_DISABLE_EXEC_TOOLS"] = "1"
self.env.update(ci_git_env())
self.env.pop("AGENT_SYSTEM_DSN", None)
self.env.pop("AGENT_SYSTEM_DATA", None)
def seed_established(self, name, port):
"""用已具备 identity 的制品把实例跑起来,源种类写成 rdm,避免换源门禁抢在身份门禁前。"""
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(self.real_bin, 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", "rdm@seed"), ("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(d / "data")
self.svc(name, "start")
def initialize_store(self, data, env=None):
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(self.real_bin), "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
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)
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):
con = sqlite3.connect(str(self.db_path(name)))
try:
return list(con.execute(
"SELECT instance_id, dbref FROM agent_system_identity"))
finally:
con.close()
def svc(self, *args, check=True, env=None):
proc = subprocess.run(
["bash", str(self.root / "scripts" / "svc.sh"), *map(str, args)],
cwd=self.root, env=env if env is not None else self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=900)
if check:
self.assertEqual(proc.returncode, 0, proc.stdout)
return proc
def db_path(self, name):
return self.runtime(name) / "data" / "agent-system.db"
def db_inode(self, name):
return self.db_path(name).stat().st_ino
def release_id(self, name):
return os.path.basename(os.readlink(self.runtime(name) / "current"))
def alive(self, name):
pid = self.pid_of(name)
if not pid:
return False
try:
os.kill(pid, 0)
except OSError:
return False
return True
def free_port(self):
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
def deploy(self, name, sha, port, check=True):
return self.svc(name, "deploy", "--from", "rdm@" + sha, "--port", str(port),
check=check)
def test_legacy_candidate_refused_before_stop(self):
"""无 identity 的 legacy_sha 在停旧服务前被拒绝,且不改已有库。"""
self.seed_established("leg", self.free_port())
pid = self.pid_of("leg")
inode = self.db_inode("leg")
before = sorted(p.name for p in (self.runtime("leg") / "data").iterdir())
result = self.deploy("leg", self.pin["legacy_sha"], 19641, check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("不支持 identity", result.stdout)
self.assertEqual(self.pid_of("leg"), pid)
self.assertTrue(self.alive("leg"))
self.assertEqual(self.db_inode("leg"), inode)
after = sorted(p.name for p in (self.runtime("leg") / "data").iterdir())
self.assertEqual(after, before)
self.svc("leg", "stop")
def test_missing_row_refused_before_stop(self):
port = self.free_port()
self.seed_established("miss", port)
pid = self.pid_of("miss")
db = self.db_path("miss")
con = sqlite3.connect(str(db))
try:
con.execute("DELETE FROM agent_system_identity")
con.commit()
finally:
con.close()
result = self.deploy("miss", self.pin["sha"], port, check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("无法验证", result.stdout)
self.assertEqual(self.pid_of("miss"), pid)
con = sqlite3.connect(str(db))
try:
count = con.execute("SELECT count(*) FROM agent_system_identity").fetchone()[0]
finally:
con.close()
self.assertEqual(count, 0)
self.svc("miss", "stop")
def test_mismatch_refused_before_stop(self):
port = self.free_port()
self.seed_established("mis", port)
pid = self.pid_of("mis")
db = self.db_path("mis")
con = sqlite3.connect(str(db))
try:
con.execute("UPDATE agent_system_identity SET instance_id='svc-foreign-mismatch'")
con.commit()
finally:
con.close()
result = self.deploy("mis", self.pin["sha"], port, check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("无法验证", result.stdout)
self.assertEqual(self.pid_of("mis"), pid)
con = sqlite3.connect(str(db))
try:
row = con.execute("SELECT instance_id FROM agent_system_identity").fetchone()
finally:
con.close()
self.assertEqual(row[0], "svc-foreign-mismatch")
self.svc("mis", "stop")
def test_prev_and_current_pin_switch_and_back(self):
"""prev_sha 与 sha 来回部署,身份行保持,release 目录跟着源 SHA 走。"""
port = self.free_port()
result = self.deploy("sw", self.pin["prev_sha"], port, check=False)
self.assertEqual(result.returncode, 0, result.stdout)
first = self.release_id("sw")
self.assertIn(self.pin["prev_sha"][:7], first)
iid = self.manifest("sw")["instance_id"]
row = self.identity_row("sw")
result = self.deploy("sw", self.pin["sha"], port, check=False)
self.assertEqual(result.returncode, 0, result.stdout)
second = self.release_id("sw")
self.assertIn(self.pin["sha"][:7], second)
self.assertNotEqual(second, first)
self.assertEqual(self.identity_row("sw"), row)
result = self.deploy("sw", self.pin["prev_sha"], port, check=False)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertIn(self.pin["prev_sha"][:7], self.release_id("sw"))
self.assertEqual(self.manifest("sw")["instance_id"], iid)
self.assertEqual(self.identity_row("sw"), row)
self.svc("sw", "stop")
web = self.runtime("swweb")
if (web / "svc.pid").exists():
self.svc("swweb", "stop")
def file_sha256(self, path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def journal(self, name):
path = self.runtime(name) / "deploy.journal"
return json.loads(path.read_text()) if path.exists() else None
def set_source(self, name, source):
path = self.runtime(name) / "svc.env"
lines = []
for line in path.read_text().splitlines():
if line.startswith("source="):
lines.append("source=" + source)
else:
lines.append(line)
path.write_text("\n".join(lines) + "\n")
def ensure_git(self):
subprocess.run(["git", "init", "--initial-branch=main"], cwd=self.root,
env=self.env, check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "add", "."], cwd=self.root, env=self.env, check=True,
stdout=subprocess.DEVNULL)
subprocess.run(["git", "commit", "-m", "fixture"], cwd=self.root, env=self.env,
check=True, stdout=subprocess.DEVNULL)
def deploy_fault(self, name, sha, port, phase):
env = dict(self.env)
env["SVC_TEST_FAULTS"] = "1"
env["SVC_FAULT_AT"] = phase
return self.svc(name, "deploy", "--from", "rdm@" + sha, "--port", str(port),
check=False, env=env)
def test_phase_interrupt_reconciles_on_real_artifacts(self):
"""staging 在停旧前退出,旧进程与指针不动;switched 在指针已切后退出,下次 start 回退。"""
port = self.free_port()
result = self.deploy("ph", self.pin["prev_sha"], port, check=False)
self.assertEqual(result.returncode, 0, result.stdout)
pid = self.pid_of("ph")
first = self.release_id("ph")
digest = self.file_sha256(self.runtime("ph") / "current" / "bin" / "agent-system")
row = self.identity_row("ph")
staged = self.deploy_fault("ph", self.pin["sha"], port, "staging")
self.assertEqual(staged.returncode, 86, staged.stdout)
self.assertIn("故障注入于 staging", staged.stdout)
self.assertEqual(self.pid_of("ph"), pid)
self.assertTrue(self.alive("ph"))
self.assertEqual(self.release_id("ph"), first)
self.assertEqual(self.journal("ph")["phase"], "staging")
resumed = self.svc("ph", "start")
self.assertIn("未切换", resumed.stdout)
self.assertIsNone(self.journal("ph"))
self.assertEqual(self.pid_of("ph"), pid)
self.assertEqual(self.release_id("ph"), first)
switched = self.deploy_fault("ph", self.pin["sha"], port, "switched")
self.assertEqual(switched.returncode, 86, switched.stdout)
self.assertIn("故障注入于 switched", switched.stdout)
self.assertIn(self.pin["sha"][:7], self.release_id("ph"))
self.assertFalse(self.alive("ph"))
self.assertEqual(self.journal("ph")["phase"], "switched")
rolled = self.svc("ph", "start")
self.assertIn("已回退上一版本", rolled.stdout)
self.assertEqual(self.release_id("ph"), first)
self.assertEqual(
self.file_sha256(self.runtime("ph") / "current" / "bin" / "agent-system"),
digest)
self.assertEqual(self.identity_row("ph"), row)
self.assertTrue(self.alive("ph"))
self.assertIsNone(self.journal("ph"))
self.svc("ph", "stop")
def test_build_failure_keeps_real_process(self):
self.ensure_git()
port = self.free_port()
self.seed_established("bld", port)
self.set_source("bld", "local@seed")
pid = self.pid_of("bld")
before = self.file_sha256("/proc/%d/exe" % pid)
(self.root / "Makefile").write_text("build:\n\texit 1\n")
result = self.svc("bld", "deploy", "--from", "local", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("构建失败", result.stdout)
self.assertEqual(self.pid_of("bld"), pid)
self.assertTrue(self.alive("bld"))
self.assertEqual(self.file_sha256("/proc/%d/exe" % pid), before)
self.svc("bld", "stop")
def test_start_exit_rolls_back_to_real_binary(self):
"""serve 立即退出的包装会回退。version 与 identity 仍转给真实制品,门禁才看得到能力。"""
self.ensure_git()
port = self.free_port()
self.seed_established("die", port)
self.set_source("die", "local@seed")
real = self.file_sha256(self.real_bin)
shim = self.root / "crash-shim"
shim.write_text(
"#!/bin/bash\n"
"real=%s\n"
"if [ \"${1:-}\" = serve ]; then\n"
" echo 'crash-shim: serve exits immediately' >&2\n"
" exit 1\n"
"fi\n"
"exec \"$real\" \"$@\"\n" % self.real_bin)
shim.chmod(0o755)
(self.root / "Makefile").write_text(
"build:\n\tmkdir -p bin\n\tcp %s bin/agent-system\n" % shim)
result = self.svc("die", "deploy", "--from", "local", check=False)
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("已回退上一版本", result.stdout)
current = self.runtime("die") / "current" / "bin" / "agent-system"
self.assertEqual(self.file_sha256(current), real)
self.assertTrue(self.alive("die"))
self.assertIsNone(self.journal("die"))
self.svc("die", "stop")
def git(self, repo, *args):
subprocess.run(["git", "-C", str(repo), *args], cwd=self.root, env=self.env,
check=True, stdout=subprocess.DEVNULL)
def make_copy_repo(self):
"""两笔提交的部署源。build 复制真实制品,frontend 只写 dist,避免整仓前端构建。"""
src = self.root / "devsrc"
(src / "modules" / "frontend" / "dist").mkdir(parents=True)
(src / "Makefile").write_text(
"build:\n\tmkdir -p bin\n\tcp %s bin/agent-system\n"
"frontend:\n\tmkdir -p modules/frontend/dist\n"
"\tcp web/index.html modules/frontend/dist/index.html\n" % self.real_bin)
(src / "web").mkdir()
(src / "web" / "index.html").write_text("old\n")
self.git(src, "init", "--initial-branch=main")
self.git(src, "add", ".")
self.git(src, "commit", "-m", "old")
old = subprocess.check_output(
["git", "-C", str(src), "rev-parse", "HEAD"], text=True).strip()
(src / "web" / "index.html").write_text("new\n")
self.git(src, "add", ".")
self.git(src, "commit", "-m", "new")
new = subprocess.check_output(
["git", "-C", str(src), "rev-parse", "HEAD"], text=True).strip()
self.git(src, "update-ref", "refs/remotes/origin/main", "HEAD")
return src, old, new
def move_ref(self, repo, sha):
ref = "refs/remotes/origin/main"
prev = subprocess.check_output(
["git", "-C", str(repo), "rev-parse", ref], text=True).strip()
def restore():
subprocess.run(["git", "-C", str(repo), "update-ref", ref, prev],
check=True, stdout=subprocess.DEVNULL)
self.addCleanup(restore)
subprocess.run(["git", "-C", str(repo), "update-ref", ref, sha],
check=True, stdout=subprocess.DEVNULL)
def test_mixed_batch_updates_forward_and_deploy_reverses(self):
"""dev、genui 走复制型源仓;rdm1、rdmcand 走 pin。update 只向前,回退必须显式 deploy。"""
src, old, new = self.make_copy_repo()
self.env["SVC_SRC_DIR"] = str(src)
ports = {name: self.free_port() for name in ("dev", "genui", "rdm1", "rdmcand")}
self.svc("dev", "deploy", "--from", "dev@" + old, "--port", str(ports["dev"]))
self.svc("genui", "deploy", "--kind", "static", "--from", "dev@" + old,
"--port", str(ports["genui"]))
self.deploy("rdm1", self.pin["prev_sha"], ports["rdm1"])
self.deploy("rdmcand", self.pin["prev_sha"], ports["rdmcand"])
# deploy 会 fetch 并把 origin/main 拉回远端。更新前再钉回 pin,且 update --no-fetch。
self.move_ref(SRC, self.pin["sha"])
old_src = {
"dev": self.kv("dev", "source"),
"genui": self.kv("genui", "source"),
"rdm1": self.kv("rdm1", "source"),
"rdmcand": self.kv("rdmcand", "source"),
}
self.assertIn(old[:7], old_src["dev"])
self.assertIn(self.pin["prev_sha"][:7], old_src["rdm1"])
for index, name in enumerate(("dev", "genui", "rdm1", "rdmcand")):
if name in ("rdm1", "rdmcand"):
# 上一服务的 deploy 子进程会 fetch,把 origin/main 拉回远端。
subprocess.run(
["git", "-C", SRC, "update-ref", "refs/remotes/origin/main",
self.pin["sha"]],
check=True, stdout=subprocess.DEVNULL)
result = self.svc("update", name, "--no-fetch")
self.assertIn("已更新", result.stdout)
for later in ("dev", "genui", "rdm1", "rdmcand")[index + 1:]:
self.assertEqual(self.kv(later, "source"), old_src[later])
self.assertIn(new[:7], self.kv("dev", "source"))
self.assertIn(new[:7], self.kv("genui", "source"))
self.assertIn(self.pin["sha"][:7], self.kv("rdm1", "source"))
self.assertIn(self.pin["sha"][:7], self.kv("rdmcand", "source"))
self.assertEqual((self.runtime("genui") / "htdocs" / "index.html").read_text(), "new\n")
self.assertEqual(len(self.identity_row("rdm1")), 1)
self.assertEqual(len(self.identity_row("rdmcand")), 1)
self.move_ref(src, old)
self.move_ref(SRC, self.pin["prev_sha"])
ahead = self.svc("update", "--check", "--no-fetch", "dev", "genui", "rdm1", "rdmcand")
self.assertIn("领先", ahead.stdout)
self.assertNotIn("已更新", ahead.stdout)
self.assertIn(new[:7], self.kv("dev", "source"))
self.assertIn(self.pin["sha"][:7], self.kv("rdmcand", "source"))
newest = {
"dev": self.kv("dev", "source"),
"genui": self.kv("genui", "source"),
"rdm1": self.kv("rdm1", "source"),
"rdmcand": self.kv("rdmcand", "source"),
}
reverse = (
("rdmcand", ("rdmcand", "deploy", "--from", "rdm@" + self.pin["prev_sha"],
"--port", str(ports["rdmcand"]))),
("rdm1", ("rdm1", "deploy", "--from", "rdm@" + self.pin["prev_sha"],
"--port", str(ports["rdm1"]))),
("genui", ("genui", "deploy", "--kind", "static", "--from", "dev@" + old,
"--port", str(ports["genui"]))),
("dev", ("dev", "deploy", "--from", "dev@" + old, "--port", str(ports["dev"]))),
)
for index, (name, args) in enumerate(reverse):
self.svc(*args)
self.assertNotEqual(self.kv(name, "source"), newest[name])
for later, _args in reverse[index + 1:]:
self.assertEqual(self.kv(later, "source"), newest[later])
self.assertEqual((self.runtime("genui") / "htdocs" / "index.html").read_text(), "old\n")
for name in ("dev", "genui", "rdm1", "rdmcand", "rdm1web", "rdmcandweb"):
if (self.runtime(name) / "svc.pid").exists():
self.svc(name, "stop")
def marker_id(self, name):
path = self.runtime(name) / "data" / ".agentx" / "data-marker.json"
return json.loads(path.read_text(encoding="utf-8"))["instance_id"]
def test_missing_manifest_refuses_the_same_way(self):
"""manifest 丢失后 deploy、update、recover 都停在归属冲突,不新建身份。"""
src, _old, _new = self.make_copy_repo()
self.env["SVC_SRC_DIR"] = str(src)
self.move_ref(SRC, self.pin["sha"])
names = ("nmdep", "nmupd", "nmrec")
rows = {}
for name in names:
self.seed_established(name, self.free_port())
self.svc(name, "stop")
rows[name] = (self.identity_row(name), self.marker_id(name))
(self.runtime(name) / "manifest.json").unlink()
self.set_source("nmupd", "rdm@" + self.pin["prev_sha"])
envf = self.runtime("nmrec") / "svc.env"
envf.write_text(envf.read_text() + "autostart=1\n")
deployed = self.svc("nmdep", "deploy", "--from", "rdm@" + self.pin["sha"],
"--port", str(self.free_port()), check=False)
updated = self.svc("update", "nmupd", "--no-fetch", check=False)
recovered = self.svc("recover", "nmrec", check=False)
for result in (deployed, updated, recovered):
self.assertNotEqual(result.returncode, 0, result.stdout)
self.assertIn("身份治理冲突", result.stdout)
for name in names:
self.assertIsNone(self.manifest(name))
self.assertEqual((self.identity_row(name), self.marker_id(name)), rows[name])
self.assertFalse(self.alive(name))
def test_pg_cross_instance_verify_refuses(self):
dsn = os.environ.get("AGENT_PG_TEST_DSN", "")
if not dsn:
self.skipTest("PG 跨实例 NOT_RUN:未设置 AGENT_PG_TEST_DSN")
dsn = self.private_pg_dsn(dsn)
port = self.free_port()
self.seed_established("pga", port)
self.svc("pga", "stop")
owner = self.manifest("pga")["instance_id"]
data = self.runtime("pga") / "data"
env = dict(self.env)
env["AGENT_SYSTEM_DSN"] = dsn
# 身份命令不建库。先用真实制品按环境 DSN 启动一次,让受控迁移落表。
self.initialize_store(data, env)
ensured = subprocess.run(
[str(self.real_bin), "identity", "ensure", "--instance-id", owner,
"--dbref", "pg", "--data", str(data)],
env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=60)
self.assertEqual(ensured.returncode, 0, ensured.stdout.replace(dsn, "<dsn>"))
self.seed_established("pgb", self.free_port())
self.svc("pgb", "stop")
other = self.manifest("pgb")["instance_id"]
self.assertNotEqual(other, owner)
envf = self.runtime("pgb") / "svc.env"
envf.write_text(envf.read_text() + "db=pg\ndsn=%s\n" % dsn)
manifest = self.manifest("pgb")
manifest["identity"]["db_mode"] = "pg"
manifest["identity"]["db_ref"] = "pg"
manifest["identity"]["db_marker"] = "established"
(self.runtime("pgb") / "manifest.json").write_text(
json.dumps(manifest, ensure_ascii=False, indent=2) + "\n")
result = self.svc("pgb", "start", check=False)
redacted = result.stdout.replace(dsn, "<dsn>")
self.assertNotEqual(result.returncode, 0, redacted)
self.assertFalse(self.alive("pgb"))
still = subprocess.run(
[str(self.real_bin), "identity", "verify", "--instance-id", owner,
"--dbref", "pg", "--data", str(data)],
env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
timeout=60)
self.assertEqual(still.returncode, 0, still.stdout.replace(dsn, "<dsn>"))
self.svc("pga", "stop", check=False)
def test_caller_dsn_stays_out_of_process_and_logs(self):
sentinel = "credscan-sentinel-7c1e"
self.seed_established("cred", self.free_port())
self.svc("cred", "stop")
env = dict(self.env)
env["AGENT_SYSTEM_DSN"] = "postgres://scanner:%s@127.0.0.1/none" % sentinel
self.svc("cred", "start", env=env)
pid = self.pid_of("cred")
cmdline = Path("/proc/%d/cmdline" % pid).read_bytes()
environ = Path("/proc/%d/environ" % pid).read_bytes()
self.assertNotIn(sentinel.encode(), cmdline)
self.assertNotIn(sentinel.encode(), environ)
for path in ("svc.log", "history.jsonl", "svc.env"):
text = (self.runtime("cred") / path).read_text(errors="replace")
self.assertNotIn(sentinel, text, path)
self.svc("cred", "stop")
def private_pg_dsn(self, dsn):
"""共享的 AGENT_PG_TEST_DSN 已被其他用例写入身份。本用例单独建库。"""
parsed = urlparse(dsn)
name = "d3pg_" + hashlib.sha256(os.urandom(8)).hexdigest()[:12]
created = subprocess.run(
["psql", dsn, "-v", "ON_ERROR_STOP=1", "-c",
'CREATE DATABASE "%s"' % name],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=30)
self.assertEqual(created.returncode, 0, created.stdout.replace(dsn, "<dsn>"))
self.addCleanup(self.drop_private_pg, dsn, name)
return urlunparse(parsed._replace(path="/" + name))
def drop_private_pg(self, dsn, name):
subprocess.run(
["psql", dsn, "-c", 'DROP DATABASE IF EXISTS "%s" WITH (FORCE)' % name],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30)
def event_count(self, name):
db = self.runtime(name) / "data" / "rdm.db"
con = sqlite3.connect("file:%s?mode=ro" % db, uri=True)
try:
return con.execute("SELECT count(*) FROM events").fetchone()[0]
finally:
con.close()
def test_bootstrap_domain_survives_upgrade(self):
port = self.free_port()
self.seed_established("boot", port)
self.svc("boot", "stop")
docs = self.root / "bootstrap-docs"
docs.mkdir()
(docs / "note.md").write_text("# 领域笔记\n\n升级后应仍在。\n")
data = self.runtime("boot") / "data"
ingested = subprocess.run(
[str(self.real_bin), "-bootstrap", str(docs), "-data", str(data)],
env=self.env, text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=60)
self.assertEqual(ingested.returncode, 0, ingested.stdout)
before = self.event_count("boot")
self.assertGreater(before, 0)
result = self.deploy("boot", self.pin["sha"], port, check=False)
self.assertEqual(result.returncode, 0, result.stdout)
self.assertEqual(self.event_count("boot"), before)
self.svc("boot", "stop")
if (self.runtime("bootweb") / "svc.pid").exists():
self.svc("bootweb", "stop")
if __name__ == "__main__":
unittest.main()
+56
View File
@@ -25,6 +25,14 @@ def checkout_script():
return textwrap.dedent("\n".join(lines[start:end]))
def rdm_clone_script():
lines = WORKFLOW.read_text().splitlines()
start = next(i for i, line in enumerate(lines) if "Clone Agent-System-RDM" in line)
start = next(i for i in range(start, len(lines)) if lines[i].strip() == "run: |") + 1
end = next(i for i in range(start, len(lines)) if lines[i].startswith(" - name:"))
return textwrap.dedent("\n".join(lines[start:end]))
class CheckoutTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
@@ -236,6 +244,54 @@ class CheckoutTests(unittest.TestCase):
self.assertNotEqual(result.returncode, 0)
self.assertFalse(any("untrusted" in path for path, auth in self.requests))
def test_rdm_pin_fetch_is_three_shas_on_the_allowlist(self):
source = self.base / "Agent-System-RDM"
source.mkdir()
self.git(source, "init", "-q", "-b", "main")
shas = []
for index in range(3):
(source / "Makefile").write_text(
"build:\n\tmkdir -p bin\n\tprintf 'rev%d\\n' > bin/agent-system-rdm\n" % index)
self.git(source, "add", ".")
self.git(source, "-c", "user.name=Fixture",
"-c", "user.email=fixture@example.invalid",
"commit", "-q", "-m", "rev %d" % index)
shas.append(self.git(source, "rev-parse", "HEAD"))
target = self.published / "weichen" / "Agent-System-RDM.git"
target.parent.mkdir(parents=True, exist_ok=True)
self.git(self.base, "clone", "-q", "--bare", str(source), str(target))
self.git(target, "config", "uploadpack.allowReachableSHA1InWant", "true")
self.git(target, "config", "uploadpack.allowAnySHA1InWant", "true")
work = self.base / "rdm-job"
pin = work / "tests" / "integration" / "lifecycle"
pin.mkdir(parents=True)
(pin / "rdm.pin").write_text(
"sha=%s\nprev_sha=%s\nlegacy_sha=%s\n" % (shas[2], shas[1], shas[0]))
script = rdm_clone_script()
self.assertNotRegex(script, r"(?m)^\s*source\s")
self.requests.clear()
result = subprocess.run(
["bash", "-c", script], cwd=work,
env=self.env | {"SUBMODULE_TOKEN": SUB_TOKEN, "GITHUB_SERVER_URL": self.url},
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertNotIn(SUB_TOKEN, result.stdout + result.stderr)
self.assertTrue(self.requests)
for path, auth in self.requests:
self.assertRegex(path, r"^/weichen/Agent-System-RDM\.git/")
self.assertEqual(auth, "token " + SUB_TOKEN)
for sha in shas:
self.assertEqual(self.git(work / "rdm", "cat-file", "-t", sha), "commit")
self.assertEqual(self.git(work / "rdm", "rev-parse", "HEAD"), shas[2])
digests = (work / "rdm" / "pin-build-digests.txt").read_text()
for sha in shas:
self.assertIn(sha, digests)
gate = WORKFLOW.read_text().split(
"Real-combination lifecycle gate", 1)[1].split("\n svc-realcombo:", 1)[0]
self.assertIn('export SVC_CI_GIT_TOKEN="$SUBMODULE_TOKEN"', gate)
self.assertNotRegex(gate, r"(?m)^\s*source\s")
self.assertNotIn(">>", gate)
if __name__ == "__main__":
unittest.main()