完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)
汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。 统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。 纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。 关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。 预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。 完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
+193
-48
@@ -7,7 +7,7 @@ CLIENT_DIR="$ROOT_DIR/client"
|
||||
source "$ROOT_DIR/scripts/qa/pwa-cross-version-fixture.sh"
|
||||
|
||||
MODE="${1:-}"
|
||||
if [[ "$MODE" == "full-local" || "$MODE" == "full-local-diagnostic" ]]; then
|
||||
if [[ "$MODE" == "full-local" || "$MODE" == "full-local-diagnostic" || "$MODE" == "retained-local" || "$MODE" == "retained-server-local" ]]; then
|
||||
if [[ -z "${QA_ENVIRONMENT_CONTEXT:-}" ]]; then
|
||||
exec node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" "$MODE"
|
||||
fi
|
||||
@@ -43,6 +43,7 @@ QA_READY_TIMEOUT_SECONDS="${QA_READY_TIMEOUT_SECONDS:-90}"
|
||||
QA_BASE_URL=""
|
||||
QA_DB_NAME="${QA_DB_NAME:-${QA_DB_PREFIX}_$(date +%Y%m%d%H%M%S)_$RANDOM}"
|
||||
QA_DB_CREATED=0
|
||||
QA_FULL_STAGE_FILE=""
|
||||
QA_SERVER_STARTED=0
|
||||
QA_SERVER_OWNED=0
|
||||
DATABASE_URL="postgresql://${QA_DB_USER}:${QA_DB_PASSWORD}@${QA_DB_HOST}:${QA_DB_PORT}/${QA_DB_NAME}?schema=public"
|
||||
@@ -57,7 +58,7 @@ qa_require_cmd() {
|
||||
|
||||
qa_usage() {
|
||||
cat <<USAGE
|
||||
Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|auth-security-local|payment-concurrency-local|business-flow-local|project-wide-visual-local|qg6-visual-matrix-local|r70-assist-local|r106-visual-local|r108-mobile-local|r109-mobile-local|r110-mobile-local|smoke-live>
|
||||
Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|retained-local|retained-server-local|auth-security-local|payment-concurrency-local|business-flow-local|project-wide-visual-local|qg6-visual-matrix-local|r70-assist-local|r106-visual-local|r108-mobile-local|r109-mobile-local|r110-mobile-local|smoke-live>
|
||||
|
||||
可选环境变量:
|
||||
QA_PORT / QA_HOST
|
||||
@@ -66,6 +67,9 @@ Usage: bash scripts/qa/run-web-gates.sh <smoke-local|full-local|auth-security-lo
|
||||
QA_DB_DOCKER_CONTAINER / QA_DOCKER_USE_SG=1
|
||||
QA_DB_PREFIX / QA_DB_NAME / QA_KEEP_DB
|
||||
QA_FULL_REPORT_DIR full-local 独立发现、构建日志与浏览器结果目录
|
||||
retained-local 受管限定重验,无普通发布资格;必填 QA_RETAINED_SPECS
|
||||
QA_RETAINED_GREP 可选历史用例标题筛选,不改变 full-local 分母
|
||||
QA_RETAINED_SERVER_SUITE retained-server-local 必填 workflow-actions 或 server-regressions
|
||||
full-local-diagnostic 仅本地诊断,始终 PARTIAL/退出 2,无发布资格
|
||||
QA_DIAGNOSTIC_REASON / QA_DIAGNOSTIC_OPERATOR / QA_DIAGNOSTIC_APPROVAL_REF
|
||||
QA_DIAGNOSTIC_REPORT_DIR 本次诊断专用空目录;禁止覆盖历史报告
|
||||
@@ -305,6 +309,13 @@ qa_cleanup() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "$QA_FULL_STAGE_FILE" ]]; then
|
||||
local stage_status="$original_status"
|
||||
if [[ "$stage_status" == "0" ]]; then stage_status="$cleanup_status"; fi
|
||||
if ! node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" finish "$QA_FULL_STAGE_FILE" "$stage_status"; then
|
||||
cleanup_status=1
|
||||
fi
|
||||
fi
|
||||
if [[ "$original_status" != "0" ]]; then
|
||||
exit "$original_status"
|
||||
fi
|
||||
@@ -513,6 +524,17 @@ qa_start_server() {
|
||||
tail -n 200 "$QA_SERVER_LOG" >&2 || true
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Derive browser capabilities from this successfully started server, never
|
||||
# from a caller's PLAYWRIGHT_OCR_MOCK assertion. A restart recalculates it.
|
||||
export PLAYWRIGHT_OCR_MOCK=0
|
||||
if [[ "$OCR_PROVIDER" == "mock" && "$JOBS_ENABLED" == "true" ]]; then
|
||||
export PLAYWRIGHT_OCR_MOCK=1
|
||||
fi
|
||||
if [[ -n "${QA_FULL_REPORT_DIR:-}" ]]; then
|
||||
mkdir -p "$QA_FULL_REPORT_DIR"
|
||||
node -e 'require("node:fs").appendFileSync(process.argv[1], JSON.stringify({provider:process.env.OCR_PROVIDER,jobsEnabled:process.env.JOBS_ENABLED === "true",mockCorrectionEnabled:process.env.PLAYWRIGHT_OCR_MOCK === "1",source:"owned-server-start"})+"\n")' "$QA_FULL_REPORT_DIR/server-capabilities.jsonl"
|
||||
fi
|
||||
}
|
||||
|
||||
qa_build_client() {
|
||||
@@ -530,7 +552,7 @@ qa_build_client() {
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
npx tsc --noEmit --pretty false --extendedDiagnostics
|
||||
npx vite build
|
||||
NODE_ENV=production npx vite build
|
||||
)
|
||||
}
|
||||
|
||||
@@ -563,75 +585,182 @@ qa_prepare_full_inputs() {
|
||||
pwa_with_fixture env E2E_MODE=local \
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE="$QA_FULL_REPORT_DIR/discovery.json" \
|
||||
QA_VISUAL_OUTPUT_DIR="$QA_FULL_REPORT_DIR/visual" \
|
||||
npx playwright test --list --retries=0 --reporter=json \
|
||||
npx playwright test --list --retries=0 --forbid-only --reporter=json \
|
||||
> "$QA_FULL_REPORT_DIR/discovery.stdout.log" 2> "$QA_FULL_REPORT_DIR/discovery.stderr.log"
|
||||
)
|
||||
node "$ROOT_DIR/scripts/qa/verify-full-local-discovery.mjs" "$QA_FULL_REPORT_DIR/discovery.json" \
|
||||
> "$QA_FULL_REPORT_DIR/discovery-coverage.json"
|
||||
node "$ROOT_DIR/scripts/qa/full-local-coverage.mjs" plan "$QA_FULL_REPORT_DIR" \
|
||||
> "$QA_FULL_REPORT_DIR/coverage-plan.json"
|
||||
}
|
||||
|
||||
# One owner prepares synthetic accounts before any business suite runs.
|
||||
qa_seed_synthetic_credentials() {
|
||||
export QA_SYNTHETIC_INITIAL_PASSWORD="Init!$(node -e 'process.stdout.write(require("node:crypto").randomBytes(24).toString("hex"))')"
|
||||
local role
|
||||
for role in CEO ADMIN MANAGER EMPLOYEE FINANCE FINANCE_MGR HR ADMINISTRATION; do
|
||||
export "SEED_${role}_PASSWORD=$QA_SYNTHETIC_INITIAL_PASSWORD"
|
||||
done
|
||||
}
|
||||
|
||||
qa_prepare_synthetic_accounts() {
|
||||
mkdir -p "$QA_FULL_REPORT_DIR"
|
||||
export SEED_DEV_DATABASE_CONFIRM="$QA_DB_NAME"
|
||||
node "$ROOT_DIR/scripts/qa/prepare-local-e2e-accounts.mjs" "$QA_FULL_REPORT_DIR/synthetic-account-preparation.json"
|
||||
unset QA_SYNTHETIC_INITIAL_PASSWORD
|
||||
for role in CEO ADMIN MANAGER EMPLOYEE FINANCE FINANCE_MGR HR ADMINISTRATION; do unset "SEED_${role}_PASSWORD"; done
|
||||
}
|
||||
|
||||
# Focused historical-case diagnosis; never a normal full-local release receipt.
|
||||
qa_run_retained_local() {
|
||||
[[ -n "${QA_RETAINED_SPECS:-}" ]] || { echo "retained-local requires an explicit spec list" >&2; return 1; }
|
||||
local -a retained_specs
|
||||
read -r -a retained_specs <<< "$QA_RETAINED_SPECS"
|
||||
local spec
|
||||
for spec in "${retained_specs[@]}"; do
|
||||
[[ "$spec" =~ ^[a-zA-Z0-9_-]+\.spec\.ts$ && -f "$CLIENT_DIR/tests/e2e/$spec" ]] || { echo "invalid local spec selector" >&2; return 1; }
|
||||
done
|
||||
mkdir -p "$QA_FULL_REPORT_DIR"
|
||||
node -e 'require("node:fs").writeFileSync(process.argv[1], JSON.stringify({scope:"focused-historical-retest",normalReleaseEligible:false,specs:process.argv.slice(2),grep:process.env.QA_RETAINED_GREP||null})+"\n",{flag:"wx"})' "$QA_FULL_REPORT_DIR/selection.json" "${retained_specs[@]}"
|
||||
qa_select_port
|
||||
export JOBS_ENABLED=true RATE_LIMIT_ENABLED=false IM_NOTIFIER_ENABLED=false
|
||||
qa_export_app_env
|
||||
export IM_NOTIFIER_ENABLED=false
|
||||
qa_seed_synthetic_credentials
|
||||
qa_prepare_db
|
||||
qa_build_client
|
||||
qa_start_server
|
||||
mkdir -p "$QA_FULL_REPORT_DIR"
|
||||
qa_prepare_synthetic_accounts
|
||||
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
|
||||
node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" report-guest > "$QA_FULL_REPORT_DIR/environment-before-browser.json"
|
||||
local -a pwa_env=()
|
||||
if [[ " $QA_RETAINED_SPECS " == *" pwa-cross-version-upgrade.spec.ts "* ]]; then
|
||||
pwa_prepare_fixture "$CLIENT_DIR" "$QA_FULL_REPORT_DIR"
|
||||
pwa_env=("PWA_BUILD_A_DIR=$PWA_BUILD_A_DIR" "PWA_BUILD_B_DIR=$PWA_BUILD_B_DIR" "PWA_BUILD_A_SHA=$PWA_BUILD_A_SHA" "PWA_BUILD_B_SHA=$PWA_BUILD_B_SHA")
|
||||
fi
|
||||
local -a grep_args=()
|
||||
if [[ -n "${QA_RETAINED_GREP:-}" ]]; then grep_args=(--grep "$QA_RETAINED_GREP"); fi
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
env "${pwa_env[@]}" E2E_MODE=local PLAYWRIGHT_JSON_OUTPUT_FILE="$QA_FULL_REPORT_DIR/playwright-results.json" \
|
||||
QA_VISUAL_OUTPUT_DIR="$QA_FULL_REPORT_DIR/visual" QA_VISUAL_ARTIFACT_DIR="$QA_FULL_REPORT_DIR/visual-artifacts" \
|
||||
node node_modules/@playwright/test/cli.js test "${retained_specs[@]}" "${grep_args[@]}" \
|
||||
--retries=0 --reporter=line,json --output="$QA_FULL_REPORT_DIR/test-results"
|
||||
)
|
||||
}
|
||||
|
||||
# A fixed server suite in the same managed environment; never full-local coverage.
|
||||
qa_run_retained_server_local() {
|
||||
case "${QA_RETAINED_SERVER_SUITE:-}" in
|
||||
workflow-actions|server-regressions) ;;
|
||||
*) echo "retained-server-local requires an allowed server suite" >&2; return 1 ;;
|
||||
esac
|
||||
mkdir -p "$QA_FULL_REPORT_DIR"
|
||||
node -e 'require("node:fs").writeFileSync(process.argv[1], JSON.stringify({scope:"focused-server-regression",normalReleaseEligible:false,suite:process.argv[2]})+"\n",{flag:"wx"})' "$QA_FULL_REPORT_DIR/selection.json" "$QA_RETAINED_SERVER_SUITE"
|
||||
qa_select_port
|
||||
qa_export_app_env
|
||||
export JOBS_ENABLED=false RATE_LIMIT_ENABLED=false IM_NOTIFIER_ENABLED=false
|
||||
if [[ "$QA_RETAINED_SERVER_SUITE" == "server-regressions" ]]; then export IM_NOTIFIER_ENABLED=true; fi
|
||||
qa_seed_synthetic_credentials
|
||||
qa_prepare_db
|
||||
qa_build_client
|
||||
qa_start_server
|
||||
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
|
||||
qa_prepare_synthetic_accounts
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
case "$QA_RETAINED_SERVER_SUITE" in
|
||||
workflow-actions) ./scripts/regression-workflow-actions.sh ;;
|
||||
server-regressions) ./scripts/regression-all.sh ;;
|
||||
esac
|
||||
)
|
||||
}
|
||||
|
||||
# Plain calls preserve errexit inside compound helpers. Failure is sealed by
|
||||
# qa_cleanup; wrapping "$@" in an if/|| would suppress their inner failures.
|
||||
qa_full_stage() {
|
||||
local stage="$1"
|
||||
shift
|
||||
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" start "$QA_FULL_STAGE_FILE" "$stage"
|
||||
echo ">>> [full-local] $stage"
|
||||
"$@"
|
||||
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" end "$QA_FULL_STAGE_FILE" "$stage" 0
|
||||
}
|
||||
|
||||
qa_full_in_directory() (
|
||||
cd "$1"
|
||||
shift
|
||||
"$@"
|
||||
)
|
||||
|
||||
qa_full_database() {
|
||||
qa_seed_synthetic_credentials
|
||||
qa_prepare_db
|
||||
}
|
||||
|
||||
qa_full_server_start() {
|
||||
qa_start_server
|
||||
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
|
||||
}
|
||||
|
||||
qa_full_browser_server() {
|
||||
qa_cleanup_server
|
||||
export JOBS_ENABLED="${JOBS_ENABLED_FOR_E2E:-true}"
|
||||
qa_start_server
|
||||
}
|
||||
|
||||
qa_run_full_local() {
|
||||
qa_select_port
|
||||
export JOBS_ENABLED="${JOBS_ENABLED:-false}"
|
||||
qa_export_app_env
|
||||
qa_prepare_full_inputs
|
||||
qa_prepare_db
|
||||
|
||||
echo ">>> 执行 server Prisma 校验"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm run prisma:validate
|
||||
)
|
||||
|
||||
echo ">>> 执行 server 单元测试"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
npm test
|
||||
)
|
||||
|
||||
echo ">>> 执行 client typecheck"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
npx tsc --noEmit --pretty false --extendedDiagnostics
|
||||
)
|
||||
|
||||
qa_build_client
|
||||
qa_start_server
|
||||
|
||||
echo ">>> 执行 workflow-actions 回归"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
./scripts/regression-workflow-actions.sh
|
||||
)
|
||||
|
||||
echo ">>> 执行 server 全量回归"
|
||||
(
|
||||
cd "$SERVER_DIR"
|
||||
./scripts/regression-all.sh
|
||||
)
|
||||
|
||||
echo ">>> 切换 server 到 Playwright E2E 模式(启用后台 job)"
|
||||
qa_cleanup_server
|
||||
export JOBS_ENABLED="${JOBS_ENABLED_FOR_E2E:-true}"
|
||||
qa_start_server
|
||||
|
||||
qa_run_full_browser
|
||||
export RATE_LIMIT_ENABLED=false
|
||||
QA_FULL_REPORT_DIR="${QA_FULL_REPORT_DIR:-$ROOT_DIR/artifacts/qa-flow/full-local/$QA_RUN_ID}"
|
||||
mkdir -p "$QA_FULL_REPORT_DIR"
|
||||
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" init "$QA_FULL_REPORT_DIR/full-local-stages.json"
|
||||
QA_FULL_STAGE_FILE="$QA_FULL_REPORT_DIR/full-local-stages.json"
|
||||
qa_full_stage inputs qa_prepare_full_inputs
|
||||
qa_full_stage database qa_full_database
|
||||
qa_full_stage prisma qa_full_in_directory "$SERVER_DIR" npm run prisma:validate
|
||||
qa_full_stage server-unit qa_full_in_directory "$SERVER_DIR" npm test
|
||||
qa_full_stage typecheck qa_full_in_directory "$CLIENT_DIR" npx tsc --noEmit --pretty false --extendedDiagnostics
|
||||
qa_full_stage client-build qa_build_client
|
||||
qa_full_stage server-start qa_full_server_start
|
||||
qa_full_stage accounts qa_prepare_synthetic_accounts
|
||||
qa_full_stage workflow-actions qa_full_in_directory "$SERVER_DIR" ./scripts/regression-workflow-actions.sh
|
||||
qa_full_stage server-regressions qa_full_in_directory "$SERVER_DIR" ./scripts/regression-all.sh
|
||||
qa_full_stage browser-server qa_full_browser_server
|
||||
qa_full_stage browser qa_run_full_browser
|
||||
}
|
||||
|
||||
qa_run_full_browser() {
|
||||
# Only the diagnostic owner may add tracing; ordinary callers cannot narrow coverage.
|
||||
if (( $# )) && [[ "${QA_DIAGNOSTIC_STAGE:-}" != "browser" || "$*" != "--trace=on" ]]; then
|
||||
echo "[ERROR] full-local browser scope override rejected" >&2
|
||||
return 1
|
||||
fi
|
||||
local -a local_specs
|
||||
mapfile -t local_specs < "$QA_FULL_REPORT_DIR/local-spec-args.txt"
|
||||
(( ${#local_specs[@]} > 0 )) || return 1
|
||||
if [[ -n "${QA_DIAGNOSTIC_STAGE:-}" ]]; then QA_DIAGNOSTIC_PHASE=environment; fi
|
||||
node "$ROOT_DIR/scripts/qa/browser-environment-probe.mjs" guest "$QA_BASE_URL"
|
||||
node "$ROOT_DIR/scripts/qa/full-local-environment.mjs" report-guest > "$QA_FULL_REPORT_DIR/environment-before-browser.json"
|
||||
if [[ -n "${QA_DIAGNOSTIC_STAGE:-}" ]]; then QA_DIAGNOSTIC_PHASE=test; fi
|
||||
echo ">>> 执行 Playwright full E2E"
|
||||
local browser_exit=0 coverage_exit=0
|
||||
node "$ROOT_DIR/scripts/qa/qa-stage-record.mjs" selection "$QA_FULL_REPORT_DIR/execution-selection.json" "${#local_specs[@]}" "${local_specs[@]}" "$@"
|
||||
(
|
||||
cd "$CLIENT_DIR"
|
||||
pwa_with_fixture env E2E_MODE=local \
|
||||
PLAYWRIGHT_JSON_OUTPUT_FILE="$QA_FULL_REPORT_DIR/playwright-results.json" \
|
||||
QA_VISUAL_OUTPUT_DIR="$QA_FULL_REPORT_DIR/visual" \
|
||||
PLAYWRIGHT_HTML_OUTPUT_DIR="$QA_FULL_REPORT_DIR/playwright-report" PLAYWRIGHT_HTML_OPEN=never \
|
||||
npm run test:e2e:full -- "$@" --retries=0 --reporter=list,json,html --output="$QA_FULL_REPORT_DIR/test-results"
|
||||
)
|
||||
npm run test:e2e:full -- "${local_specs[@]}" "$@" --retries=0 --forbid-only --reporter=list,json,html --output="$QA_FULL_REPORT_DIR/test-results"
|
||||
) || browser_exit=$?
|
||||
node "$ROOT_DIR/scripts/qa/full-local-coverage.mjs" verify-local "$QA_FULL_REPORT_DIR" \
|
||||
> "$QA_FULL_REPORT_DIR/local-coverage-result.json" || coverage_exit=$?
|
||||
if (( browser_exit )); then return "$browser_exit"; fi
|
||||
return "$coverage_exit"
|
||||
}
|
||||
|
||||
qa_run_r70_assist_local() {
|
||||
@@ -815,6 +944,22 @@ case "$MODE" in
|
||||
source "$ROOT_DIR/scripts/qa/full-local-diagnostic-stage.sh"
|
||||
qa_run_diagnostic_stage "${2:-}"
|
||||
;;
|
||||
retained-local)
|
||||
qa_require_cmd curl
|
||||
qa_require_db_client
|
||||
qa_require_cmd node
|
||||
qa_require_cmd npm
|
||||
trap qa_cleanup EXIT
|
||||
qa_run_retained_local
|
||||
;;
|
||||
retained-server-local)
|
||||
qa_require_cmd curl
|
||||
qa_require_db_client
|
||||
qa_require_cmd node
|
||||
qa_require_cmd npm
|
||||
trap qa_cleanup EXIT
|
||||
qa_run_retained_server_local
|
||||
;;
|
||||
full-local)
|
||||
qa_require_cmd curl
|
||||
qa_require_db_client
|
||||
|
||||
在新工单中引用
屏蔽一个用户