Add a dedicated acceptance script for providers.html, cover it in the local real-host script regression suite, and document the current frontend review baseline, closure audit, providers action matrix, and remediation task board. This keeps the frontend acceptance boundary explicit: providers.html now has a repeatable verification entry point for its page-level actions, while non-UI provider operations remain documented as backend-only capabilities.
379 lines
14 KiB
Bash
Executable File
379 lines
14 KiB
Bash
Executable File
#!/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"
|