完成 2.0.0 S4 的外部 AI、admin、account 与 solo SQL 收口,并推进 S5 studio 市场/handoff/旧编辑器入口裁剪。S5 的前端单测、构建与 route-only e2e 已留证;MSW-off 创作主线 e2e 仍因本地后端活体启动口径待修正,已在总账标为未完成验收。
255 lines
8.9 KiB
Bash
255 lines
8.9 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,SoloExternalAiProviderGateTest,SoloAccountDefaultsGateTest,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
|