Files
sub2api-cn-relay-manager/scripts/test_real_host_scripts.sh
2026-05-23 15:03:59 +08:00

542 lines
18 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
}
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"
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'"
local auth_cache_key balance_cache_key subscription_cache_key
auth_cache_key="$(build_api_key_auth_cache_key 'user-key')"
balance_cache_key="$(build_user_balance_cache_key 42)"
subscription_cache_key="$(build_subscription_billing_cache_key 42 7)"
assert_contains "$auth_cache_key" "apikey:auth:"
assert_contains "$balance_cache_key" "billing:balance:42"
assert_contains "$subscription_cache_key" "billing:sub:42:7"
}
run_test_real_host_acceptance_after_import_hook() {
local tmpdir fakebin artifact_dir hook_file guide_file stdout_file
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' RETURN
fakebin="$tmpdir/bin"
artifact_dir="$tmpdir/artifacts"
hook_file="$artifact_dir/hook.txt"
guide_file="$artifact_dir/00-artifact-guide.txt"
stdout_file="$tmpdir/real_host_acceptance.stdout.txt"
mkdir -p "$fakebin"
cat > "$fakebin/curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
url=""
for arg in "$@"; do
if [[ "$arg" == *'***'* ]]; then
echo "unexpected redacted auth placeholder in curl args: $*" >&2
exit 1
fi
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)
printf '%s\n' '{"host_id":"test-host"}'
;;
*/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" \
REMOTE_PG_CONTAINER="fresh-pg" \
REMOTE_REDIS_CONTAINER="fresh-redis" \
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" >"$stdout_file"
[[ -f "$hook_file" ]] || fail "after-import hook did not create $hook_file"
[[ -f "$guide_file" ]] || fail "artifact guide was not created"
local hook_contents
hook_contents="$(cat "$hook_file")"
assert_contains "$hook_contents" "123:"
assert_contains "$hook_contents" "05a-batch-detail-pre-access.json:subscription"
local guide_contents stdout_contents
guide_contents="$(cat "$guide_file")"
stdout_contents="$(cat "$stdout_file")"
assert_contains "$guide_contents" "清单 4必须分层留证据不可混用"
assert_contains "$guide_contents" "/api/v1/admin/accounts/:id/models 正确 ≠ /v1/models 正确"
assert_contains "$guide_contents" "/v1/models 正确 ≠ /v1/chat/completions 正确"
assert_contains "$stdout_contents" "artifact guide: $artifact_dir/00-artifact-guide.txt"
assert_contains "$stdout_contents" "checklist layered evidence: see 05b-after-import-hook.stdout.txt / 05b-after-import-hook.stderr.txt"
}
run_test_check_deepseek_completion_split() {
local tmpdir fakebin artifact_dir summary_file stdout_file
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' RETURN
fakebin="$tmpdir/bin"
artifact_dir="$tmpdir/artifacts"
summary_file="$artifact_dir/summary.json"
stdout_file="$tmpdir/check_deepseek_completion_split.stdout.txt"
mkdir -p "$fakebin" "$artifact_dir"
cat > "$fakebin/curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
headers_file=""
body_file=""
url=""
prev=""
for arg in "$@"; do
case "$prev" in
-D)
headers_file="$arg"
prev=""
continue
;;
-o)
body_file="$arg"
prev=""
continue
;;
esac
case "$arg" in
-D|-o)
prev="$arg"
continue
;;
http://*|https://*)
url="$arg"
;;
esac
done
[[ -n "$headers_file" && -n "$body_file" && -n "$url" ]] || {
echo "missing curl capture args: $*" >&2
exit 1
}
case "$url" in
http://host.example.com/v1/models)
printf '%s
Content-Type: application/json
' 'HTTP/1.1 200 OK' > "$headers_file"
printf '%s
' '{"data":[{"id":"deepseek-v4-flash"},{"id":"deepseek-v4-pro"}]}' > "$body_file"
;;
http://host.example.com/v1/chat/completions)
printf '%s
Content-Type: application/json
' 'HTTP/1.1 502 Bad Gateway' > "$headers_file"
printf '%s
' '{"error":{"message":"Upstream service temporarily unavailable","type":"upstream_error"}}' > "$body_file"
;;
https://upstream.example.com/v1/chat/completions)
printf '%s
Content-Type: text/event-stream
' 'HTTP/1.1 200 OK' > "$headers_file"
printf '%s
' 'data: {"choices":[{"delta":{"content":"pong"}}]}' > "$body_file"
;;
*)
echo "unexpected curl url: $url" >&2
exit 1
;;
esac
EOF
chmod +x "$fakebin/curl"
PATH="$fakebin:$PATH" ARTIFACT_DIR="$artifact_dir" HOST_BASE="http://host.example.com" HOST_MANAGED_KEY="managed-key" UPSTREAM_BASE="https://upstream.example.com/v1" UPSTREAM_API_KEY="upstream-key" MODEL="deepseek-v4-flash" bash "$ROOT_DIR/scripts/check_deepseek_completion_split.sh" >"$stdout_file"
[[ -f "$summary_file" ]] || fail "missing summary file: $summary_file"
local summary stdout_contents
summary="$(cat "$summary_file")"
stdout_contents="$(cat "$stdout_file")"
assert_contains "$summary" '"classification": "host_compatibility_gap"'
assert_contains "$summary" '"host_models_status": 200'
assert_contains "$summary" '"host_chat_status": 502'
assert_contains "$summary" '"upstream_chat_status": 200'
assert_contains "$summary" '"upstream_chat_content_type": "text/event-stream"'
assert_contains "$stdout_contents" '"classification": "host_compatibility_gap"'
}
run_test_import_remote43_provider_subscription_prep() {
local tmpdir fakebin artifact_dir ssh_log psql_sql pack_dir
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"
pack_dir="$tmpdir/pack"
mkdir -p "$fakebin"
mkdir -p "$pack_dir/providers"
printf '%s\n' '{"provider_id":"deepseek","base_url":"https://upstream.example.com/v1"}' > "$pack_dir/providers/deepseek.json"
cat > "$fakebin/curl" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
headers_file=""
body_file=""
url=""
prev=""
for arg in "$@"; do
if [[ "$arg" == *'***'* ]]; then
echo "unexpected redacted auth placeholder in curl args: $*" >&2
exit 1
fi
case "$prev" in
-D)
headers_file="$arg"
prev=""
continue
;;
-o)
body_file="$arg"
prev=""
continue
;;
esac
case "$arg" in
-D|-o)
prev="$arg"
continue
;;
http://*|https://*)
url="$arg"
;;
esac
done
write_headers() {
[[ -n "$headers_file" ]] && printf '%s\n' 'HTTP/1.1 200 OK' > "$headers_file"
}
write_body() {
local body="$1"
if [[ -n "$body_file" ]]; then
printf '%s\n' "$body" > "$body_file"
else
printf '%s\n' "$body"
fi
}
case "$url" in
*/api/hosts)
write_body '{"host_id":"remote43-current-host"}'
;;
*/api/providers/deepseek/import)
write_headers
write_body '{"batch_id":123,"batch_status":"partially_succeeded","access_status":"broken","provider_status":"ready","accepted_keys_count":1,"group":{"id":"7","name":"DeepSeek 默认分组"}}'
;;
*/api/import-batches/123)
write_body '{"managed_resources":[{"ResourceType":"group","HostResourceID":"7","ResourceName":"DeepSeek 默认分组"}]}'
;;
*/api/providers/deepseek/status*)
write_body '{"status":"ready"}'
;;
*/api/providers/deepseek/access/status*)
write_body '{"latest_access_status":"subscription_ready"}'
;;
*/api/providers/deepseek/access/preview*)
write_body '{"available":true}'
;;
*)
echo "unexpected curl url: $url" >&2
exit 1
;;
esac
EOF
chmod +x "$fakebin/curl"
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"
if [[ "$cmd" == *'***'* ]]; then
echo "unexpected redacted auth placeholder in ssh command: $cmd" >&2
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'
;;
*"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'
;;
*"/api/providers/deepseek/import"*)
printf '%s\n' '{"batch_id":123,"batch_status":"partially_succeeded","access_status":"broken","group":{"id":"7","name":"DeepSeek 默认分组"}}' > /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
;;
*"/api/import-batches/123"*)
printf '%s\n' '{"managed_resources":[{"ResourceType":"account","HostResourceID":"8","ResourceName":"deepseek-01"}]}'
;;
*"curl -sS -D /tmp/models_headers.txt"*)
printf '%s\n' 'HTTP/1.1 200 OK' > /tmp/models_headers.txt
printf '%s\n' '{"data":[{"id":"gpt-4"},{"id":"gpt-4.1"}]}' > /tmp/models_body.json
;;
"cat /tmp/models_headers.txt")
cat /tmp/models_headers.txt
;;
"cat /tmp/models_body.json")
cat /tmp/models_body.json
;;
*"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 -D /tmp/upstream_models_headers.txt"*)
printf '%s\n' 'HTTP/1.1 200 OK' > /tmp/upstream_models_headers.txt
printf '%s\n' '{"data":[{"id":"openai/gpt-4"},{"id":"openai/gpt-4.1"}]}' > /tmp/upstream_models_body.json
;;
"cat /tmp/upstream_models_headers.txt")
cat /tmp/upstream_models_headers.txt
;;
"cat /tmp/upstream_models_body.json")
cat /tmp/upstream_models_body.json
;;
*"curl -sS -D /tmp/upstream_chat_headers.txt"*)
printf '%s\n' 'HTTP/1.1 200 OK' > /tmp/upstream_chat_headers.txt
printf '%s\n' '{"choices":[{"message":{"content":"upstream-pong"}}]}' > /tmp/upstream_chat_body.txt
;;
"cat /tmp/upstream_chat_headers.txt")
cat /tmp/upstream_chat_headers.txt
;;
"cat /tmp/upstream_chat_body.txt")
cat /tmp/upstream_chat_body.txt
;;
*"/api/providers/deepseek/status"*)
printf '%s\n' '{"status":"ready"}'
;;
*"/api/providers/deepseek/access/status"*)
printf '%s\n' '{"latest_access_status":"subscription_ready"}'
;;
*"/api/providers/deepseek/access/preview"*)
printf '%s\n' '{"available":true}'
;;
*"/api/providers/deepseek/reconcile"*)
printf '%s\n' '{"status":"in_sync"}'
;;
*"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 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']
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}')
sql = base64.b64decode(match.group(1)).decode()
if "select id from users where email like 'relay-sub-%@sub2api.local' and not exists" in sql:
print('')
elif "select k.key from users u join api_keys k on k.user_id=u.id" in sql and "not exists" in sql:
print('')
elif "UPDATE users" in sql and "INSERT INTO user_subscriptions" in sql:
log_dir.joinpath('prep.sql').write_text(sql, encoding='utf-8')
print('')
elif "INSERT INTO users" in sql and "INSERT INTO api_keys" in sql:
log_dir.joinpath('create-user.sql').write_text(sql, encoding='utf-8')
print('84\tuser-key-fresh')
elif "SELECT json_build_object(" in sql:
log_dir.joinpath('group-state.sql').write_text(sql, encoding='utf-8')
print('{"group_id":7,"subscription":{"status":"active"},"key":{"group_id":7}}')
else:
print('')
PY
;;
*"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'
;;
*)
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" \
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" \
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"
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"
assert_contains "$invalidation_log" "subscription_cache_key=billing:sub:84:7"
local subscription_state models_body chat_body upstream_models upstream_chat summary_json
subscription_state="$(cat "$artifact_dir/run/08-subscription-group-state.json")"
assert_contains "$subscription_state" '"group_id":7'
assert_contains "$subscription_state" '"status":"active"'
models_body="$(cat "$artifact_dir/run/10-models.body.json")"
chat_body="$(cat "$artifact_dir/run/12-chat.body.json")"
upstream_models="$(cat "$artifact_dir/run/18-upstream-models.body.json")"
upstream_chat="$(cat "$artifact_dir/run/20-upstream-chat.body.txt")"
summary_json="$(cat "$artifact_dir/run/21-summary.json" 2>/dev/null || true)"
assert_contains "$models_body" '"id":"gpt-4"'
assert_contains "$chat_body" '"content":"pong"'
assert_contains "$upstream_models" '"id":"openai/gpt-4"'
assert_contains "$upstream_chat" '"content":"upstream-pong"'
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
run_test_real_host_acceptance_after_import_hook
run_test_check_deepseek_completion_split
run_test_import_remote43_provider_subscription_prep
echo "PASS: real host script regression checks"