oh-my-muse/muse-cloud/scripts/run-p1r-verification.sh

255 lines
8.8 KiB
Bash

#!/usr/bin/env bash
# P1R 分层验收入口。
#
# WHY: 默认 Maven CI 只能证明纯本地层绿,不能证明真实 PostgreSQL、New-API、RAGFlow 或 Dify 链路。
# 本脚本把三类证据显式分层,避免把台账门禁、默认跳过的 live IT 或 mock 绿混成"真验证"。
set -euo pipefail
MODE="${1:-}"
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd)"
CLOUD_DIR="${REPO_ROOT}/muse-cloud"
LOCAL_TESTS="P1rApiCoverageReportTest,P1r*RealApiGateTest,P1rMarketDetachedAssemblyGateTest,AgentsInfraIntegrityTest,ContractFirstGateTest,BcBoundaryArchTest,AiGrantRuntimeBoundaryArchTest"
REAL_PG_TESTS="${P1R_REAL_PG_TESTS:-P1r*IT,!P1rAiRuntimeEndToEndLiveAcceptanceIT,!P1rKnowledgeRuntimeEndToEndLiveAcceptanceIT,!P1rMarketKbForkMaterializationIT,!P1rImportLlmNewApiLiveAcceptanceIT,!P1rNewApiLiveAcceptanceIT,!P1rRagFlowLiveAcceptanceIT,!P1rDifyChatLiveAcceptanceIT,!P1rDifyDatasetsContractLiveIT}"
EXTERNAL_LIVE_TESTS="${P1R_EXTERNAL_LIVE_TESTS:-P1rAiRuntimeEndToEndLiveAcceptanceIT,P1rKnowledgeRuntimeEndToEndLiveAcceptanceIT,P1rMarketKbForkMaterializationIT}"
MODULE_LIVE_TESTS="${P1R_MODULE_LIVE_TESTS:-P1rNewApiLiveAcceptanceIT,P1rImportLlmNewApiLiveAcceptanceIT,P1rRagFlowLiveAcceptanceIT,P1rDifyChatLiveAcceptanceIT,P1rDifyDatasetsContractLiveIT}"
usage() {
cat <<'EOF'
用法:
bash muse-cloud/scripts/run-p1r-verification.sh local
bash muse-cloud/scripts/run-p1r-verification.sh real-pg
bash muse-cloud/scripts/run-p1r-verification.sh external-live
bash muse-cloud/scripts/run-p1r-verification.sh all
分层:
local 纯本地门禁: coverage/RealApiGate/ArchUnit/契约结构。无 DB、无外部服务。
real-pg 非 live P1r*IT: 真实 PostgreSQL _test 库,会 flyway.clean()。
external-live live P1r*IT + module live IT: 真实 PG + New-API/RAGFlow/Dify,必须显式 opt-in。
all 依次运行 local、real-pg、external-live。
real-pg / external-live 必需环境变量:
P1R_FLYWAY_URL 或 p1r.flyway.url JDBC URL,库名必须以 _test 结尾,不得携带 password/token/secret query。
P1R_FLYWAY_USER 或 p1r.flyway.user PostgreSQL 用户。
P1R_FLYWAY_PASSWORD 或 MUSE_POSTGRES_PASSWORD PostgreSQL 密码,只从环境变量读取。
external-live 额外要求:
MUSE_P1R_EXTERNAL_ACCEPTANCE=true
已加载 muse-cloud/scripts/dev/p1r-external-acceptance.env 或等价 New-API/RAGFlow/Dify 环境变量。
示例:
set -a && . "$HOME/.config/muse-repo/infra.env" && set +a
P1R_FLYWAY_URL='jdbc:postgresql://100.64.0.8:5433/<专属_test库>' \
P1R_FLYWAY_USER=root \
bash muse-cloud/scripts/run-p1r-verification.sh real-pg
EOF
}
mask_jdbc_url() {
local url="$1"
local no_query="${url%%\?*}"
local suffix=""
if [[ "${url}" == *"?"* ]]; then
suffix="?<query-redacted>"
fi
local prefix="${no_query%/*}/"
local db="${no_query##*/}"
prefix="$(printf '%s' "${prefix}" | sed -E 's#//([^:/?#]+)#//<host>#')"
printf '%s%s%s' "${prefix}" "${db}" "${suffix}"
}
jdbc_database_name() {
local url_no_query="${1%%\?*}"
printf '%s' "${url_no_query##*/}"
}
contains_credential_query() {
local url="$1"
[[ "${url}" == *"?"* ]] || return 1
local query="${url#*\?}"
local pair key normalized
IFS='&' read -r -a pairs <<< "${query}"
for pair in "${pairs[@]}"; do
key="${pair%%=*}"
normalized="$(printf '%s' "${key}" | tr '[:upper:]-' '[:lower:]_')"
case "${normalized}" in
password|pwd|token|secret|api_key|apikey|access_key|access_token|*_password|*_token|*_secret)
return 0
;;
esac
done
return 1
}
require_real_pg_env() {
P1R_FLYWAY_URL="${P1R_FLYWAY_URL:-$(printenv 'p1r.flyway.url' 2>/dev/null || true)}"
P1R_FLYWAY_USER="${P1R_FLYWAY_USER:-$(printenv 'p1r.flyway.user' 2>/dev/null || true)}"
if [[ -z "${P1R_FLYWAY_URL}" ]]; then
echo "ERROR: 缺少 P1R_FLYWAY_URL。必须显式指向 _test 隔离库。" >&2
exit 2
fi
if [[ -z "${P1R_FLYWAY_USER}" ]]; then
echo "ERROR: 缺少 P1R_FLYWAY_USER。" >&2
exit 2
fi
if [[ -z "${P1R_FLYWAY_PASSWORD:-${MUSE_POSTGRES_PASSWORD:-}}" ]]; then
echo "ERROR: 缺少 P1R_FLYWAY_PASSWORD 或 MUSE_POSTGRES_PASSWORD。" >&2
exit 2
fi
if contains_credential_query "${P1R_FLYWAY_URL}"; then
echo "ERROR: P1R_FLYWAY_URL 不得携带 password/token/secret/api-key query: $(mask_jdbc_url "${P1R_FLYWAY_URL}")" >&2
exit 2
fi
local database
database="$(jdbc_database_name "${P1R_FLYWAY_URL}")"
if [[ "${database}" != *_test ]]; then
echo "ERROR: P1R_FLYWAY_URL 必须指向 _test 后缀隔离库,实际: $(mask_jdbc_url "${P1R_FLYWAY_URL}")" >&2
exit 2
fi
export P1R_FLYWAY_URL P1R_FLYWAY_USER
}
require_database_exists() {
echo "== DB preflight: $(mask_jdbc_url "${P1R_FLYWAY_URL}")"
python3 - <<'PY'
import os
import sys
from urllib.parse import urlparse
try:
import psycopg2
except Exception:
print("ERROR: 缺少 python3 psycopg2,无法预检 _test 库是否存在。", file=sys.stderr)
sys.exit(2)
url = os.environ["P1R_FLYWAY_URL"]
user = os.environ["P1R_FLYWAY_USER"]
password = os.environ.get("P1R_FLYWAY_PASSWORD") or os.environ.get("MUSE_POSTGRES_PASSWORD")
raw = url[len("jdbc:"):] if url.startswith("jdbc:") else url
parsed = urlparse(raw)
database = (parsed.path or "").lstrip("/")
maintenance_database = os.environ.get("P1R_FLYWAY_MAINTENANCE_DB", "postgres")
def redact(message):
text = str(message)
if password:
text = text.replace(password, "<password-redacted>")
if user:
text = text.replace(user, "<user-redacted>")
return text
if not parsed.hostname or not database:
print("ERROR: P1R_FLYWAY_URL 无法解析 host 或 database。", file=sys.stderr)
sys.exit(2)
try:
connection = psycopg2.connect(
host=parsed.hostname,
port=parsed.port or 5432,
dbname=maintenance_database,
user=user,
password=password,
connect_timeout=5,
)
try:
with connection.cursor() as cursor:
cursor.execute("select 1 from pg_database where datname = %s", (database,))
exists = cursor.fetchone() is not None
finally:
connection.close()
except Exception as exc:
print("ERROR: 无法连接 PostgreSQL 预检 _test 库存在性: " + redact(exc), file=sys.stderr)
sys.exit(2)
if not exists:
print(
"ERROR: 目标 _test 库不存在: "
+ database
+ "。请经人类 DDL 闸门预建后重跑;脚本不会自动 CREATE DATABASE。",
file=sys.stderr,
)
sys.exit(2)
PY
}
base_arg_line() {
printf '%s' "-DsocksProxyHost= -DsocksProxyPort= -Dp1r.flyway.url=${P1R_FLYWAY_URL} -Dp1r.flyway.user=${P1R_FLYWAY_USER} -Dp1r.flyway.locations=filesystem:sql/muse"
}
live_arg_line() {
printf '%s' "$(base_arg_line) -Dhttp.proxyHost= -Dhttp.proxyPort= -Dhttps.proxyHost= -Dhttps.proxyPort= -Djava.net.useSystemProxies=false"
}
run_local() {
echo "== P1R local gates: ${LOCAL_TESTS}"
(cd "${CLOUD_DIR}" && MUSE_P1R_EXTERNAL_ACCEPTANCE=false mvn -B -pl muse-server -am test \
-Dtest="${LOCAL_TESTS}" \
-DfailIfNoSpecifiedTests=false \
-Dsurefire.failIfNoSpecifiedTests=false)
}
run_real_pg() {
require_real_pg_env
require_database_exists
echo "== P1R real-PG IT: ${REAL_PG_TESTS}"
echo "== JDBC: $(mask_jdbc_url "${P1R_FLYWAY_URL}") user=<redacted>"
(cd "${CLOUD_DIR}" && mvn -B -pl muse-server -am test \
-Dtest="${REAL_PG_TESTS}" \
-DreuseForks=false \
-DfailIfNoSpecifiedTests=false \
-Dsurefire.failIfNoSpecifiedTests=false \
-DargLine="$(base_arg_line)")
}
run_external_live() {
require_real_pg_env
require_database_exists
if [[ "${MUSE_P1R_EXTERNAL_ACCEPTANCE:-}" != "true" ]]; then
echo "ERROR: external-live 必须显式设置 MUSE_P1R_EXTERNAL_ACCEPTANCE=true,否则 live IT 会 assumeTrue 跳过。" >&2
exit 2
fi
echo "== P1R external-live server IT: ${EXTERNAL_LIVE_TESTS}"
echo "== JDBC: $(mask_jdbc_url "${P1R_FLYWAY_URL}") user=<redacted>"
(cd "${CLOUD_DIR}" && mvn -B -pl muse-server -am test \
-Dtest="${EXTERNAL_LIVE_TESTS}" \
-DreuseForks=false \
-DfailIfNoSpecifiedTests=false \
-Dsurefire.failIfNoSpecifiedTests=false \
-DargLine="$(live_arg_line)")
echo "== P1R external-live module IT: ${MODULE_LIVE_TESTS}"
(cd "${CLOUD_DIR}" && mvn -B -pl muse-module-ai/muse-module-ai-server,muse-module-knowledge/muse-module-knowledge-server -am test \
-Dtest="${MODULE_LIVE_TESTS}" \
-DfailIfNoSpecifiedTests=false \
-Dsurefire.failIfNoSpecifiedTests=false \
-DargLine="$(live_arg_line)")
}
case "${MODE}" in
local)
run_local
;;
real-pg)
run_real_pg
;;
external-live)
run_external_live
;;
all)
run_local
run_real_pg
run_external_live
;;
-h|--help|help|"")
usage
[[ -n "${MODE}" ]] || exit 2
;;
*)
echo "ERROR: 未知模式: ${MODE}" >&2
usage >&2
exit 2
;;
esac