#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" # shellcheck disable=SC1091 source "$ROOT_DIR/scripts/acceptance/route_acceptance_lib.sh" PROVIDER_ADMIN_ROOT="${PROVIDER_ADMIN_ROOT:-$ROOT_DIR/artifacts/provider-admin-matrix}" PROVIDER_ADMIN_PAGE_URL="${PROVIDER_ADMIN_PAGE_URL:-https://sub.tksea.top/portal/admin/providers.html}" TS="${TS:-$(timestamp_token)}" ARTIFACT_DIR="${ARTIFACT_DIR:-$PROVIDER_ADMIN_ROOT/${TS}_provider_admin_actions}" PACK_ID="${PACK_ID:-}" HOST_ID="${HOST_ID:-}" PACK_PATH="${PACK_PATH:-}" PROVIDER_ID="${PROVIDER_ID:-}" MODE="${MODE:-partial}" ACCESS_MODE="${ACCESS_MODE:-self_service}" ACCESS_API_KEY="${ACCESS_API_KEY:-}" PROVIDER_KEYS="${PROVIDER_KEYS:-}" SUBSCRIPTION_USERS="${SUBSCRIPTION_USERS:-}" SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" VERIFY_PUBLISH="${VERIFY_PUBLISH:-0}" crud_provider_id="${CRUD_PROVIDER_ID:-provider-admin-draft-$TS}" crud_display_name="${CRUD_DISPLAY_NAME:-Provider Admin Draft $TS}" crud_platform="${CRUD_PLATFORM:-openai}" crud_base_url="${CRUD_BASE_URL:-https://draft-$TS.example.com/v1}" crud_smoke_model="${CRUD_SMOKE_MODEL:-provider-admin-draft-$TS}" crud_supported_models="${CRUD_SUPPORTED_MODELS:-provider-admin-draft-$TS,provider-admin-draft-mini-$TS}" crud_notes="${CRUD_NOTES:-provider-admin draft acceptance $TS}" crud_updated_display_name="${CRUD_UPDATED_DISPLAY_NAME:-Provider Admin Draft Updated $TS}" crud_updated_base_url="${CRUD_UPDATED_BASE_URL:-https://draft-updated-$TS.example.com/v1}" crud_updated_smoke_model="${CRUD_UPDATED_SMOKE_MODEL:-provider-admin-draft-updated-$TS}" crud_updated_supported_models="${CRUD_UPDATED_SUPPORTED_MODELS:-provider-admin-draft-updated-$TS}" crud_updated_notes="${CRUD_UPDATED_NOTES:-provider-admin draft acceptance updated $TS}" publish_provider_id="${PUBLISH_PROVIDER_ID:-provider-admin-publish-$TS}" publish_display_name="${PUBLISH_DISPLAY_NAME:-Provider Admin Publish $TS}" publish_platform="${PUBLISH_PLATFORM:-openai}" publish_base_url="${PUBLISH_BASE_URL:-https://publish-$TS.example.com/v1}" publish_smoke_model="${PUBLISH_SMOKE_MODEL:-provider-admin-publish-$TS}" publish_supported_models="${PUBLISH_SUPPORTED_MODELS:-provider-admin-publish-$TS}" publish_notes="${PUBLISH_NOTES:-provider-admin publish acceptance $TS}" publish_commit_message="${PUBLISH_COMMIT_MESSAGE:-feat(pack): publish provider admin draft $publish_provider_id}" require_var CRM_BASE require_var ACCESS_API_KEY require_var PROVIDER_KEYS crm_auth_init ensure_artifact_dir curl_status_to_file "$PROVIDER_ADMIN_PAGE_URL" "$ARTIFACT_DIR/00-provider-admin.html" json_field_from_file() { local file="$1" local path="$2" python3 - "$file" "$path" <<'PY' import json import sys file_path, path = sys.argv[1:3] value = json.load(open(file_path, "r", encoding="utf-8")) for part in path.split("."): if isinstance(value, dict): value = value.get(part) else: value = None break if value is None: raise SystemExit(2) if isinstance(value, (dict, list)): print(json.dumps(value, ensure_ascii=False)) else: print(value) PY } first_collection_field_from_file() { local file="$1" local collection="$2" local field="$3" python3 - "$file" "$collection" "$field" <<'PY' import json import sys file_path, collection, field = sys.argv[1:4] payload = json.load(open(file_path, "r", encoding="utf-8")) items = payload.get(collection) or [] if not items: raise SystemExit(2) value = items[0].get(field) if value in ("", None): raise SystemExit(2) print(value) PY } normalize_csv_json() { local raw="$1" python3 - "$raw" <<'PY' import json import sys raw = sys.argv[1] values = [] for line in raw.replace("\r", "\n").split("\n"): for item in line.split(","): item = item.strip() if item: values.append(item) print(json.dumps(values, ensure_ascii=False)) PY } build_preview_payload() { local host_id="$1" local pack_path="$2" local provider_id="$3" local mode="$4" local keys_json="$5" python3 - "$host_id" "$pack_path" "$provider_id" "$mode" "$keys_json" <<'PY' import json import sys host_id, pack_path, provider_id, mode, keys_json = sys.argv[1:6] print(json.dumps({ "host_id": host_id, "pack_path": pack_path, "provider_id": provider_id, "keys": json.loads(keys_json), "mode": mode, }, ensure_ascii=False)) PY } build_import_payload() { local host_id="$1" local pack_path="$2" local provider_id="$3" local mode="$4" local access_mode="$5" local access_api_key="$6" local keys_json="$7" local subscription_users_json="$8" local subscription_days="$9" python3 - "$host_id" "$pack_path" "$provider_id" "$mode" "$access_mode" "$access_api_key" "$keys_json" "$subscription_users_json" "$subscription_days" <<'PY' import json import sys host_id, pack_path, provider_id, mode, access_mode, access_api_key, keys_json, subscription_users_json, subscription_days = sys.argv[1:10] payload = { "host_id": host_id, "pack_path": pack_path, "provider_id": provider_id, "keys": json.loads(keys_json), "mode": mode, "access_mode": access_mode, "access_api_key": access_api_key, } if access_mode == "subscription": payload["subscription_users"] = json.loads(subscription_users_json) payload["subscription_days"] = int(subscription_days) print(json.dumps(payload, ensure_ascii=False)) PY } build_draft_payload() { local pack_id="$1" local provider_id="$2" local display_name="$3" local platform="$4" local base_url="$5" local smoke_model="$6" local supported_models_json="$7" local source_host_id="$8" local notes="$9" python3 - "$pack_id" "$provider_id" "$display_name" "$platform" "$base_url" "$smoke_model" "$supported_models_json" "$source_host_id" "$notes" <<'PY' import json import sys pack_id, provider_id, display_name, platform, base_url, smoke_model, supported_models_json, source_host_id, notes = sys.argv[1:10] supported_models = json.loads(supported_models_json) manifest = { "provider_id": provider_id, "display_name": display_name, "platform": platform, "base_url": base_url, "smoke_test_model": smoke_model, "supported_models": supported_models, } print(json.dumps({ "pack_id": pack_id, "provider_id": provider_id, "display_name": display_name, "platform": platform, "base_url": base_url, "smoke_test_model": smoke_model, "supported_models": supported_models, "source_host_id": source_host_id, "notes": notes, "manifest": manifest, }, ensure_ascii=False)) PY } crm_curl_status() { local method="$1" local path="$2" local payload="${3:-}" local -a curl_args curl_args=(-fsS -o /dev/null -w '%{http_code}' -X "$method") if [[ -n "${crm_token:-}" ]]; then curl_args+=(-H "Authorization: Bearer $crm_token") elif [[ -n "${crm_cookie_jar:-}" ]]; then curl_args+=(-b "$crm_cookie_jar" -c "$crm_cookie_jar") else echo "missing CRM auth: set CRM_ADMIN_TOKEN or CRM_ADMIN_USERNAME/CRM_ADMIN_PASSWORD" >&2 exit 2 fi if [[ -n "$payload" ]]; then curl_args+=(-H 'Content-Type: application/json' "${CRM_BASE%/}${path}" -d "$payload") else curl_args+=("${CRM_BASE%/}${path}") fi curl "${curl_args[@]}" } provider_keys_json="$(normalize_csv_json "$PROVIDER_KEYS")" subscription_users_json="$(normalize_csv_json "$SUBSCRIPTION_USERS")" crud_supported_models_json="$(normalize_csv_json "$crud_supported_models")" crud_updated_supported_models_json="$(normalize_csv_json "$crud_updated_supported_models")" publish_supported_models_json="$(normalize_csv_json "$publish_supported_models")" save_json 01-packs "$(crm_curl_json GET "/api/packs")" if [[ -z "$PACK_ID" ]]; then PACK_ID="$(first_collection_field_from_file "$ARTIFACT_DIR/01-packs.json" packs pack_id)" fi save_json 02-hosts "$(crm_curl_json GET "/api/hosts")" if [[ -z "$HOST_ID" ]]; then HOST_ID="$(first_collection_field_from_file "$ARTIFACT_DIR/02-hosts.json" hosts host_id)" fi if [[ -z "$PACK_PATH" ]]; then PACK_PATH="/app/packs/$PACK_ID" fi save_json 03-pack-providers "$(crm_curl_json GET "/api/packs/$PACK_ID/providers")" if [[ -z "$PROVIDER_ID" ]]; then PROVIDER_ID="$(first_collection_field_from_file "$ARTIFACT_DIR/03-pack-providers.json" providers provider_id)" fi preview_payload="$(build_preview_payload "$HOST_ID" "$PACK_PATH" "$PROVIDER_ID" "$MODE" "$provider_keys_json")" save_json 04-preview-import "$(crm_curl_json POST "/api/providers/$PROVIDER_ID/preview-import" "$preview_payload")" import_payload="$(build_import_payload "$HOST_ID" "$PACK_PATH" "$PROVIDER_ID" "$MODE" "$ACCESS_MODE" "$ACCESS_API_KEY" "$provider_keys_json" "$subscription_users_json" "$SUBSCRIPTION_DAYS")" save_json 05-import "$(crm_curl_json POST "/api/providers/$PROVIDER_ID/import" "$import_payload")" crud_create_payload="$(build_draft_payload "$PACK_ID" "$crud_provider_id" "$crud_display_name" "$crud_platform" "$crud_base_url" "$crud_smoke_model" "$crud_supported_models_json" "$HOST_ID" "$crud_notes")" save_json 06-create-draft "$(crm_curl_json POST "/api/provider-drafts" "$crud_create_payload")" crud_draft_id="$(json_field_from_file "$ARTIFACT_DIR/06-create-draft.json" draft.draft_id)" save_json 07-list-drafts-before-delete "$(crm_curl_json GET "/api/provider-drafts?pack_id=$PACK_ID")" save_json 08-get-draft "$(crm_curl_json GET "/api/provider-drafts/$crud_draft_id")" crud_update_payload="$(build_draft_payload "$PACK_ID" "$crud_provider_id" "$crud_updated_display_name" "$crud_platform" "$crud_updated_base_url" "$crud_updated_smoke_model" "$crud_updated_supported_models_json" "$HOST_ID" "$crud_updated_notes")" save_json 09-update-draft "$(crm_curl_json PUT "/api/provider-drafts/$crud_draft_id" "$crud_update_payload")" delete_status="$(crm_curl_status DELETE "/api/provider-drafts/$crud_draft_id")" save_text 10-delete-draft.status "$delete_status" save_json 11-list-drafts-after-delete "$(crm_curl_json GET "/api/provider-drafts?pack_id=$PACK_ID")" publish_verified="false" if [[ "$VERIFY_PUBLISH" == "1" ]]; then publish_create_payload="$(build_draft_payload "$PACK_ID" "$publish_provider_id" "$publish_display_name" "$publish_platform" "$publish_base_url" "$publish_smoke_model" "$publish_supported_models_json" "$HOST_ID" "$publish_notes")" save_json 12-create-publish-draft "$(crm_curl_json POST "/api/provider-drafts" "$publish_create_payload")" publish_draft_id="$(json_field_from_file "$ARTIFACT_DIR/12-create-publish-draft.json" draft.draft_id)" publish_payload="$(python3 - "$publish_commit_message" <<'PY' import json import sys print(json.dumps({"commit_message": sys.argv[1]}, ensure_ascii=False)) PY )" save_json 13-publish-draft "$(crm_curl_json POST "/api/provider-drafts/$publish_draft_id/publish" "$publish_payload")" publish_verified="true" fi python3 - \ "$ARTIFACT_DIR" \ "$PACK_ID" \ "$HOST_ID" \ "$PACK_PATH" \ "$PROVIDER_ID" \ "$crud_draft_id" \ "$crud_updated_display_name" \ "$publish_verified" \ "$publish_provider_id" \ >"$ARTIFACT_DIR/99-summary.json" <<'PY' import json import sys from pathlib import Path ( artifact_dir, pack_id, host_id, pack_path, provider_id, crud_draft_id, crud_updated_display_name, publish_verified, publish_provider_id, ) = sys.argv[1:10] art = Path(artifact_dir) page = (art / "00-provider-admin.html").read_text(encoding="utf-8") packs = json.loads((art / "01-packs.json").read_text(encoding="utf-8")) hosts = json.loads((art / "02-hosts.json").read_text(encoding="utf-8")) providers = json.loads((art / "03-pack-providers.json").read_text(encoding="utf-8")) preview = json.loads((art / "04-preview-import.json").read_text(encoding="utf-8")) import_result = json.loads((art / "05-import.json").read_text(encoding="utf-8")) create_draft = json.loads((art / "06-create-draft.json").read_text(encoding="utf-8"))["draft"] list_before = json.loads((art / "07-list-drafts-before-delete.json").read_text(encoding="utf-8"))["provider_drafts"] get_draft = json.loads((art / "08-get-draft.json").read_text(encoding="utf-8"))["draft"] update_draft = json.loads((art / "09-update-draft.json").read_text(encoding="utf-8"))["draft"] delete_status = (art / "10-delete-draft.status").read_text(encoding="utf-8").strip() list_after = json.loads((art / "11-list-drafts-after-delete.json").read_text(encoding="utf-8"))["provider_drafts"] assert "Provider Admin" in page assert packs["packs"] assert hosts["hosts"] assert providers["providers"] assert pack_id assert host_id assert pack_path assert provider_id assert int(preview["accepted_keys_count"]) >= 1 assert int(import_result["batch_id"]) > 0 assert import_result["batch_status"] assert create_draft["draft_id"] == crud_draft_id assert any(item["draft_id"] == crud_draft_id for item in list_before) assert get_draft["draft_id"] == crud_draft_id assert update_draft["display_name"] == crud_updated_display_name assert delete_status == "204" assert not any(item["draft_id"] == crud_draft_id for item in list_after) summary = { "page_title_seen": "Provider Admin" in page, "pack_id": pack_id, "host_id": host_id, "pack_path": pack_path, "provider_id": provider_id, "preview_accepted_keys_count": int(preview["accepted_keys_count"]), "import_batch_id": int(import_result["batch_id"]), "import_batch_status": import_result["batch_status"], "crud_draft_id": crud_draft_id, "crud_updated_display_name": update_draft["display_name"], "crud_delete_status": delete_status, "publish_verified": publish_verified == "true", } if publish_verified == "true": create_publish = json.loads((art / "12-create-publish-draft.json").read_text(encoding="utf-8"))["draft"] publish_result = json.loads((art / "13-publish-draft.json").read_text(encoding="utf-8"))["publish"] assert create_publish["provider_id"] == publish_provider_id assert publish_result["provider_id"] == publish_provider_id assert publish_result["commit_sha"] summary["publish_draft_id"] = create_publish["draft_id"] summary["publish_provider_id"] = publish_result["provider_id"] summary["publish_commit_sha"] = publish_result["commit_sha"] else: summary["publish_skipped_reason"] = "VERIFY_PUBLISH=0" print(json.dumps(summary, ensure_ascii=False, indent=2)) PY cat "$ARTIFACT_DIR/99-summary.json"