Require RDM companion UI and source-switch gate in svc.sh #450
File diff suppressed because one or more lines are too long
+11
-6
@@ -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
@@ -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
@@ -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 全新部署:首启健康循环不得误判「进程即退」 ----
|
||||
|
||||
|
||||
Reference in New Issue
Block a user