Files
BlueArchiveToolkit/scripts/bat-api-local-live-smoke.sh
T
nyaKazuha 7f465523e1
bat-rust / Build and test Go API (push) Canceled after 0s
bat-rust / Build and test Rust (push) Canceled after 0s
fix(bat-api): 完成 issue #19 同机 live 联调
2026-08-29 22:58:01 +08:00

395 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${REPO_ROOT}"
timestamp="$(date -u +%Y%m%dT%H%M%SZ)"
SMOKE_ROOT="${BAT_API_LIVE_SMOKE_ROOT:-/tmp/bat-api-local-live-${timestamp}}"
OUTPUT_DIR="${SMOKE_ROOT}/official"
LOCALIZED_DIR="${SMOKE_ROOT}/localized"
STATE_DIR="${SMOKE_ROOT}/state"
REPORT_DIR="${SMOKE_ROOT}/report"
FIXTURE_ROOT="${REPO_ROOT}/internal/api/testdata/release"
BAT_BIN="${BAT_BIN:-${REPO_ROOT}/target/debug/bat}"
BAT_API_BIN="${BAT_API_BIN:-${SMOKE_ROOT}/bat-api}"
PORT="${BAT_API_LIVE_PORT:-18181}"
BASE_URL="http://127.0.0.1:${PORT}"
SOCKET_PATH="${STATE_DIR}/bat.sock"
API_PID=""
fail() {
printf 'error: %s\n' "$*" >&2
exit 1
}
require_command() {
command -v "$1" >/dev/null 2>&1 || fail "required command is missing: $1"
}
require_command cargo
require_command curl
require_command go
require_command jq
require_command realpath
[[ -d "${FIXTURE_ROOT}" ]] || fail "release fixture directory is missing: ${FIXTURE_ROOT}"
[[ "${SMOKE_ROOT}" == /tmp/* ]] || fail "BAT_API_LIVE_SMOKE_ROOT must be under /tmp"
[[ ! -e "${SMOKE_ROOT}" ]] || fail "smoke root already exists: ${SMOKE_ROOT}"
[[ -x "${BAT_BIN}" || "${BAT_API_LIVE_SKIP_BUILD:-}" != "1" ]] || fail "bat binary is not executable: ${BAT_BIN}"
mkdir -p "${OUTPUT_DIR}" "${LOCALIZED_DIR}" "${STATE_DIR}" "${REPORT_DIR}"
if [[ "${BAT_API_LIVE_SKIP_BUILD:-}" != "1" ]]; then
cargo build -p bat-infrastructure --bin bat
GOCACHE="${GOCACHE:-/tmp/bat-go-cache}" go build -o "${BAT_API_BIN}" ./cmd/bat-api
fi
[[ -x "${BAT_BIN}" ]] || fail "bat binary is not executable: ${BAT_BIN}"
[[ -x "${BAT_API_BIN}" ]] || fail "bat-api binary is not executable: ${BAT_API_BIN}"
write_version_state() {
local id="$1"
local version_dir="$2"
local snapshot_path="${version_dir}/official-sync-snapshot.json"
local state_path="${OUTPUT_DIR}/official-version-state.json"
local temp_path="${state_path}.tmp.$$"
jq -n \
--arg id "${id}" \
--arg app_version "1.70.0" \
--arg bundle_version "s8tloc7lo3" \
--arg addressables_root "https://prod-clientpatch.bluearchiveyostar.com/r93_fixture" \
--arg resource_root "${version_dir}" \
--arg snapshot_path "${snapshot_path}" \
'{
state_version: 1,
current_completed_version: {
id: $id,
app_version: $app_version,
bundle_version: $bundle_version,
addressables_root: $addressables_root,
resource_root: $resource_root,
snapshot_path: $snapshot_path,
staging_path: null,
version_path: $resource_root,
started_unix_seconds: null,
completed_unix_seconds: 1
},
in_progress_version: null,
previous_available_version: null,
failed_versions: [],
updated_unix_seconds: 1
}' >"${temp_path}"
mv -f "${temp_path}" "${state_path}"
}
write_empty_version_state() {
local state_path="${OUTPUT_DIR}/official-version-state.json"
local temp_path="${state_path}.tmp.$$"
jq -n '{
state_version: 1,
current_completed_version: null,
in_progress_version: null,
previous_available_version: null,
failed_versions: [],
updated_unix_seconds: 1
}' >"${temp_path}"
mv -f "${temp_path}" "${state_path}"
}
switch_current() {
local id="$1"
local link_path="${OUTPUT_DIR}/current"
local temp_link="${OUTPUT_DIR}/.current.$$"
ln -s "versions/${id}" "${temp_link}"
mv -Tf "${temp_link}" "${link_path}"
}
stop_daemon() {
if [[ -S "${SOCKET_PATH}" || -f "${STATE_DIR}/bat.pid" ]]; then
"${BAT_BIN}" stop \
--state-dir "${STATE_DIR}" \
--json \
--no-progress \
--no-banner \
>"${REPORT_DIR}/daemon-stop.json" 2>"${REPORT_DIR}/daemon-stop.log" || true
fi
}
cleanup() {
if [[ -n "${API_PID}" ]] && kill -0 "${API_PID}" 2>/dev/null; then
kill "${API_PID}" 2>/dev/null || true
wait "${API_PID}" 2>/dev/null || true
fi
stop_daemon
printf 'Smoke artifacts: %s\n' "${SMOKE_ROOT}" >&2
}
trap cleanup EXIT
mkdir -p "${OUTPUT_DIR}/versions/v-current"
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-current/"
write_version_state "v-current" "${OUTPUT_DIR}/versions/v-current"
switch_current "v-current"
# The daemon is real and owns the live RPC socket, but this smoke must never
# reach official endpoints. Its sync loop can fail and retry while the seeded
# published release remains available to bat-api.
LOCAL_CURL="${SMOKE_ROOT}/curl-network-blocked"
printf '%s\n' \
'#!/usr/bin/env bash' \
'printf "local live smoke blocks network access: curl %q\n" "$*" >&2' \
'exit 7' >"${LOCAL_CURL}"
chmod +x "${LOCAL_CURL}"
printf '==> start Rust bat daemon\n'
BAT_AUTO_DISCOVER=0 "${BAT_BIN}" \
--server-info-path "${FIXTURE_ROOT}/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
--connection-group Prod \
--app-version 1.70.0 \
--output "${OUTPUT_DIR}" \
--localized-output "${LOCALIZED_DIR}" \
--state-dir "${STATE_DIR}" \
--interval 1h \
--error-retry 1s \
--platforms Windows \
--curl "${LOCAL_CURL}" \
--daemon \
--json \
--no-progress \
--no-banner \
>"${REPORT_DIR}/daemon-start.json" 2>"${REPORT_DIR}/daemon-start.log"
for _ in $(seq 1 100); do
[[ -S "${SOCKET_PATH}" ]] && break
sleep 0.1
done
[[ -S "${SOCKET_PATH}" ]] || fail "Rust bat did not create ${SOCKET_PATH}; see ${REPORT_DIR}/daemon-start.log"
printf '==> start bat-api on the same host\n'
BAT_API_SKIP_ENV_FILE=1 "${BAT_API_BIN}" \
--listen "127.0.0.1:${PORT}" \
--public-base-url "${BASE_URL}" \
--state-dir "${STATE_DIR}" \
--socket "${SOCKET_PATH}" \
--server-info-file "${FIXTURE_ROOT}/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
--refresh-interval 100ms \
>"${REPORT_DIR}/bat-api.log" 2>&1 &
API_PID="$!"
wait_http_status() {
local path="$1"
local expected="$2"
local response_file="$3"
shift 3
local curl_args=("$@")
local status=""
for _ in $(seq 1 100); do
status="$(curl -sS "${curl_args[@]}" -o "${response_file}" -w '%{http_code}' "${BASE_URL}${path}" || true)"
if [[ "${status}" == "${expected}" ]]; then
return 0
fi
sleep 0.1
done
fail "${path} expected HTTP ${expected}, got ${status}; response: $(tr '\n' ' ' <"${response_file}")"
}
wait_json() {
local path="$1"
local expression="$2"
local response_file="$3"
for _ in $(seq 1 100); do
curl -sS "${BASE_URL}${path}" >"${response_file}" 2>/dev/null || true
if jq -e "${expression}" "${response_file}" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
fail "${path} did not satisfy ${expression}; response: $(tr '\n' ' ' <"${response_file}")"
}
wait_json_status() {
local path="$1"
local expected="$2"
local expression="$3"
local response_file="$4"
local status=""
for _ in $(seq 1 100); do
status="$(curl -sS -o "${response_file}" -w '%{http_code}' "${BASE_URL}${path}" || true)"
if [[ "${status}" == "${expected}" ]] &&
jq -e "${expression}" "${response_file}" >/dev/null 2>&1; then
return 0
fi
sleep 0.1
done
fail "${path} expected HTTP ${expected} and ${expression}, got ${status}; response: $(tr '\n' ' ' <"${response_file}")"
}
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-current"))' \
"${REPORT_DIR}/ready-initial.json"
curl -fsS "${BASE_URL}/v1/bootstrap" >"${REPORT_DIR}/bootstrap-initial.json"
jq -e '
.ready == true and
.policy.pull_owner == "rust_bat" and
.policy.resource_root_discovery == "bat_sock_rpc" and
.resource.release.version_id == "v-current"
' "${REPORT_DIR}/bootstrap-initial.json" >/dev/null
curl -fsS "${BASE_URL}/v1/launcher/bootstrap" >"${REPORT_DIR}/launcher-bootstrap-initial.json"
jq -e '
.ready == true and
.policy.package_update_manifest == false and
.policy.downloads_launcher_package == false and
.addressables_catalog_url_root ==
"http://127.0.0.1:'"${PORT}"'/prod-clientpatch.bluearchiveyostar.com/r93_fixture"
' "${REPORT_DIR}/launcher-bootstrap-initial.json" >/dev/null
curl -fsS "${BASE_URL}/api-launcher-jp.yo-star.com/api/launcher/game/config/json" \
>"${REPORT_DIR}/launcher-config-json-initial.json"
jq -e '
.code == 200 and
.data.package_update_manifest == false and
.data.scope == "resource_bootstrap_only"
' "${REPORT_DIR}/launcher-config-json-initial.json" >/dev/null
curl -fsS "${BASE_URL}/v1/server-info" >"${REPORT_DIR}/server-info-initial.json"
jq -e '
.ConnectionGroups[0].AddressablesCatalogUrlRoot ==
"http://127.0.0.1:'"${PORT}"'/prod-clientpatch.bluearchiveyostar.com/r93_fixture"
' "${REPORT_DIR}/server-info-initial.json" >/dev/null
CDN_PATH="/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
curl -fsS "${BASE_URL}${CDN_PATH}" >"${REPORT_DIR}/cdn-full.bytes"
[[ "$(cat "${REPORT_DIR}/cdn-full.bytes")" == "TABLE_CATALOG_FIXTURE" ]] || fail "CDN full body mismatch"
curl -fsSI "${BASE_URL}${CDN_PATH}" >"${REPORT_DIR}/cdn-head.headers"
grep -Fqi 'content-length: 21' "${REPORT_DIR}/cdn-head.headers" || fail "CDN HEAD content length missing"
grep -Fqi 'cache-control: public, max-age=31536000, immutable' "${REPORT_DIR}/cdn-head.headers" ||
fail "CDN cache header missing"
curl -fsS -D "${REPORT_DIR}/cdn-range.headers" \
-H 'Range: bytes=0-3' \
-o "${REPORT_DIR}/cdn-range.bytes" \
"${BASE_URL}${CDN_PATH}"
grep -Fq '206 Partial Content' "${REPORT_DIR}/cdn-range.headers" || fail "CDN range did not return 206"
[[ "$(cat "${REPORT_DIR}/cdn-range.bytes")" == "TABL" ]] || fail "CDN range body mismatch"
wait_http_status "/prod-clientpatch.bluearchiveyostar.com/r93_fixture/missing.bin" "404" \
"${REPORT_DIR}/cdn-missing.response"
wait_http_status \
"/prod-clientpatch.bluearchiveyostar.com/%2e%2e/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
"404" \
"${REPORT_DIR}/cdn-escape.response" \
--path-as-is
printf '==> switch current release through the local published state\n'
mkdir -p "${OUTPUT_DIR}/versions/v-next"
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-next/"
write_version_state "v-next" "${OUTPUT_DIR}/versions/v-next"
switch_current "v-next"
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-next"))' \
"${REPORT_DIR}/ready-after-switch.json"
curl -fsS "${BASE_URL}/v1/bootstrap" >"${REPORT_DIR}/bootstrap-after-switch.json"
jq -e '.ready == true and .resource.release.version_id == "v-next"' \
"${REPORT_DIR}/bootstrap-after-switch.json" >/dev/null
printf '==> publish a size-mismatched release and require not-ready\n'
mkdir -p "${OUTPUT_DIR}/versions/v-broken"
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-broken/"
printf 'intentional size mismatch\n' >"${OUTPUT_DIR}/versions/v-broken/prod-clientpatch.bluearchiveyostar.com/r93_fixture/TableBundles/TableCatalog.bytes"
write_version_state "v-broken" "${OUTPUT_DIR}/versions/v-broken"
switch_current "v-broken"
wait_json_status "/readyz" "503" \
'.ready == false and .present_count == 1 and .missing_count == 1 and (.resource_root | endswith("/versions/v-broken"))' \
"${REPORT_DIR}/ready-size-mismatch.json"
wait_http_status "${CDN_PATH}" "404" "${REPORT_DIR}/cdn-size-mismatch.response"
printf '==> publish a manifest-missing release and require not-ready\n'
mkdir -p "${OUTPUT_DIR}/versions/v-no-manifest"
cp -a "${FIXTURE_ROOT}/." "${OUTPUT_DIR}/versions/v-no-manifest/"
mv "${OUTPUT_DIR}/versions/v-no-manifest/official-download-manifest.json" \
"${OUTPUT_DIR}/versions/v-no-manifest/official-download-manifest.json.disabled"
write_version_state "v-no-manifest" "${OUTPUT_DIR}/versions/v-no-manifest"
switch_current "v-no-manifest"
wait_json_status "/readyz" "503" \
'.ready == false and .resource_root == ""' \
"${REPORT_DIR}/ready-manifest-missing.json"
printf '==> publish no-release state and require not-ready\n'
write_empty_version_state
wait_json_status "/readyz" "503" '.ready == false and .resource_root == ""' \
"${REPORT_DIR}/ready-no-release.json"
switch_current "v-next"
write_version_state "v-next" "${OUTPUT_DIR}/versions/v-next"
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-next"))' \
"${REPORT_DIR}/ready-after-restore.json"
printf '==> disconnect and reconnect bat.sock\n'
stop_daemon
for _ in $(seq 1 100); do
[[ ! -S "${SOCKET_PATH}" ]] && break
sleep 0.1
done
wait_json "/healthz" '.rpc_available == false and .ready == false' \
"${REPORT_DIR}/health-rpc-disconnected.json"
wait_http_status "/readyz" "503" "${REPORT_DIR}/ready-rpc-disconnected.json"
BAT_AUTO_DISCOVER=0 "${BAT_BIN}" \
--server-info-path "${FIXTURE_ROOT}/yostar-serverinfo.bluearchiveyostar.com/r93_fixture.json" \
--connection-group Prod \
--app-version 1.70.0 \
--output "${OUTPUT_DIR}" \
--localized-output "${LOCALIZED_DIR}" \
--state-dir "${STATE_DIR}" \
--interval 1h \
--error-retry 1s \
--platforms Windows \
--curl "${LOCAL_CURL}" \
--daemon \
--json \
--no-progress \
--no-banner \
>"${REPORT_DIR}/daemon-restart.json" 2>"${REPORT_DIR}/daemon-restart.log"
for _ in $(seq 1 100); do
[[ -S "${SOCKET_PATH}" ]] && break
sleep 0.1
done
[[ -S "${SOCKET_PATH}" ]] || fail "Rust bat did not recreate ${SOCKET_PATH}"
wait_json "/readyz" '.ready == true and (.resource_root | endswith("/versions/v-next"))' \
"${REPORT_DIR}/ready-after-reconnect.json"
curl -fsS "${BASE_URL}/healthz" >"${REPORT_DIR}/health-final.json"
jq -e '.ok == true and .rpc_available == true and .ready == true' \
"${REPORT_DIR}/health-final.json" >/dev/null
cat >"${REPORT_DIR}/SMOKE_REPORT.md" <<EOF
# bat-api Local Live Smoke Report
- generated_at_utc: ${timestamp}
- bat: ${BAT_BIN}
- bat_api: ${BAT_API_BIN}
- state_dir: ${STATE_DIR}
- official_output: ${OUTPUT_DIR}
- socket: ${SOCKET_PATH}
- listen: ${BASE_URL}
## Covered
- same-host Rust \`bat --daemon\` and Go \`bat-api\` over \`bat.sock\`
- initial ready and resource bootstrap
- server-info Addressables root rewrite
- CDN GET, HEAD, Range, ETag/cache semantics and missing/escape paths
- atomic \`current\` release switch
- size-mismatched or manifest-missing release -> \`/readyz\` 503 and no stale index
- no published release -> \`/readyz\` 503 and empty index
- RPC disconnect -> \`/healthz\` reports unavailable and \`/readyz\` 503
- daemon restart and release rediscovery
All inputs are isolated under \`${SMOKE_ROOT}\`; no official network or client
directory is used.
EOF
printf 'LOCAL_BAT_API_LIVE_SMOKE_OK\n'
printf 'Smoke report: %s\n' "${REPORT_DIR}/SMOKE_REPORT.md"