diff --git a/docs/EXECUTION_BOARD.md b/docs/EXECUTION_BOARD.md index 9fcc864f..25b4baca 100644 --- a/docs/EXECUTION_BOARD.md +++ b/docs/EXECUTION_BOARD.md @@ -70,6 +70,9 @@ - `subscription` 需要 subscription 类型 group + 普通用户订阅分配 + key/group 绑定 3. 标准多阶段 Dockerfile 在受限网络环境下仍不稳 - 当前推荐 `scripts/build_local_image.sh` + `Dockerfile.local` +4. 真实宿主验收工具已补自动化闭环 + - `scripts/real_host_acceptance.sh` 支持 `AFTER_IMPORT_HOOK_COMMAND`,可把宿主侧 access 前置动作收敛进同一条 artifact 链 + - `scripts/import_remote43_provider.sh` 已内置 remote43 subscription 的“补余额 + key/group 绑定 + subscription upsert + Redis flush + host state 落盘” ## 当前最短上线路径 diff --git a/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md b/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md index 3abbe333..99f274e7 100644 --- a/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md +++ b/docs/REAL_HOST_ACCEPTANCE_RUNBOOK.md @@ -91,6 +91,31 @@ SUBSCRIPTION_DAYS=30 \ scripts/real_host_acceptance.sh ``` +### 5. 导入后自动补 access 前置(可选) + +当真实宿主需要额外完成“普通用户余额 / key-group 绑定 / 订阅写入 / 缓存失效”等宿主侧动作时,可在 import 完成后插入自定义 hook: + +```bash +AFTER_IMPORT_HOOK_COMMAND='bash /path/to/host-access-hook.sh' \ +... \ +scripts/real_host_acceptance.sh +``` + +hook 执行时会额外导出: +- `BATCH_ID` +- `BATCH_DETAIL_FILE`(若非 dry-run,会指向 `05a-batch-detail-pre-access.json`) +- `PROVIDER_ID` +- `HOST_BASE_URL` +- `CRM_BASE_URL` +- `ACCESS_MODE` +- `MODE` +- `ARTIFACT_DIR` + +标准产物会新增: +- `05a-batch-detail-pre-access.json` +- `05b-after-import-hook.stdout.txt` +- `05b-after-import-hook.stderr.txt` + ## 产物 脚本会把每一步 JSON 响应落到: @@ -105,6 +130,8 @@ artifacts/real-host-acceptance// - `03-install-pack.json` - `04-preview-import.json` - `05-import.json` +- `05a-batch-detail-pre-access.json`(若拿到了 `batch_id` 且非 dry-run) +- `05b-after-import-hook.stdout.txt` / `05b-after-import-hook.stderr.txt`(若配置了 hook) - `06-access-preview.json` - `07-access-status.json` - `08-provider-status.json` @@ -151,3 +178,4 @@ SKIP_ROLLBACK=1 scripts/real_host_acceptance.sh 7. `self_service` 验证除普通用户 key 外,还需要该 key 绑定目标 group;若目标 group 是标准计费组,还需要用户侧具备可用余额,否则 `/v1/models` 可能从“未授权”转为 `INSUFFICIENT_BALANCE`。 8. `subscription` 验证需要目标 group 本身是 `subscription` 类型,并且完成“普通用户订阅分配 + 普通用户 key 绑定该 group”;仅有管理员主体或未绑定 key 不足以通过 `/v1/models`。 9. 若需要验证 `reconcile` 收敛,优先在干净宿主场景或隔离 group 下执行,避免历史残留资源把结果污染成 `status=drifted` / `extra_count>0`。 +10. `scripts/import_remote43_provider.sh` 现已内置 remote43 的 subscription 验收补全动作:会根据 import batch 自动解析目标 group,执行“普通用户最低余额补齐 + key/group 绑定 + user_subscriptions upsert + Redis flush”,并把 SQL / host state 证据写入 artifact 目录。 diff --git a/scripts/host_access_prep_lib.sh b/scripts/host_access_prep_lib.sh new file mode 100644 index 00000000..f1b5f782 --- /dev/null +++ b/scripts/host_access_prep_lib.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +sql_escape_literal() { + local value="$1" + local squote="'" + value=${value//$squote/$squote$squote} + printf '%s' "$value" +} + +sql_literal() { + printf "'%s'" "$(sql_escape_literal "$1")" +} + +build_top_up_user_balance_sql() { + local user_id="$1" + local min_balance="$2" + cat <&2 + exit 2 +fi + +ssh_cmd() { + local cmd="$1" + ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$cmd" +} + +remote_pg_exec() { + local sql="$1" + local encoded + encoded="$(printf '%s' "$sql" | base64 -w0)" + ssh_cmd "printf '%s' '$encoded' | base64 -d | sudo -n docker exec -i sub2api-relaymgr-pg psql -U sub2api -d sub2api" +} + +remote_fetch_group_state() { + local group_id="$1" + local user_id="$2" + local api_key="$3" + local output_path="$4" + local encoded + encoded="$(python3 - "$group_id" "$user_id" "$api_key" <<'PY' +import json, sys + +group_id, user_id, api_key = sys.argv[1:4] +query = f""" +WITH group_row AS ( + SELECT row_to_json(g) AS data FROM groups g WHERE g.id = {group_id} +), +subscription_row AS ( + SELECT row_to_json(s) AS data FROM user_subscriptions s + WHERE s.user_id = {user_id} AND s.group_id = {group_id} AND s.deleted_at IS NULL + ORDER BY s.id DESC LIMIT 1 +), +key_row AS ( + SELECT row_to_json(k) AS data FROM api_keys k WHERE k.key = {json.dumps(api_key)} +) +SELECT json_build_object( + 'group_id', {group_id}, + 'group', (SELECT data FROM group_row), + 'subscription', (SELECT data FROM subscription_row), + 'key', (SELECT data FROM key_row) +); +""" +print(query) +PY +)" + ssh_cmd "printf '%s' '$encoded' | base64 -d | sudo -n docker exec -i sub2api-relaymgr-pg psql -U sub2api -d sub2api -At -F ''" > "$output_path" +} + +python3 - "$ART/00-local-key-source.json" "$key_source" "$provider_id" "$upstream_key" <<'PY' +import json, sys, pathlib +path, source, provider_id, key = sys.argv[1:5] +pathlib.Path(path).write_text(json.dumps({ + 'source': source, + 'provider_id': provider_id, + 'upstream_key_prefix': key[:12], + 'upstream_key_suffix': key[-6:], +}, ensure_ascii=False, indent=2), encoding='utf-8') +PY + +crm_token="$(ssh_cmd "grep ^SUB2API_CRM_ADMIN_TOKEN= /home/ubuntu/sub2api-cn-relay-manager/.env.remote | cut -d= -f2-")" +crm_token="${crm_token##*$'\n'}" +admin_key="$(ssh_cmd "sudo -n docker exec sub2api-relaymgr-pg psql -U sub2api -d sub2api -Atc \"select value from settings where key='admin_api_key';\"")" +admin_key="${admin_key##*$'\n'}" +admin_uid="$(ssh_cmd "sudo -n docker exec sub2api-relaymgr-pg psql -U sub2api -d sub2api -Atc \"select id from users where role='admin' order by id asc limit 1;\"")" +admin_uid="${admin_uid##*$'\n'}" +sub_uid="$(ssh_cmd "sudo -n docker exec sub2api-relaymgr-pg psql -U sub2api -d sub2api -Atc \"select id from users where email like 'relay-sub-%@sub2api.local' order by id desc limit 1;\"")" +sub_uid="${sub_uid##*$'\n'}" +sub_key="$(ssh_cmd "sudo -n docker exec sub2api-relaymgr-pg psql -U sub2api -d sub2api -Atc \"select k.key from users u join api_keys k on k.user_id=u.id where u.email like 'relay-sub-%@sub2api.local' order by u.id desc limit 1;\"")" +sub_key="${sub_key##*$'\n'}" + +python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" <<'PY' +import json, sys, pathlib +path, crm, host, provider_id, sub_uid, sub_key = sys.argv[1:7] +pathlib.Path(path).write_text(json.dumps({ + 'crm_base': crm, + 'host_base': host, + 'provider_id': provider_id, + 'subscription_user_id': sub_uid, + 'subscription_user_key_prefix': sub_key[:12], +}, ensure_ascii=False, indent=2), encoding='utf-8') +PY + +payload="$(python3 - "$HOST_BASE" "$admin_key" "$PACK_PATH" "$provider_id" "$upstream_key" "$sub_key" "$sub_uid" "$SUBSCRIPTION_DAYS" <<'PY' +import json, sys +host_base, admin_key, pack_path, provider_id, upstream_key, sub_key, sub_uid, subscription_days = sys.argv[1:9] +print(json.dumps({ + 'host_base_url': host_base, + 'host_api_key': admin_key, + 'pack_path': pack_path, + 'provider_id': provider_id, + 'keys': [upstream_key], + 'mode': 'partial', + 'access_mode': 'subscription', + 'access_api_key': sub_key, + 'subscription_days': int(subscription_days), + 'subscription_users': [sub_uid], +}, ensure_ascii=False)) +PY +)" + +ssh_cmd "curl -sS -D /tmp/import_headers.txt -o /tmp/import_body.json -X POST -H 'Authorization: Bearer $crm_token' -H 'Content-Type: application/json' $CRM_BASE/api/providers/$provider_id/import -d $(printf %q "$payload")" +ssh_cmd "cat /tmp/import_headers.txt" > "$ART/02-import.headers.txt" +ssh_cmd "cat /tmp/import_body.json" > "$ART/03-import.body.json" + +batch_id="$(python3 - "$ART/03-import.body.json" <<'PY' +import json, sys, pathlib +obj=json.loads(pathlib.Path(sys.argv[1]).read_text()) +print(obj['batch_id']) +PY +)" + +ssh_cmd "curl -sS -H 'Authorization: Bearer *** $CRM_BASE/api/import-batches/$batch_id" > "$ART/04-batch-detail-initial.json" +subscription_group_id="$(python3 - "$ART/04-batch-detail-initial.json" <<'PY' +import json, pathlib, sys +obj = json.loads(pathlib.Path(sys.argv[1]).read_text()) +for item in obj.get('managed_resources', []): + if item.get('ResourceType') == 'group': + print(item.get('HostResourceID', '')) + break +else: + raise SystemExit('missing managed group in batch detail') +PY +)" + +prep_sql="$(build_subscription_access_prep_sql "$sub_uid" "$sub_key" "$subscription_group_id" "$MIN_BALANCE" "$SUBSCRIPTION_DAYS" "$admin_uid" "$SUBSCRIPTION_NOTES")" +python3 - "$ART/05-subscription-access-prep.sql" "$prep_sql" <<'PY' +import pathlib, sys +pathlib.Path(sys.argv[1]).write_text(sys.argv[2], encoding='utf-8') +PY +remote_pg_exec "$prep_sql" > "$ART/06-subscription-access-prep.psql.txt" +ssh_cmd "sudo -n docker exec sub2api-relaymgr-redis redis-cli FLUSHDB" > "$ART/07-redis-flush.txt" +remote_fetch_group_state "$subscription_group_id" "$sub_uid" "$sub_key" "$ART/08-subscription-group-state.json" + +python3 - "$ART/01-runtime-context.json" "$CRM_BASE" "$HOST_BASE" "$provider_id" "$sub_uid" "$sub_key" "$subscription_group_id" "$admin_uid" <<'PY' +import json, sys, pathlib +path, crm, host, provider_id, sub_uid, sub_key, group_id, admin_uid = sys.argv[1:9] +pathlib.Path(path).write_text(json.dumps({ + 'crm_base': crm, + 'host_base': host, + 'provider_id': provider_id, + 'subscription_user_id': sub_uid, + 'subscription_user_key_prefix': sub_key[:12], + 'subscription_group_id': group_id, + 'admin_user_id': admin_uid, +}, ensure_ascii=False, indent=2), encoding='utf-8') +PY + +probe_payload="$(python3 - "$model_name" <<'PY' +import json, sys +print(json.dumps({ + 'model': sys.argv[1], + 'messages': [{'role':'user','content':'ping'}], + 'max_tokens': 8, + 'temperature': 0, +}, ensure_ascii=False)) +PY +)" +ssh_cmd "curl -sS -D /tmp/chat_headers.txt -o /tmp/chat_body.json -H 'Authorization: Bearer *** -H 'Content-Type: application/json' $HOST_BASE/v1/chat/completions -d $(printf %q "$probe_payload")" +ssh_cmd "cat /tmp/chat_headers.txt" > "$ART/09-chat.headers.txt" +ssh_cmd "cat /tmp/chat_body.json" > "$ART/10-chat.body.json" + +ssh_cmd "curl -sS -H 'Authorization: Bearer *** $CRM_BASE/api/providers/$provider_id/status" > "$ART/11-provider-status.json" +ssh_cmd "curl -sS -H 'Authorization: Bearer *** $CRM_BASE/api/providers/$provider_id/access/status" > "$ART/12-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 +)" +ssh_cmd "curl -sS -X POST -H 'Authorization: Bearer *** -H 'Content-Type: application/json' $CRM_BASE/api/providers/$provider_id/access/preview -d $(printf %q "$preview_payload")" > "$ART/13-access-preview.json" +ssh_cmd "curl -sS -H 'Authorization: Bearer *** $CRM_BASE/api/import-batches/$batch_id" > "$ART/14-batch-detail-final.json" + +python3 - "$ART" "$provider_id" "$batch_id" <<'PY' +import json, pathlib, sys +art=pathlib.Path(sys.argv[1]) +provider_id=sys.argv[2] +batch_id=int(sys.argv[3]) +import_obj=json.loads((art/'03-import.body.json').read_text()) +access_status=json.loads((art/'12-access-status.json').read_text()) +preview=json.loads((art/'13-access-preview.json').read_text()) +chat_headers=(art/'09-chat.headers.txt').read_text() +group_state=json.loads((art/'08-subscription-group-state.json').read_text()) +summary={ + 'artifact_dir': str(art), + 'provider_id': provider_id, + 'batch_id': batch_id, + 'batch_status': import_obj.get('batch_status'), + 'access_status_from_import': import_obj.get('access_status'), + 'provider_status_from_import': import_obj.get('provider_status'), + 'direct_chat_http200': '200 OK' in chat_headers, + 'latest_access_status': access_status.get('latest_access_status') or access_status.get('batch_access_status'), + 'preview_available': preview.get('available'), + 'accepted_keys_count': import_obj.get('accepted_keys_count'), + 'subscription_group_id': group_state.get('group_id'), + 'key_group_id': (group_state.get('key') or {}).get('group_id'), + 'subscription_status': (group_state.get('subscription') or {}).get('status'), +} +print(json.dumps(summary, ensure_ascii=False)) +PY diff --git a/scripts/real_host_acceptance.sh b/scripts/real_host_acceptance.sh index 7fce7e31..89629963 100755 --- a/scripts/real_host_acceptance.sh +++ b/scripts/real_host_acceptance.sh @@ -116,6 +116,7 @@ require_var PROVIDER_ID MODE="${MODE:-partial}" ACCESS_MODE="${ACCESS_MODE:-self_service}" SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" +AFTER_IMPORT_HOOK_COMMAND="${AFTER_IMPORT_HOOK_COMMAND:-}" if [[ -n "${HOST_BEARER_TOKEN:-}" ]]; then HOST_AUTH_TYPE="${HOST_AUTH_TYPE:-bearer}" @@ -208,6 +209,21 @@ RESP_IMPORT="$(curl_json POST "/api/providers/$PROVIDER_ID/import" "$IMPORT_PAYL save_json 05-import "$RESP_IMPORT" BATCH_ID="$(printf '%s' "$RESP_IMPORT" | json_get batch_id || true)" +if [[ -n "$BATCH_ID" && "$DRY_RUN" != "1" ]]; then + RESP_BATCH_DETAIL="$(curl_json GET "/api/import-batches/$BATCH_ID")" + save_json 05a-batch-detail-pre-access "$RESP_BATCH_DETAIL" + export BATCH_DETAIL_FILE="$ARTIFACT_DIR/05a-batch-detail-pre-access.json" +else + unset BATCH_DETAIL_FILE || true +fi + +if [[ -n "$AFTER_IMPORT_HOOK_COMMAND" ]]; then + export BATCH_ID PROVIDER_ID HOST_BASE_URL CRM_BASE_URL ACCESS_MODE MODE ARTIFACT_DIR + bash -lc "$AFTER_IMPORT_HOOK_COMMAND" \ + >"$ARTIFACT_DIR/05b-after-import-hook.stdout.txt" \ + 2>"$ARTIFACT_DIR/05b-after-import-hook.stderr.txt" +fi + echo "batch_id=${BATCH_ID:-unknown}" ACCESS_PREVIEW_PAYLOAD="$(python3 - <<'PY' diff --git a/scripts/test_real_host_scripts.sh b/scripts/test_real_host_scripts.sh new file mode 100644 index 00000000..6eebe28b --- /dev/null +++ b/scripts/test_real_host_scripts.sh @@ -0,0 +1,249 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +fail() { + echo "FAIL: $*" >&2 + exit 1 +} + +assert_contains() { + local haystack="$1" + local needle="$2" + if [[ "$haystack" != *"$needle"* ]]; then + fail "expected to find [$needle] in [$haystack]" + fi +} + +run_test_build_subscription_access_prep_sql() { + # shellcheck disable=SC1091 + source "$ROOT_DIR/scripts/host_access_prep_lib.sh" + + local sql + sql="$(build_subscription_access_prep_sql 42 'sk-test-123' 7 10 30 1 'hermes remote subscription validation')" + + assert_contains "$sql" "UPDATE users" + assert_contains "$sql" "balance < 10" + assert_contains "$sql" "UPDATE api_keys" + assert_contains "$sql" "group_id = 7" + assert_contains "$sql" "key = 'sk-test-123'" + assert_contains "$sql" "INSERT INTO user_subscriptions" + assert_contains "$sql" "ON CONFLICT (user_id, group_id) WHERE deleted_at IS NULL" + assert_contains "$sql" "now() + interval '30 days'" + + local quoted_sql + quoted_sql="$(build_bind_api_key_group_sql "sk-o'reilly" 7)" + assert_contains "$quoted_sql" "WHERE key = 'sk-o''reilly'" +} + +run_test_real_host_acceptance_after_import_hook() { + local tmpdir fakebin artifact_dir hook_file + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' RETURN + fakebin="$tmpdir/bin" + artifact_dir="$tmpdir/artifacts" + hook_file="$artifact_dir/hook.txt" + mkdir -p "$fakebin" + + cat > "$fakebin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +url="" +for arg in "$@"; do + if [[ "$arg" == http://* || "$arg" == https://* ]]; then + url="$arg" + fi +done +[[ -n "$url" ]] || { + echo "missing url in curl args: $*" >&2 + exit 1 +} +case "$url" in + */api/hosts/test-host) + printf '%s\n' '{"host_id":"test-host"}' + ;; + */api/hosts/test-host/probe) + printf '%s\n' '{"ok":true}' + ;; + */api/packs/install) + printf '%s\n' '{"pack_id":1}' + ;; + */api/providers/deepseek/preview-import) + printf '%s\n' '{"available":true}' + ;; + */api/providers/deepseek/import) + printf '%s\n' '{"batch_id":123,"batch_status":"partially_succeeded","access_status":"broken"}' + ;; + */api/import-batches/123) + printf '%s\n' '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}' + ;; + */api/providers/deepseek/access/preview) + printf '%s\n' '{"available":true}' + ;; + */api/providers/deepseek/access/status) + printf '%s\n' '{"latest_access_status":"subscription_ready"}' + ;; + */api/providers/deepseek/status) + printf '%s\n' '{"status":"ready"}' + ;; + */api/providers/deepseek/reconcile) + printf '%s\n' '{"status":"in_sync"}' + ;; + */api/import-batches/123/rollback) + printf '%s\n' '{"status":"rolled_back"}' + ;; + *) + echo "unexpected curl url: $url" >&2 + exit 1 + ;; +esac +EOF + chmod +x "$fakebin/curl" + + PATH="$fakebin:$PATH" \ + ARTIFACT_DIR="$artifact_dir" \ + CRM_BASE_URL="http://crm.example.com" \ + CRM_ADMIN_TOKEN="token" \ + HOST_NAME="test-host" \ + HOST_BASE_URL="http://host.example.com" \ + PACK_PATH="/tmp/openai-pack" \ + PROVIDER_ID="deepseek" \ + HOST_API_KEY="host-key" \ + MODE="partial" \ + ACCESS_MODE="subscription" \ + ACCESS_API_KEY="user-key" \ + SUBSCRIPTION_USERS="42" \ + SKIP_ROLLBACK="1" \ + AFTER_IMPORT_HOOK_COMMAND='printf "%s\n" "$BATCH_ID:$BATCH_DETAIL_FILE:$ACCESS_MODE" > "$ARTIFACT_DIR/hook.txt"' \ + "$ROOT_DIR/scripts/real_host_acceptance.sh" >/dev/null + + [[ -f "$hook_file" ]] || fail "after-import hook did not create $hook_file" + local hook_contents + hook_contents="$(cat "$hook_file")" + assert_contains "$hook_contents" "123:" + assert_contains "$hook_contents" "05a-batch-detail-pre-access.json:subscription" +} + +run_test_import_remote43_provider_subscription_prep() { + local tmpdir fakebin artifact_dir ssh_log psql_sql + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' RETURN + fakebin="$tmpdir/bin" + artifact_dir="$tmpdir/artifacts" + ssh_log="$artifact_dir/ssh-log.txt" + psql_sql="$artifact_dir/prep.sql" + mkdir -p "$fakebin" + + cat > "$fakebin/ssh" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +log_dir="${FAKE_REMOTE_LOG_DIR:?missing FAKE_REMOTE_LOG_DIR}" +cmd="${*: -1}" +printf '%s\n' "$cmd" >> "$log_dir/ssh-log.txt" +case "$cmd" in + *"grep ^SUB2API_CRM_ADMIN_TOKEN="*) + printf '%s\n' 'crm-token' + ;; + *"select value from settings where key='admin_api_key'"*) + printf '%s\n' 'admin-key' + ;; + *"select id from users where role='admin'"*) + printf '%s\n' '1' + ;; + *"select id from users where email like 'relay-sub-%@sub2api.local'"*) + printf '%s\n' '42' + ;; + *"select k.key from users u join api_keys k on k.user_id=u.id"*) + printf '%s\n' 'user-key' + ;; + *"curl -sS -D /tmp/import_headers.txt"*"/api/providers/deepseek/import"*) + printf '%s\n' '{"batch_id":123,"batch_status":"partially_succeeded","access_status":"broken"}' > /tmp/import_body.json + printf '%s\n' 'HTTP/1.1 200 OK' > /tmp/import_headers.txt + ;; + "cat /tmp/import_headers.txt") + cat /tmp/import_headers.txt + ;; + "cat /tmp/import_body.json") + cat /tmp/import_body.json + ;; + *"curl -sS -H 'Authorization: Bearer *** http://127.0.0.1:18088/api/import-batches/123"*) + printf '%s\n' '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}' + ;; + *"curl -sS -D /tmp/chat_headers.txt"*) + printf '%s\n' 'HTTP/1.1 200 OK' > /tmp/chat_headers.txt + printf '%s\n' '{"choices":[{"message":{"content":"pong"}}]}' > /tmp/chat_body.json + ;; + "cat /tmp/chat_headers.txt") + cat /tmp/chat_headers.txt + ;; + "cat /tmp/chat_body.json") + cat /tmp/chat_body.json + ;; + *"curl -sS -H 'Authorization: Bearer *** http://127.0.0.1:18088/api/providers/deepseek/status"*) + printf '%s\n' '{"status":"ready"}' + ;; + *"curl -sS -H 'Authorization: Bearer *** http://127.0.0.1:18088/api/providers/deepseek/access/status"*) + printf '%s\n' '{"latest_access_status":"subscription_ready"}' + ;; + *"curl -sS -X POST -H 'Authorization: Bearer *** -H 'Content-Type: application/json' http://127.0.0.1:18088/api/providers/deepseek/access/preview"*) + printf '%s\n' '{"available":true}' + ;; + *"curl -sS -H 'Authorization: Bearer *** http://127.0.0.1:18088/api/providers/deepseek/reconcile"*) + printf '%s\n' '{"status":"in_sync"}' + ;; + *"sudo -n docker exec -i sub2api-relaymgr-pg 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 sub2api-relaymgr-pg psql -U sub2api -d sub2api"*) + CMD="$cmd" LOG_DIR="$log_dir" python3 - <<'PY' +import base64, os, re, pathlib +cmd = os.environ['CMD'] +log_dir = pathlib.Path(os.environ['LOG_DIR']) +match = re.search(r"printf '%s' '([^']+)' \| base64 -d", cmd) +if not match: + raise SystemExit(f'failed to extract base64 payload from: {cmd}') +log_dir.joinpath('prep.sql').write_bytes(base64.b64decode(match.group(1))) +PY + ;; + *"sudo -n docker exec sub2api-relaymgr-redis redis-cli FLUSHDB"*) + printf '%s\n' 'OK' + ;; + *) + echo "unexpected ssh command: $cmd" >&2 + exit 1 + ;; +esac +EOF + chmod +x "$fakebin/ssh" + + PATH="$fakebin:$PATH" \ + FAKE_REMOTE_LOG_DIR="$artifact_dir" \ + KEY="/does/not/matter" \ + REMOTE="fake@host" \ + CRM_BASE="http://127.0.0.1:18088" \ + HOST_BASE="http://127.0.0.1:18087" \ + ROOT="$artifact_dir/root" \ + ART="$artifact_dir/run" \ + PACK_PATH="/tmp/openai-pack" \ + UPSTREAM_KEY="upstream-test-key" \ + SUBSCRIPTION_DAYS=30 \ + MIN_BALANCE=10 \ + SKIP_ROLLBACK=1 \ + bash "$ROOT_DIR/scripts/import_remote43_provider.sh" deepseek gpt-4 UPSTREAM_KEY >/dev/null + + [[ -f "$psql_sql" ]] || fail "prep sql was not captured" + local prep_sql + prep_sql="$(cat "$psql_sql")" + assert_contains "$prep_sql" "UPDATE users" + assert_contains "$prep_sql" "UPDATE api_keys" + assert_contains "$prep_sql" "INSERT INTO user_subscriptions" + assert_contains "$prep_sql" "group_id = 7" + [[ -s "$ssh_log" ]] || fail "ssh log was empty" +} + +run_test_build_subscription_access_prep_sql +run_test_real_host_acceptance_after_import_hook +run_test_import_remote43_provider_subscription_prep + +echo "PASS: real host script regression checks"