diff --git a/docs/EXECUTION_BOARD.md b/docs/EXECUTION_BOARD.md index e129c144..037520cc 100644 --- a/docs/EXECUTION_BOARD.md +++ b/docs/EXECUTION_BOARD.md @@ -18,6 +18,19 @@ - latest-head relay-manager 已新增宿主 capability 自愈: - 当第三方 OpenAI-compatible upstream 因宿主把 `openai_responses_supported` 误判成 `true` 而导致 host `/v1/chat/completions` 返回 `502 upstream_error` 时,access closure 与后台 reconcile 会自动把相关 account 修正到 raw `/chat/completions` 路径后再重试 - 该修正现在不再依赖宿主长期保留补丁,宿主升级后只要下次 import/access/reconcile 触发,就能重新收敛到正确 capability +- 2026-05-23 remote43 线上验收脚本已继续收口: + - `scripts/import_remote43_provider.sh` 现已明确拆分 `CRM_HOST_BASE` 与 `REMOTE_HOST_BASE` + - 远端 Postgres / Redis 容器已改成按目标宿主端口自动解析,不再硬编码落到 `sub2api-relaymgr-pg/redis` + - 远端 managed probe `/v1/models` 与 `/v1/chat/completions` 已改成只走 `REMOTE_HOST_BASE` + - provider status / access status / access preview 末尾查询已补 `host_id`,避免本地 CRM 有多宿主历史时被 `provider exists on multiple hosts` 截断 +- `artifacts/real-host-acceptance/20260523_144937_remote43_kimi-a7m_key_import` 已证明: + - 这轮线上 `kimi-a7m` 不再复现“错库取 key 导致统一 401”或“模型列表串成 GPT 默认集合” + - import 已返回 `gateway.status_code=200`、`models=["kimi-k2.6"]`、`has_expected_model=true` + - upstream `/models` 与 `/chat/completions` 都是 `200` + - 未改宿主的真实阻塞已收缩为 host `/v1/chat/completions` 仍返回 `503/502`,不再是插件脚本的数据面问题 +- `artifacts/real-host-acceptance/20260523_145531_remote43_kimi-a7m_key_import` 说明另一类运行时噪音: + - 当本地 SSH 隧道端口存活但链路已失活时,`POST /api/hosts` 阶段会在 `get host version` 处超时 + - 这类现象应优先解释为 tunnel/runtime 故障,而不是 provider 导入逻辑回退 - 官方 provider 验证矩阵当前仍保留一条非阻塞事实: - `artifacts/real-host-acceptance/20260521_222212_remote43_minimax-m2-7-official_key_import/21-summary.json` 已证明 official MiniMax 模板链路是通的,但该验证 key 当前命中 upstream `429` - `reconcile=drifted` 仍可能在 shared fresh-host 上出现,但当前解释是“历史残留资源噪音”,不阻塞 PRD 首版放行 @@ -122,6 +135,11 @@ - `21-summary.json` 已到 `batch_status=succeeded`、`provider_status=active` - `account_probe_summary` 明确记录 `probe_advisory=true`、`validation_status=warning`,证明 403 probe race 已被 relay-manager 正确降级 +8. `artifacts/real-host-acceptance/20260523_144937_remote43_kimi-a7m_key_import` + - remote43 未改宿主 + 修正后的 latest-head 验收脚本样本 + - 已证明脚本层的“错库取 key / 错地址 / 多 host 历史查询”问题被收掉 + - 仍保留的真实阻塞是宿主 completion 路径 `502/503` + ## 剩余项(P2 / 运营前置,不阻塞按 PRD 首版范围上线) 1. 运营前置 diff --git a/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md b/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md index 1221492d..c208d93b 100644 --- a/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md +++ b/docs/REAL_HOST_ACCEPTANCE_LEARNINGS.md @@ -623,6 +623,98 @@ - 如果 account `/models` 已对、channel 落库也对,但普通用户流量不对,优先怀疑运营前置或 harness 参数 - 不要立即重开“导入代码失效”的结论 +### 13. remote43 如果同时有“本地 CRM + 远端宿主 + 远端 DB/Redis”,最容易错哪三件事? + +2026-05-23 这一轮 remote43 `kimi-a7m` 复验把最容易反复出错的 3 个点彻底暴露出来了。 + +#### 1) 把 `CRM_HOST_BASE` 和 `REMOTE_HOST_BASE` 混成一个地址 + +- 本地运行的 CRM 访问宿主时,应该走本地 SSH 隧道,例如 `http://127.0.0.1:18089` +- 远端 SSH 内部执行 `curl` 或 `docker exec` 时,才应该走远端机器自己能看到的地址,例如 `http://127.0.0.1:18097` +- 如果把两者都写成 `18097`,本地 CRM 会尝试访问自己机器上的 `127.0.0.1:18097`,结果在 `POST /api/hosts` 阶段直接掉进 `500 internal_error` + +这类错误的现象通常是: +- `01a-create-host.json` 为空 +- `03-import.body.json` 直接是 `batch_id=0` +- message 落在 `get host version` 或 `probe host capabilities` + +经验结论: +- **本地 CRM 到宿主的地址** 和 **远端 SSH 侧到宿主的地址** 必须分开记录 +- 以后若脚本同时涉及 `curl CRM API` 和 `ssh remote curl host API`,必须显式区分 `CRM_HOST_BASE` 与 `REMOTE_HOST_BASE` + +#### 2) 远端 DB/Redis 误指到 relaymgr 数据面 + +之前 remote43 统一 `401 INVALID_API_KEY` 的主因不是 provider key 坏,而是: +- 脚本错误地从 `sub2api-relaymgr-pg` 里找普通用户 key +- 但实际宿主是另一套 fresh-host app + postgres + redis + +修正后脚本已经改为: +- 先按目标宿主端口解析远端 `app` 容器 +- 再自动推导同栈的 `postgres/redis` + +2026-05-23 的 `20260523_144937_remote43_kimi-a7m_key_import` 已证明这条修正生效: +- `subscription_user_key_prefix`、`managed_user_id`、`managed_probe_key_prefix` 都来自目标 fresh-host 数据面 +- 不再复现统一 `401` + +经验结论: +- 远端若同时存在 `relaymgr` 和 `fresh-host` 两套栈,**任何 subscription user / api key / group state / redis invalidation 都必须落到目标宿主自己的数据面** +- 不要再靠固定容器名假设 + +#### 3) provider status / access status 忘了带 `host_id` + +当本地 CRM 状态库里同一个 provider 已经跑过多个 host 样本时: +- `GET /api/providers/{provider}/status` +- `GET /api/providers/{provider}/access/status` +- `POST /api/providers/{provider}/access/preview` + +如果不显式带 `host_id`,很容易直接返回: +- `provider exists on multiple hosts; host_id is required` +- 外部看起来像验收在最后一步莫名其妙 `400` + +经验结论: +- 这不是导入失败,也不是宿主坏了 +- 这是 **状态查询维度不完整** +- 对带历史样本的 live CRM,所有 provider 尾部查询都应该带 `host_id` + +### 14. `20260523_144937_remote43_kimi-a7m_key_import` 到底证明了什么? + +这份 artifact 很关键,因为它把“脚本问题”和“宿主问题”拆开了。 + +它证明了: +- `POST /api/hosts` 已成功 +- import 已成功返回 `HTTP 200` +- `gateway.models=["kimi-k2.6"]` +- `has_expected_model=true` +- upstream `/models=200` +- upstream `/chat/completions=200` + +同时它也证明: +- 未改宿主的 host `/v1/chat/completions` 仍然返回 `503 Service temporarily unavailable` +- account probe 仍是 `403 Forbidden`,但已经只是 advisory / warning,不再阻断 import 主链 + +经验结论: +- 这份样本可以用来证明:**插件脚本的数据面/地址问题已经修掉** +- 它不能用来证明“宿主已经通过” +- 它应该被归类为:**插件侧修复完成,未改宿主 completion 路径仍异常** + +### 15. 如果 create-host 阶段突然又回到 `500`,先查什么? + +`20260523_145531_remote43_kimi-a7m_key_import` 提供了另一类重要样本: + +- `01a-create-host.json` 仍成功 +- 但 `03-import.body.json` 直接写明: + - `get host version: perform GET /api/v1/admin/system/version request` + - `context deadline exceeded` + +这说明当时不是 provider key 坏,不是脚本回退,而是: +- 本地 `18089` 隧道虽然监听着端口 +- 但到远端宿主的链路已经不再返回字节 + +经验结论: +- 如果 `host tunnel` 端口还在监听,但 `curl -I --max-time 5 $CRM_HOST_BASE/healthz` 无法返回任何 header +- 那就先把它当成 **隧道失活 / 运行时链路问题** +- 不要先把结论写成“导入逻辑回退”或“provider 又坏了” + ### 10. 新增供应链账号或模型后,哪些结果可以算“已确认”,哪些只能算“部分确认”? #### 可算“已确认” diff --git a/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md b/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md index c513bc65..fbfe8c15 100644 --- a/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md +++ b/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md @@ -232,17 +232,33 @@ SKIP_ROLLBACK=1 scripts/real_host_acceptance.sh 18. self_service 场景里,普通用户 gateway key 访问宿主 `/v1/models` / `/v1/chat/completions` 时,真实语义是 `Authorization: Bearer `;若 CRM 的 self_service closure 仍显示 `401/403 broken`,优先排查 gateway probe 是否错误复用了 `x-api-key`。 19. fresh-host 管理员 bearer token 过期时,最前面的 `POST /api/hosts` / `probe-host` 可能直接表现成 CRM 侧 `502`。遇到这类现象,先刷新 host bearer token,再继续验收,不要先把它归因为最新代码故障。 20. shared fresh-host 上若 `05-import.json` / `07-access-status.json` 已经 ready,而 `09-reconcile.json` 仍是 `status=drifted`,优先把它解释为历史残留资源噪音;PRD 首版放行判断应以 import/access 闭环是否打通为主。 +21. 如果 CRM 本身运行在本机,而宿主运行在远端 SSH 隧道后面,必须同时明确 2 个地址: + - `CRM_HOST_BASE`:本地 CRM 实际访问宿主时使用的地址,例如 `http://127.0.0.1:18089` + - `REMOTE_HOST_BASE`:远端 SSH 会话内部访问宿主时使用的地址,例如 `http://127.0.0.1:18097` + - 两者不能混用;混用后 `POST /api/hosts` 往往会先在 `get host version` / `probe host capabilities` 处直接变成 `500` +22. 如果远端同时存在 `relaymgr` 和 `fresh-host` 两套容器栈,不要手填 `REMOTE_PG_CONTAINER` / `REMOTE_REDIS_CONTAINER` 到旧的 relaymgr 容器。优先按目标宿主端口自动解析同栈的 `postgres/redis`;否则最容易回到“错库取 key 导致统一 401”。 +23. 若同一个 provider 已在本地 CRM 状态库里跑过多个宿主样本,尾部查询必须带 `host_id`: + - `GET /api/providers/{provider}/status` + - `GET /api/providers/{provider}/access/status` + - `POST /api/providers/{provider}/access/preview` + - 否则很容易在最后一步收到 `provider exists on multiple hosts; host_id is required` +24. 不要把“隧道端口还在 LISTEN”误判成“链路可用”。 + - 若 `curl -I --max-time 5 $CRM_HOST_BASE/healthz` 完全收不到 header + - 就应先判定为 tunnel 失活或远端链路异常 + - 这类现象会在 `03-import.body.json` 中表现为 `get host version ... context deadline exceeded` ## 建议固定执行的快速诊断顺序 1. 先看环境 - - CRM 是否是最新提交对应的在线进程 - - `PACK_PATH` 是否是 CRM 本机可读路径 - - `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致 + - CRM 是否是最新提交对应的在线进程 + - `PACK_PATH` 是否是 CRM 本机可读路径 + - `CRM_HOST_BASE` 是否与 CRM 到 host 的实际访问地址一致 + - 如果走 SSH 隧道,`CRM_HOST_BASE` 是否真的可在本机 `curl -I --max-time 5` 读到响应 + - 若脚本还要在 SSH 会话里执行 host probe,`REMOTE_HOST_BASE` 是否与远端主机看到的地址一致 2. 再看宿主落库 - - account `credentials.model_mapping` - - `GET /api/v1/admin/accounts/:id/models` - - channel `model_mapping/model_pricing/restrict_models/billing_model_source` + - account `credentials.model_mapping` + - `GET /api/v1/admin/accounts/:id/models` + - channel `model_mapping/model_pricing/restrict_models/billing_model_source` 3. 最后看普通用户流量 - `/v1/models` - `/v1/chat/completions` diff --git a/scripts/import_remote43_provider.sh b/scripts/import_remote43_provider.sh index c98fd4bb..df626552 100755 --- a/scripts/import_remote43_provider.sh +++ b/scripts/import_remote43_provider.sh @@ -15,10 +15,11 @@ REMOTE="${REMOTE:-ubuntu@43.155.133.187}" CRM_BASE="${CRM_BASE:-http://127.0.0.1:18088}" HOST_BASE="${HOST_BASE:-http://127.0.0.1:18087}" CRM_HOST_BASE="${CRM_HOST_BASE:-$HOST_BASE}" +REMOTE_HOST_BASE="${REMOTE_HOST_BASE:-$CRM_HOST_BASE}" HOST_NAME="${HOST_NAME:-remote43-current-host}" REMOTE_HOST_ENV_FILE="${REMOTE_HOST_ENV_FILE:-/home/ubuntu/sub2api-host-validation-fresh-deepseek-20260519_115244/.env}" -REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-sub2api-relaymgr-pg}" -REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-sub2api-relaymgr-redis}" +REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-}" +REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-}" PACK_PATH="${PACK_PATH:-$ROOT_DIR/packs/openai-cn-pack}" ROOT="${ROOT:-$ROOT_DIR/artifacts/real-host-acceptance}" ART="${ART:-$ROOT/$(date +%Y%m%d_%H%M%S)_remote43_${provider_id}_key_import}" @@ -26,8 +27,6 @@ MIN_BALANCE="${MIN_BALANCE:-10}" SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" SUBSCRIPTION_NOTES="${SUBSCRIPTION_NOTES:-hermes remote subscription validation}" mkdir -p "$ART" -REMOTE_PG_CONTAINER_Q="$(printf '%q' "$REMOTE_PG_CONTAINER")" -REMOTE_REDIS_CONTAINER_Q="$(printf '%q' "$REMOTE_REDIS_CONTAINER")" if [[ -n "$key_file" ]]; then upstream_key="$(tr -d '\r\n' < "$key_file")" @@ -60,6 +59,62 @@ ssh_cmd() { ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd" } +resolve_remote_host_runtime() { + local remote_port host_containers + remote_port="$(python3 - "$REMOTE_HOST_BASE" <<'PY' +from urllib.parse import urlparse +import sys + +target = urlparse(sys.argv[1]) +if target.port is not None: + print(target.port) +elif target.scheme == 'https': + print(443) +else: + print(80) +PY +)" + host_containers="$(ssh_cmd "sudo -n docker ps --format '{{.Names}}\t{{.Ports}}'")" + HOST_CONTAINERS="$host_containers" python3 - "$remote_port" <<'PY' +import os +import sys + +port = sys.argv[1] +rows = os.environ.get("HOST_CONTAINERS", "").splitlines() +for row in rows: + name, _, ports = row.partition('\t') + if f":{port}->" not in ports: + continue + app = name.strip() + if app.endswith("-app-1"): + prefix = app[:-len("-app-1")] + print(app) + print(f"{prefix}-postgres-1") + print(f"{prefix}-redis-1") + raise SystemExit(0) + if app.endswith("-app"): + prefix = app[:-len("-app")] + print(app) + print(f"{prefix}-pg") + print(f"{prefix}-redis") + raise SystemExit(0) +raise SystemExit(f"unable to derive target host containers from port {port}") +PY +} + +if [[ -z "$REMOTE_PG_CONTAINER" || -z "$REMOTE_REDIS_CONTAINER" ]]; then + mapfile -t resolved_remote_runtime < <(resolve_remote_host_runtime) + if [[ ${#resolved_remote_runtime[@]} -lt 3 ]]; then + echo "unable to resolve remote host runtime containers for $REMOTE_HOST_BASE" >&2 + exit 2 + fi + REMOTE_PG_CONTAINER="${REMOTE_PG_CONTAINER:-${resolved_remote_runtime[1]}}" + REMOTE_REDIS_CONTAINER="${REMOTE_REDIS_CONTAINER:-${resolved_remote_runtime[2]}}" +fi + +REMOTE_PG_CONTAINER_Q="$(printf '%q' "$REMOTE_PG_CONTAINER")" +REMOTE_REDIS_CONTAINER_Q="$(printf '%q' "$REMOTE_REDIS_CONTAINER")" + build_managed_subscription_identity_json() { local selector="$1" local group_id="$2" @@ -130,7 +185,7 @@ from pathlib import Path import json, subprocess, sys env_path = Path(${REMOTE_HOST_ENV_FILE@Q}) -host_base = ${HOST_BASE@Q} +host_base = ${REMOTE_HOST_BASE@Q} vals = {} for line in env_path.read_text().splitlines(): if '=' not in line: @@ -318,13 +373,14 @@ $(remote_pg_query "$create_user_sql") EOF fi -python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" <<'PY' +python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$REMOTE_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" <<'PY' import json, sys, pathlib -path, crm, host, crm_host, provider_id, sub_uid, sub_key = sys.argv[1:8] +path, crm, host, crm_host, remote_host, provider_id, sub_uid, sub_key = sys.argv[1:9] pathlib.Path(path).write_text(json.dumps({ 'crm_base': crm, 'host_base': host, 'crm_host_base': crm_host, + 'remote_host_base': remote_host, 'provider_id': provider_id, 'subscription_user_id': sub_uid, 'subscription_user_key_prefix': sub_key[:12], @@ -432,13 +488,14 @@ else remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json" fi -python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" "$managed_user_email" "$managed_probe_key" "$managed_user_id" <<'PY' +python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$CRM_HOST_BASE" "$REMOTE_HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" "$managed_user_email" "$managed_probe_key" "$managed_user_id" <<'PY' import json, sys, pathlib -path, crm, host, crm_host, provider_id, sub_uid, sub_key, group_id, admin_uid, managed_user_email, managed_probe_key, managed_user_id = sys.argv[1:13] +path, crm, host, crm_host, remote_host, provider_id, sub_uid, sub_key, group_id, admin_uid, managed_user_email, managed_probe_key, managed_user_id = sys.argv[1:14] pathlib.Path(path).write_text(json.dumps({ 'crm_base': crm, 'host_base': host, 'crm_host_base': crm_host, + 'remote_host_base': remote_host, 'provider_id': provider_id, 'subscription_user_id': sub_uid, 'subscription_user_key_prefix': sub_key[:12], @@ -460,11 +517,11 @@ print(json.dumps({ }, ensure_ascii=False)) PY )" -ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $managed_probe_key' $HOST_BASE/v1/models" +ssh_cmd "curl -sS -D /tmp/models_headers.txt -o /tmp/models_body.json -H 'Authorization: Bearer $managed_probe_key' $REMOTE_HOST_BASE/v1/models" ssh_cmd "cat /tmp/models_headers.txt" > "$ART/09-models.headers.txt" ssh_cmd "cat /tmp/models_body.json" > "$ART/10-models.body.json" -ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $managed_probe_key' -H 'Content-Type: application/json' $HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")" +ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer $managed_probe_key' -H 'Content-Type: application/json' $REMOTE_HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")" ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/11-chat.headers.txt" ssh_cmd "cat /tmp/chat_body.json" > "$ART/12-chat.body.json" @@ -476,14 +533,21 @@ ssh_cmd "curl -sS -D /tmp/upstream_chat_headers.txt -o /tmp/upstream_chat_body.t ssh_cmd "cat /tmp/upstream_chat_headers.txt" > "$ART/19-upstream-chat.headers.txt" ssh_cmd "cat /tmp/upstream_chat_body.txt" > "$ART/20-upstream-chat.body.txt" -crm_curl_json GET "/api/providers/$provider_id/status" > "$ART/13-provider-status.json" -crm_curl_json GET "/api/providers/$provider_id/access/status" > "$ART/14-access-status.json" +provider_query_suffix="?host_id=$(python3 - "$HOST_NAME" <<'PY' +import sys +from urllib.parse import quote +print(quote(sys.argv[1], safe='')) +PY +)" + +crm_curl_json GET "/api/providers/$provider_id/status${provider_query_suffix}" > "$ART/13-provider-status.json" +crm_curl_json GET "/api/providers/$provider_id/access/status${provider_query_suffix}" > "$ART/14-access-status.json" preview_payload="$(python3 - "$provider_id" <<'PY' import json, sys print(json.dumps({'provider_id': sys.argv[1], 'mode': 'subscription'}, ensure_ascii=False)) PY )" -crm_curl_json POST "/api/providers/$provider_id/access/preview" "$preview_payload" > "$ART/15-access-preview.json" +crm_curl_json POST "/api/providers/$provider_id/access/preview${provider_query_suffix}" "$preview_payload" > "$ART/15-access-preview.json" crm_curl_json GET "/api/import-batches/$batch_id" > "$ART/16-batch-detail-final.json" python3 - "$ART" "$provider_id" "$batch_id" "$subscription_group_id" "$model_name" <<'PY' diff --git a/scripts/test_real_host_scripts.sh b/scripts/test_real_host_scripts.sh index 6516ffc4..a951a097 100644 --- a/scripts/test_real_host_scripts.sh +++ b/scripts/test_real_host_scripts.sh @@ -16,6 +16,14 @@ assert_contains() { fi } +assert_not_contains() { + local haystack="$1" + local needle="$2" + if [[ "$haystack" == *"$needle"* ]]; then + fail "expected to avoid [$needle] in [$haystack]" + fi +} + run_test_build_subscription_access_prep_sql() { # shellcheck disable=SC1091 source "$ROOT_DIR/scripts/host_access_prep_lib.sh" @@ -314,13 +322,13 @@ case "$url" in */api/import-batches/123) write_body '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}' ;; - */api/providers/deepseek/status) + */api/providers/deepseek/status*) write_body '{"status":"ready"}' ;; - */api/providers/deepseek/access/status) + */api/providers/deepseek/access/status*) write_body '{"latest_access_status":"subscription_ready"}' ;; - */api/providers/deepseek/access/preview) + */api/providers/deepseek/access/preview*) write_body '{"available":true}' ;; *) @@ -342,6 +350,9 @@ if [[ "$cmd" == *'***'* ]]; then exit 1 fi case "$cmd" in + "sudo -n docker ps --format '{{.Names}}\t{{.Ports}}'"*) + printf '%s\n' 'sub2api-fresh-deepseek-20260519_115244-app-1 127.0.0.1:18093->8080/tcp' + ;; *"/api/v1/auth/login"*) printf '%s\n' 'host-bearer-token' ;; @@ -425,10 +436,10 @@ fi *"/api/providers/deepseek/reconcile"*) printf '%s\n' '{"status":"in_sync"}' ;; - *"sudo -n docker exec -i fresh-pg psql -U sub2api -d sub2api -At -F ''"*) + *"sudo -n docker exec -i sub2api-fresh-deepseek-20260519_115244-postgres-1 psql -U sub2api -d sub2api -At -F ''"*) printf '%s\n' '{"group_id":7,"subscription":{"status":"active"},"key":{"group_id":7}}' ;; - *"sudo -n docker exec -i fresh-pg psql -U sub2api -d sub2api"*) + *"sudo -n docker exec -i sub2api-fresh-deepseek-20260519_115244-postgres-1 psql -U sub2api -d sub2api"*) CMD="$cmd" LOG_DIR="$log_dir" python3 - <<'PY' import base64, os, re, pathlib, sys cmd = os.environ['CMD'] @@ -454,7 +465,7 @@ else: print('') PY ;; - *"sudo -n docker exec fresh-redis redis-cli DEL apikey:auth:"*" billing:balance:"*" billing:sub:"*":7"*) + *"sudo -n docker exec sub2api-fresh-deepseek-20260519_115244-redis-1 redis-cli DEL apikey:auth:"*" billing:balance:"*" billing:sub:"*":7"*) printf '%s\n' '3' ;; *) @@ -472,11 +483,10 @@ EOF CRM_BASE="http://127.0.0.1:18088" \ HOST_BASE="http://127.0.0.1:18087" \ CRM_HOST_BASE="http://127.0.0.1:18093" \ + REMOTE_HOST_BASE="http://127.0.0.1:18093" \ ROOT="$artifact_dir/root" \ ART="$artifact_dir/run" \ PACK_PATH="$pack_dir" \ - REMOTE_PG_CONTAINER="fresh-pg" \ - REMOTE_REDIS_CONTAINER="fresh-redis" \ UPSTREAM_KEY="upstream-test-key" \ SUBSCRIPTION_DAYS=30 \ MIN_BALANCE=10 \ @@ -493,6 +503,7 @@ EOF local runtime_context invalidation_log runtime_context="$(cat "$artifact_dir/run/01-runtime-context.json")" assert_contains "$runtime_context" '"crm_host_base": "http://127.0.0.1:18093"' + assert_contains "$runtime_context" '"remote_host_base": "http://127.0.0.1:18093"' invalidation_log="$(cat "$artifact_dir/run/07-redis-targeted-invalidation.txt")" assert_contains "$invalidation_log" "auth_cache_key=apikey:auth:" assert_contains "$invalidation_log" "balance_cache_key=billing:balance:84" @@ -513,6 +524,13 @@ EOF assert_contains "$summary_json" '"upstream_models_has_expected_model": true' assert_contains "$summary_json" '"completion_classification": "unknown"' [[ -s "$ssh_log" ]] || fail "ssh log was empty" + local ssh_contents + ssh_contents="$(cat "$ssh_log")" + assert_contains "$ssh_contents" "sudo -n docker ps --format" + assert_contains "$ssh_contents" "http://127.0.0.1:18093/v1/models" + assert_contains "$ssh_contents" "http://127.0.0.1:18093/v1/chat/completions" + assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/models" + assert_not_contains "$ssh_contents" "http://127.0.0.1:18087/v1/chat/completions" } run_test_build_subscription_access_prep_sql