Require RDM companion UI and source-switch gate in svc.sh #450

Merged
weichen merged 8 commits from feature/svc-rdm-ui-gate into main 2026-10-12 11:37:49 +00:00
4 changed files with 547 additions and 24 deletions
File diff suppressed because one or more lines are too long
+11 -6
View File
@@ -336,9 +336,12 @@ Releases 仓跑 `scripts/build-release.sh <v>`。
无声明式插件资产;管理页「插件管理」列表只反映目录插件,组合插件经
`/api/registry` 与命令面可见;
- **web/ 工作台**:RDM 自带零构建静态工作台(直连后端 API),deploy 时复制到
`runtime/<name>/htdocs/`,用伴生 static 服务伺服
(`scripts/svc.sh <name>web deploy --kind static --dir runtime/<name>/htdocs`;
注意该目录随本环境 `rm --purge` 一并删除);
`runtime/<name>/htdocs/`,并**自动**部署伴生 static 服务
(`scripts/svc.sh <name>web deploy --kind static --dir runtime/<name>/htdocs`);
伴生失败或 `GET /` 非 200 时 deploy 记 `ui-missing` 并以非 ok 结束。
注意 `htdocs/` 随本环境 `rm --purge` 一并删除;
- **换源门禁**:同名服务切换源种类(如 `dev@*`→`rdm@*`)须加
`--allow-source-switch`;同种类换 sha(含 `update`)不需要;
- **启动兼容**:组合二进制容忍基座风格的首位 `serve` 子命令并提供 `version`
子命令;svc 的 `version`/`serve -h` 探测带 timeout 护栏;
- **不支持** `--mock` 与 `--config`(组合 CLI 无此旗标);模型设置播种
@@ -359,11 +362,13 @@ Releases 仓跑 `scripts/build-release.sh <v>`。
3. **候选(禁止双写)**:先 `scripts/svc.sh <name> stop`,再
`scripts/svc.sh <cand> deploy --from rdm@<sha> --data runtime/<name>/data --port <空闲> --auth on`;
验登录 / `/api/projects` / `rdmAuditReady` 后停候选。
4. **Cutover**:`scripts/svc.sh <name> deploy --from rdm@<sha>`(同名 redeploy 保留
data/auth)。失败回滚:`scripts/svc.sh <name> deploy --from dev@<旧源>`。
4. **Cutover**:若当前为非 rdm 源,加 `--allow-source-switch`:
`scripts/svc.sh <name> deploy --from rdm@<sha> --allow-source-switch`
(同名 redeploy 保留 data/auth;伴生 `<name>web` 自动就位)。失败回滚:
`scripts/svc.sh <name> deploy --from dev@<旧源> --allow-source-switch`。
5. **基座 FE 验收**:`PLAYWRIGHT_API_TARGET=http://127.0.0.1:<port> npm run preview`
(或 Vite dev 代理);`svc` static **无** `/api` 反代。RDM `web/` 工作台是另一套 UI
(`htdocs` + 伴生 static),与 React SPA 门控并行可用。
(`htdocs` + 自动伴生 static),与 React SPA 门控并行可用。
### 5.5 辅助
+165 -12
View File
@@ -298,15 +298,19 @@ EOF
pins/ 子模块后 make build(纯 Go,无需 npm),产物
bin/agent-system-rdm 复制为环境二进制;插件目录按空集安装
(组合插件编译进二进制),web/ 工作台静态资产随环境落
htdocs/(伴生 static 服务用 --dir 指向该目录)。
htdocs/,并自动部署伴生 <name>web(--kind static --dir)。
注意:rdm 源不支持 --mock 与 --config;模型设置播种
(--model-*/--tavily-keys)仅基座产物支持。
--pick [dev|release|rdm] 交互式编号选择(需 TTY;列表与提示走 stderr)
--dry-run 只打印部署计划不执行
--allow-source-switch 同名服务换源种类(dev↔rdm↔release↔local)须显式确认;
同种类换 sha(含 update)不需要
行为细节:
- deploy 前自动停止旧实例(只停自己 pidfile+cmdline 校验的进程)
- agent 启动后等健康检查(20s),随后按 --model-*/--tavily-keys 播种设置(幂等)
- 部署收尾按源种类做 UI 探活:dev/local/release 要求本服务 GET /=200;
rdm 要求伴生 <name>web GET /=200(失败记 ui-missing,deploy 非 ok)
- 旧版本产物不认识 --auth/--config 时自动省略(探测 serve 用法)并提示;
不消费环境变量 DSN(AGENT_SYSTEM_DSN)的旧产物在需要交付数据库配置时
拒绝部署/启动(不静默按内嵌 SQLite 运行)
@@ -317,6 +321,7 @@ EOF
示例:
scripts/svc.sh prod deploy --from release@0.1.0 --tag stable
scripts/svc.sh prod deploy --from dev@73b2802 --dry-run
scripts/svc.sh dev deploy --from rdm@310c3fe --allow-source-switch
scripts/svc.sh ui-r4 deploy --kind static --from dev@73b2802
scripts/svc.sh ui-qa deploy --kind static --dir ../frontend/dist
EOF
@@ -629,6 +634,7 @@ CLI_SEED_OWNER=""
CLI_FROM=""; CLI_PICK=""; CLI_DRYRUN=0
CLI_KIND=""; CLI_DIR=""; CLI_PURGE=0; CLI_REVEAL=0
CLI_APPLY=0; CLI_FORCE=0; CLI_ONLINE=0; CLI_FILE=""; CLI_OUT=""
CLI_ALLOW_SOURCE_SWITCH=0
CLI_CHECK=0; CLI_ALL=0; CLI_NOFETCH=0; CLI_QUIET=0
CLI_DB_USER=""; CLI_DB_PASSWORD=""; CLI_DB_NAME=""; CLI_DB_AUTH=""
CLI_TAGS=()
@@ -665,6 +671,7 @@ parse_args() {
--reveal) CLI_REVEAL=1; shift ;;
--apply) CLI_APPLY=1; shift ;;
--force) CLI_FORCE=1; shift ;;
--allow-source-switch) CLI_ALLOW_SOURCE_SWITCH=1; shift ;;
--online) CLI_ONLINE=1; shift ;;
--file) need_value "$@"; CLI_FILE="$2"; shift 2 ;;
--out) need_value "$@"; CLI_OUT="$2"; shift 2 ;;
@@ -1652,8 +1659,124 @@ agent_do_user() {
# ---- agent:部署与启动 ----
# source_kind_prefix LABEL:取 source= 的种类前缀(dev@sha → dev;空 → 空)。
source_kind_prefix() {
case "$1" in
*@*) printf '%s' "${1%%@*}" ;;
*) printf '%s' "$1" ;;
esac
}
# require_allow_source_switch:同名 agent 换源种类(dev↔rdm↔release↔local)须显式确认;
# 同种类换 sha(含 update)不要求。与 service kind 冲突门禁正交。
require_allow_source_switch() {
local prev prev_kind new_kind
prev="$(svc_get source)"
[ -n "$prev" ] || return 0
prev_kind="$(source_kind_prefix "$prev")"
new_kind="$(source_kind_prefix "$SRC_LABEL")"
[ -n "$prev_kind" ] && [ -n "$new_kind" ] || return 0
[ "$prev_kind" = "$new_kind" ] && return 0
if [ "${CLI_ALLOW_SOURCE_SWITCH:-0}" = 1 ]; then
c_warn "[$ENV_NAME] 换源已确认:$prev → $SRC_LABEL(--allow-source-switch)"
return 0
fi
c_err "[$ENV_NAME] 换源种类 $prev_kind → $new_kind($prev → $SRC_LABEL)需显式确认"
c_err " 加 --allow-source-switch 后重试(会改变环境是否自带 UI)"
return 1
}
rdm_companion_name() { printf '%sweb' "$1"; }
# http_code URL:返回 HTTP 状态码(连接失败 → 000)。
http_code() {
curl -s -o /dev/null -m 2 -w '%{http_code}' "$1" 2>/dev/null || printf '000'
}
# rdm_workbench_missing NAME:rdm@* agent 且伴生未登记或未运行 → 0(真)。
rdm_workbench_missing() {
local name="$1" src cname st
src="$(env_kv_of "$name" source)"
case "$src" in rdm@*) ;; *) return 1 ;; esac
cname="$(rdm_companion_name "$name")"
[ -f "$RUNTIME/$cname/svc.env" ] || return 0
st="$(svc_pid_state "$cname")"
[ "${st%% *}" = alive ] && return 1
return 0
}
# deploy_rdm_companion_ui:子进程部署 ${ENV_NAME}web --kind static --dir htdocs。
deploy_rdm_companion_ui() {
local cname="$1" htdocs="$2"
if ! valid_env "$cname"; then
c_err "[$ENV_NAME] 伴生名非法:$cname(服务名 ≤16 字符;ui-missing)"
return 1
fi
if [ ! -d "$htdocs" ] || [ ! -f "$htdocs/index.html" ]; then
c_err "[$ENV_NAME] RDM 工作台缺失:$htdocs(ui-missing)"
return 1
fi
c_info "[$ENV_NAME] 自动部署伴生工作台:$cname --kind static --dir $htdocs"
if ! "$0" "$cname" deploy --kind static --dir "$htdocs"; then
c_err "[$ENV_NAME] 伴生工作台部署失败(ui-missing)"
return 1
fi
return 0
}
# assert_ui_ready:按源种类断言页面入口 HTTP 200(与进程探活分层)。
# rdm@* → 伴生 ${name}web;dev@*/local@*/release@* → agent 自身 GET /。
assert_ui_ready() {
local src="${SRC_LABEL:-$(svc_get source)}"
local code url host port addr cname probe
case "$src" in
rdm@*)
cname="$(rdm_companion_name "$ENV_NAME")"
addr="$(env_kv_of "$cname" addr)"
if [ -z "$addr" ]; then
c_err "[$ENV_NAME] UI 探活失败:伴生 $cname 未登记(ui-missing)"
return 1
fi
host="${addr%:*}"; port="${addr##*:}"
[ "$host" = "0.0.0.0" ] || [ "$host" = "::" ] && host="127.0.0.1"
url="http://$host:$port/"
code="$(http_code "$url")"
if [ "$code" != 200 ]; then
c_err "[$ENV_NAME] UI 探活失败:伴生 $url → HTTP $code(期望 200;ui-missing)"
return 1
fi
c_ok "[$ENV_NAME] UI 就绪:伴生 $cname → $url"
return 0
;;
dev@*|local@*|release@*)
probe="${ENV_ADDR}"
case "$(svc_get exec)" in *--addr\ *)
probe="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--addr \([^ ]*\).*/\1/p')" ;;
esac
[ -n "$probe" ] || probe="$ENV_ADDR"
host="${probe%:*}"; port="${probe##*:}"
[ "$host" = "0.0.0.0" ] || [ "$host" = "::" ] && host="127.0.0.1"
url="http://$host:$port/"
code="$(http_code "$url")"
if [ "$code" != 200 ]; then
c_err "[$ENV_NAME] UI 探活失败:$url → HTTP $code(期望 200;ui-missing)"
return 1
fi
c_ok "[$ENV_NAME] UI 就绪:$url"
return 0
;;
*)
return 0
;;
esac
}
agent_do_deploy() {
resolve_source
if ! require_allow_source_switch; then
hist_event deploy fail "$(read_pid)" "换源种类需 --allow-source-switch($SRC_LABEL)"
return 1
fi
if [ "$CLI_DRYRUN" = 1 ]; then print_agent_plan; return 0; fi
# 停旧之前的静态配置验证:数据库配置可解析(绑定有效/无冲突)、conffile 存在、
# 现产物承接能力——配置性问题不制造停机窗口(登记保持可恢复,失败直接返回)。
@@ -1663,7 +1786,7 @@ agent_do_deploy() {
fi
hist_event deploy begin "$(read_pid)" "from=$SRC_LABEL"
do_stop_quiet
local wt
local wt RDM_UI_OK=0
case "$SRC_KINDSRC" in
local)
c_info "[$ENV_NAME] 构建当前工作区(make build)——唯一写主检出构建产物的路径"
@@ -1688,15 +1811,17 @@ agent_do_deploy() {
mkdir -p "$wt/.svc-empty-plugins"
install_artifacts "$wt/bin/agent-system-rdm" "$wt/.svc-empty-plugins" \
|| { hist_event deploy fail "$(read_pid)" "产物复制失败"; return 1; }
# RDM 自带零构建工作台(web/,直连后端 API 的静态资产)随环境落 htdocs/,
# 供伴生 static 服务直接 --dir 使用(注意:随本环境 rm --purge 一并删除)。
# RDM 自带零构建工作台(web/)落 htdocs/;收尾自动部署伴生 ${name}web。
if [ -d "$wt/web" ]; then
rm -rf "$ENV_DIR/htdocs"
if cp -r "$wt/web" "$ENV_DIR/htdocs"; then
c_ok "[$ENV_NAME] RDM 工作台就位:$ENV_DIR/htdocs(伴生服务:scripts/svc.sh ${ENV_NAME}web deploy --kind static --dir $ENV_DIR/htdocs)"
RDM_UI_OK=1
c_ok "[$ENV_NAME] RDM 工作台就位:$ENV_DIR/htdocs(将自动部署伴生 $(rdm_companion_name "$ENV_NAME"))"
else
c_warn "[$ENV_NAME] RDM web/ 工作台复制失败(不影响 API 服务启动)"
c_warn "[$ENV_NAME] RDM web/ 工作台复制失败"
fi
else
c_warn "[$ENV_NAME] RDM 源无 web/ 工作台目录"
fi
recycle_worktree "rdm-$SRC_SHA" "$RDM_SRC_DIR"
;;
@@ -1709,6 +1834,23 @@ agent_do_deploy() {
return 1
fi
agent_do_start || { hist_event deploy fail "$(read_pid)" "启动失败($SRC_LABEL)"; return 1; }
case "$SRC_KINDSRC" in
rdm)
if [ "$RDM_UI_OK" != 1 ]; then
c_err "[$ENV_NAME] RDM 工作台未就位(ui-missing):缺少可伺服的 htdocs/"
hist_event deploy fail "$(read_pid)" "ui-missing(htdocs 未就位)"
return 1
fi
if ! deploy_rdm_companion_ui "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR/htdocs"; then
hist_event deploy fail "$(read_pid)" "ui-missing(伴生部署失败)"
return 1
fi
;;
esac
if ! assert_ui_ready; then
hist_event deploy fail "$(read_pid)" "ui-missing"
return 1
fi
hist_event deploy ok "$(read_pid)" "from=$SRC_LABEL"
return 0
}
@@ -1733,7 +1875,8 @@ print_agent_plan() {
printf ' 来源:%s(完整提交 %s;独立 RDM 源仓 %s)\n' "$SRC_LABEL" "$SRC_FULL" "$RDM_SRC_DIR"
printf ' 物化:worktree %s(pins submodule update --init --recursive + make build)\n' "$RUNTIME/.worktrees/rdm-$SRC_SHA"
printf ' 产物:%s ← bin/agent-system-rdm + 空插件目录(组合插件编译进二进制)\n' "$ENV_BIN"
printf ' 工作台:web/ → %s/htdocs(伴生 static:--kind static --dir 该目录)\n' "$ENV_DIR" ;;
printf ' 工作台:web/ → %s/htdocs;自动伴生:scripts/svc.sh %s deploy --kind static --dir 该目录\n' \
"$ENV_DIR" "$(rdm_companion_name "$ENV_NAME")" ;;
esac
return 0
}
@@ -2356,10 +2499,11 @@ static_do_start() {
c_info "[$ENV_NAME] 启动静态服务:python3 http.server $port --bind $host --directory $htdocs"
nohup python3 -m http.server "$port" --bind "$host" --directory "$htdocs" >> "$ENV_DIR/svc.log" 2>&1 9>&- &
echo $! > "$ENV_PID"
local probe_host="$host"
local probe_host="$host" code
[ "$probe_host" = "0.0.0.0" ] || [ "$probe_host" = "::" ] && probe_host="127.0.0.1"
for _ in $(seq 1 50); do
if curl -s -o /dev/null -m 2 "http://$probe_host:$port/" 2>/dev/null; then
code="$(http_code "http://$probe_host:$port/")"
if [ "$code" = 200 ]; then
record_state "htdocs=$htdocs" # 健康通过才写 svc.env(与 agent 路径同规)
svc_mark_autostart
c_ok "[$ENV_NAME] 已启动 pid=$(read_pid) → http://$probe_host:$port/(目录 $htdocs)"
@@ -2375,7 +2519,7 @@ static_do_start() {
done
record_state "htdocs=$htdocs"
svc_mark_autostart
c_err "[$ENV_NAME] 10s 健康检查未通过"
c_err "[$ENV_NAME] 10s 健康检查未通过(末次 HTTP ${code:-?})"
hist_event start fail "$(read_pid)" "健康检查超时"
return 1
}
@@ -3598,6 +3742,11 @@ global_list() {
fi
done
[ "${#names[@]}" -eq 0 ] && c_info "(暂无已登记服务;db create / deploy 会登记)"
for name in ${names[@]+"${names[@]}"}; do
if rdm_workbench_missing "$name"; then
c_warn " [$name] 无工作台入口(伴生 $(rdm_companion_name "$name") 未登记或未运行)"
fi
done
return 0
}
@@ -3628,6 +3777,10 @@ env_status_detail() {
printf ' 监听守卫 insecureListen=%s allowedHosts=%s\n' \
"$(env_var "$ENV_NAME" INSECURE_LISTEN)" \
"$(env_var "$ENV_NAME" ALLOWED_HOSTS)"
if rdm_workbench_missing "$ENV_NAME"; then
printf ' 工作台 无入口(伴生 %s 未登记或未运行;scripts/svc.sh %s deploy --kind static --dir %s/htdocs)\n' \
"$(rdm_companion_name "$ENV_NAME")" "$(rdm_companion_name "$ENV_NAME")" "$ENV_DIR"
fi
;;
postgres)
printf ' 库 %s 用户 %s\n' "$(svc_get dbname)" "$(svc_get user)"
@@ -4410,8 +4563,8 @@ main() {
exit 0
fi
parse_args "$@"
if { [ -n "$CLI_FROM" ] || [ -n "$CLI_PICK" ] || [ "$CLI_DRYRUN" = 1 ] || [ -n "$CLI_DIR" ]; } && [ "$action" != deploy ]; then
c_err "--from/--pick/--dry-run/--dir 仅用于 deploy 动作"
if { [ -n "$CLI_FROM" ] || [ -n "$CLI_PICK" ] || [ "$CLI_DRYRUN" = 1 ] || [ -n "$CLI_DIR" ] || [ "$CLI_ALLOW_SOURCE_SWITCH" = 1 ]; } && [ "$action" != deploy ]; then
c_err "--from/--pick/--dry-run/--dir/--allow-source-switch 仅用于 deploy 动作"
exit 2
fi
env_config "$ENV_NAME"
+88 -6
View File
@@ -11,6 +11,7 @@ import shutil
import subprocess
import tempfile
import unittest
import urllib.error
import urllib.request
from pathlib import Path
@@ -61,8 +62,10 @@ case "${1:-}" in
trap 'kill "$child" 2>/dev/null; exit 0' TERM INT
# GET/HEAD 一律 200(健康探测语义与 http.server 等价);PUT /api/* 捕获头与
# 请求体到 trace/put.txt 供播种断言(settings 播种验证,2026-10-11)。
# HTTP 子进程轮询 ppid:父进程被 SIGKILL 后自行退出,避免与 recover 套件
# 共用 1880x 端口时泄漏占用(与 test_svc_recover 桩同规)。
python3 - "$port" "$host" "$trace_dir" <<'PYSRV' >/dev/null 2>&1 &
import sys
import os, sys, threading, time
from http.server import BaseHTTPRequestHandler, HTTPServer
port, host, trace = int(sys.argv[1]), sys.argv[2], sys.argv[3]
class H(BaseHTTPRequestHandler):
@@ -84,7 +87,20 @@ class H(BaseHTTPRequestHandler):
self._ok()
def log_message(self, *args):
pass
HTTPServer((host, port), H).serve_forever()
ppid = os.getppid()
def watch():
while True:
try:
if os.getppid() != ppid:
os._exit(0)
except Exception:
os._exit(0)
time.sleep(0.3)
threading.Thread(target=watch, daemon=True).start()
try:
HTTPServer((host, port), H).serve_forever()
except Exception:
os._exit(1)
PYSRV
child=$!
wait "$child"
@@ -722,10 +738,14 @@ class SvcDbRecoveryTests(unittest.TestCase):
# ---- 13. rdm 部署源(下游组合仓 Agent-System-RDM):物化/产物/工作台/播种跳过 ----
def make_rdm_repo(self):
"""构造下游组合仓夹具:Makefile build 产出桩 agent-system-rdm + web/ 工作台。"""
def make_rdm_repo(self, with_web=True):
"""构造下游组合仓夹具:Makefile build 产出桩 agent-system-rdm + 可选 web/。"""
remote = self.root / "rdmremote.git"
work = self.root / "rdmwork"
if remote.exists():
shutil.rmtree(remote)
if work.exists():
shutil.rmtree(work)
run = lambda cwd, *args: subprocess.run(
["git", *args], cwd=cwd, env=self.env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
@@ -734,8 +754,9 @@ class SvcDbRecoveryTests(unittest.TestCase):
run(self.root, "init", "--initial-branch=main", str(work))
(work / "Makefile").write_text(
"build:\n\tmkdir -p bin\n\tcp %s bin/agent-system-rdm\n" % self.stub)
(work / "web").mkdir()
(work / "web" / "index.html").write_text("<html>rdm workbench</html>\n")
if with_web:
(work / "web").mkdir()
(work / "web" / "index.html").write_text("<html>rdm workbench</html>\n")
run(work, "add", ".")
run(work, "commit", "-m", "rdm v1")
run(work, "remote", "add", "origin", str(remote))
@@ -753,6 +774,23 @@ class SvcDbRecoveryTests(unittest.TestCase):
})
return env
def http_get(self, url, timeout=2):
try:
with urllib.request.urlopen(url, timeout=timeout) as resp:
return resp.status, resp.read().decode("utf-8", "replace")
except urllib.error.HTTPError as exc:
return exc.code, ""
except OSError:
return 0, ""
def companion_url(self, agent_name):
addr = self.kv(agent_name + "web", "addr")
self.assertIsNotNone(addr, "companion %sweb not registered" % agent_name)
host, port = addr.rsplit(":", 1)
if host in ("0.0.0.0", "::"):
host = "127.0.0.1"
return "http://%s:%s/" % (host, port)
def test_rdm_source_deploy_end_to_end(self):
remote, work = self.make_rdm_repo()
env = self.rdm_env(remote)
@@ -766,10 +804,50 @@ class SvcDbRecoveryTests(unittest.TestCase):
self.assertEqual(list((d / "plugins").iterdir()), [])
self.assertIn("rdm workbench", (d / "htdocs" / "index.html").read_text())
self.assertIn("args:", self.serve_record()) # 健康通过即登记启动参数
# 伴生 static 自动部署并探活 200。
self.assertEqual(self.kv("rdm1web", "kind"), "static")
self.assertTrue(self.kv("rdm1web", "source").startswith("dir@"))
code, body = self.http_get(self.companion_url("rdm1"))
self.assertEqual(code, 200)
self.assertIn("rdm workbench", body)
self.assertIn("UI 就绪", result.stdout)
# rdm 形态跳过设置播种(无 user/apikey 子命令):给了模型键也不产生 PUT/CLI。
self.assertFalse((self.trace / "put.txt").exists())
self.assertNotIn("user list", self.cli_record())
def test_rdm_companion_missing_web_fails_deploy(self):
remote, work = self.make_rdm_repo(with_web=False)
env = self.rdm_env(remote)
result = self.svc("rdm2", "deploy", "--from", "rdm", "--port", "18843",
env=env, check=False)
self.assertNotEqual(result.returncode, 0)
self.assertIn("ui-missing", result.stdout)
hist = (self.runtime("rdm2") / "history.jsonl").read_text()
self.assertIn("ui-missing", hist)
self.assertIsNone(self.kv("rdm2web", "kind"))
def test_source_switch_requires_flag(self):
remote, work = self.make_rdm_repo()
env = self.rdm_env(remote)
# 先以 local 源部署(内嵌 UI 桩一律 200)。
self.svc("sw1", "deploy", "--from", "local", "--port", "18844")
old = self.kv("sw1", "source")
self.assertTrue(old.startswith("local@"))
# 无旗标换到 rdm → 拒绝,source 不变。
denied = self.svc("sw1", "deploy", "--from", "rdm", "--port", "18844",
env=env, check=False)
self.assertNotEqual(denied.returncode, 0)
self.assertIn("allow-source-switch", denied.stdout)
self.assertEqual(self.kv("sw1", "source"), old)
# 带旗标允许换源,伴生就位。
ok = self.svc("sw1", "deploy", "--from", "rdm", "--port", "18844",
"--allow-source-switch", env=env)
self.assertTrue(self.kv("sw1", "source").startswith("rdm@"))
self.assertIn("UI 就绪", ok.stdout)
code, body = self.http_get(self.companion_url("sw1"))
self.assertEqual(code, 200)
self.assertIn("rdm workbench", body)
def test_rdm_source_commits_and_update_track(self):
remote, work = self.make_rdm_repo()
env = self.rdm_env(remote)
@@ -791,6 +869,10 @@ class SvcDbRecoveryTests(unittest.TestCase):
self.assertTrue(new.startswith("rdm@"))
self.assertNotEqual(new, old)
self.assertIn("args:", self.serve_record()) # 更新后健康在跑
# 同轨 update 不需 --allow-source-switch;伴生仍 200。
code, body = self.http_get(self.companion_url("rdm1"))
self.assertEqual(code, 200)
self.assertIn("rdm workbench", body)
# ---- static 全新部署:首启健康循环不得误判「进程即退」 ----