역할별 PostgreSQL pool 상한과 동일 process 공유 pool을 적용하고 health 지표를 노출한다.\n\n게임 기능 advisory lock을 schema namespace로 통일하고 실제 PostgreSQL 통합 하네스를 보강한다.
654 lines
25 KiB
Bash
Executable File
654 lines
25 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
usage() {
|
|
echo "usage: $0 [env-file]" >&2
|
|
exit 64
|
|
}
|
|
|
|
[ "$#" -le 1 ] || usage
|
|
|
|
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
|
workspace_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
|
|
registry_file="$script_dir/conditional-integration-registry.tsv"
|
|
env_file=${1:-"$workspace_root/.env.ci"}
|
|
|
|
if [ ! -f "$env_file" ]; then
|
|
echo "missing integration environment file: $env_file" >&2
|
|
exit 66
|
|
fi
|
|
if [ ! -f "$registry_file" ]; then
|
|
echo "missing integration marker registry: $registry_file" >&2
|
|
exit 66
|
|
fi
|
|
env_file=$(CDPATH= cd -- "$(dirname -- "$env_file")" && pwd)/$(basename -- "$env_file")
|
|
|
|
# .env.ci is generated by the sam_rebuild development Compose helper and is a
|
|
# shell-compatible, credential-bearing local file. Never print its contents.
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$env_file"
|
|
set +a
|
|
|
|
: "${POSTGRES_HOST:?POSTGRES_HOST is required in $env_file}"
|
|
: "${POSTGRES_PORT:?POSTGRES_PORT is required in $env_file}"
|
|
: "${POSTGRES_DB:?POSTGRES_DB is required in $env_file}"
|
|
: "${POSTGRES_USER:?POSTGRES_USER is required in $env_file}"
|
|
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required in $env_file}"
|
|
: "${REDIS_URL:?REDIS_URL is required in $env_file}"
|
|
|
|
node_tag=$(printf '%s' "${CI_NODE_INDEX:-local}" | tr -cd 'a-zA-Z0-9_' | tr 'A-Z' 'a-z' | cut -c1-8)
|
|
run_id=$(date -u +%m%d%H%M%S)_$$_${node_tag}
|
|
export CONDITIONAL_INTEGRATION_RUN_ID=$run_id
|
|
schema_ownership_token="sammo-conditional-integration:$run_id"
|
|
supported_registry_modes="core create_general external_fixture gateway_runtime immediate_action npc_possession reference_live_sortie reference_npc_possession select_pool"
|
|
term_grace_seconds=${CONDITIONAL_INTEGRATION_TERM_GRACE_SECONDS:-10}
|
|
case "$term_grace_seconds" in
|
|
''|*[!0-9]*)
|
|
echo "CONDITIONAL_INTEGRATION_TERM_GRACE_SECONDS must be an integer from 1 to 60" >&2
|
|
exit 64
|
|
;;
|
|
esac
|
|
if [ "$term_grace_seconds" -lt 1 ] || [ "$term_grace_seconds" -gt 60 ]; then
|
|
echo "CONDITIONAL_INTEGRATION_TERM_GRACE_SECONDS must be an integer from 1 to 60" >&2
|
|
exit 64
|
|
fi
|
|
integration_schema=${CONDITIONAL_INTEGRATION_SCHEMA:-ci_${run_id}_integration}
|
|
scenario_schema=${SCENARIO_SEED_INTEGRATION_SCHEMA:-ci_${run_id}_scenario_seed}
|
|
npc_possession_schema=${NPC_POSSESSION_INTEGRATION_SCHEMA:-ci_${run_id}_npc_possession_integration}
|
|
create_general_schema=${CREATE_GENERAL_INTEGRATION_SCHEMA:-ci_${run_id}_create_general_integration}
|
|
select_pool_schema=${SELECT_POOL_INTEGRATION_SCHEMA:-ci_${run_id}_select_pool_integration}
|
|
immediate_action_schema=${IMMEDIATE_ACTION_INTEGRATION_SCHEMA:-ci_${run_id}_immediate_action_integration}
|
|
gateway_runtime_schema=${GATEWAY_RUNTIME_INTEGRATION_SCHEMA:-ci_${run_id}_gateway_runtime_integration}
|
|
npc_possession_differential_schema=${NPC_POSSESSION_DIFFERENTIAL_SCHEMA:-ci_${run_id}_npc_possession_differential}
|
|
live_sortie_schema=${LIVE_SORTIE_PERSISTENCE_SCHEMA:-ci_${run_id}_live_sortie_persistence}
|
|
|
|
for schema in \
|
|
"$integration_schema" \
|
|
"$scenario_schema" \
|
|
"$npc_possession_schema" \
|
|
"$create_general_schema" \
|
|
"$select_pool_schema" \
|
|
"$immediate_action_schema" \
|
|
"$gateway_runtime_schema" \
|
|
"$npc_possession_differential_schema" \
|
|
"$live_sortie_schema"; do
|
|
case "$schema" in
|
|
''|[!a-z_]*|*[!a-z0-9_]*)
|
|
echo "integration schema must be a lowercase PostgreSQL identifier: $schema" >&2
|
|
exit 64
|
|
;;
|
|
esac
|
|
if [ "${#schema}" -gt 63 ]; then
|
|
echo "integration schema exceeds PostgreSQL's 63-byte identifier limit: $schema" >&2
|
|
exit 64
|
|
fi
|
|
done
|
|
|
|
report_dir=$(mktemp -d)
|
|
summary_file="$report_dir/summary.tsv"
|
|
validated_registry_file="$report_dir/validated-registry.tsv"
|
|
image_upload_secret_file="$report_dir/image-upload-secret"
|
|
(
|
|
umask 077
|
|
node --input-type=module -e \
|
|
'import { randomBytes } from "node:crypto"; process.stdout.write(randomBytes(32).toString("hex"));' \
|
|
>"$image_upload_secret_file"
|
|
)
|
|
export GAME_IMAGE_UPLOAD_SECRET_FILE=$image_upload_secret_file
|
|
export GATEWAY_IMAGE_UPLOAD_SECRET_FILE=$image_upload_secret_file
|
|
active_process_group=
|
|
cleanup_resources_started=0
|
|
|
|
build_database_url() {
|
|
SCHEMA_NAME=$1 node --input-type=module -e '
|
|
const required = [
|
|
"POSTGRES_HOST",
|
|
"POSTGRES_PORT",
|
|
"POSTGRES_DB",
|
|
"POSTGRES_USER",
|
|
"POSTGRES_PASSWORD",
|
|
"SCHEMA_NAME",
|
|
];
|
|
for (const name of required) {
|
|
if (!process.env[name]) {
|
|
throw new Error(`missing ${name}`);
|
|
}
|
|
}
|
|
const url = new URL("postgresql://localhost");
|
|
url.hostname = process.env.POSTGRES_HOST;
|
|
url.port = process.env.POSTGRES_PORT;
|
|
url.username = process.env.POSTGRES_USER;
|
|
url.password = process.env.POSTGRES_PASSWORD;
|
|
url.pathname = `/${process.env.POSTGRES_DB}`;
|
|
url.searchParams.set("schema", process.env.SCHEMA_NAME);
|
|
process.stdout.write(url.href);
|
|
'
|
|
}
|
|
|
|
base_database_url=$(build_database_url public)
|
|
|
|
create_owned_schema() {
|
|
schema=$1
|
|
SCHEMA_NAME=$schema SCHEMA_OWNERSHIP_TOKEN=$schema_ownership_token \
|
|
DATABASE_URL=$base_database_url \
|
|
pnpm --filter @sammo-ts/infra exec node --input-type=module -e '
|
|
import pg from "pg";
|
|
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
const quoteIdentifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
|
|
const apostrophe = String.fromCharCode(39);
|
|
const quoteLiteral = (value) =>
|
|
`${apostrophe}${value.replaceAll(apostrophe, apostrophe.repeat(2))}${apostrophe}`;
|
|
await client.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
await client.query(`CREATE SCHEMA ${quoteIdentifier(process.env.SCHEMA_NAME)}`);
|
|
await client.query(
|
|
`COMMENT ON SCHEMA ${quoteIdentifier(process.env.SCHEMA_NAME)} IS ${quoteLiteral(
|
|
process.env.SCHEMA_OWNERSHIP_TOKEN
|
|
)}`
|
|
);
|
|
await client.query("COMMIT");
|
|
} catch (error) {
|
|
await client.query("ROLLBACK");
|
|
throw error;
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
'
|
|
}
|
|
|
|
drop_owned_schemas() {
|
|
SCHEMA_OWNERSHIP_TOKEN=$schema_ownership_token DATABASE_URL=$base_database_url \
|
|
pnpm --filter @sammo-ts/infra exec node --input-type=module -e '
|
|
import pg from "pg";
|
|
const client = new pg.Client({ connectionString: process.env.DATABASE_URL });
|
|
const quoteIdentifier = (value) => `"${value.replaceAll("\"", "\"\"")}"`;
|
|
await client.connect();
|
|
try {
|
|
const result = await client.query(
|
|
`SELECT nspname
|
|
FROM pg_namespace
|
|
WHERE obj_description(oid, $$pg_namespace$$) = $1
|
|
ORDER BY nspname`,
|
|
[process.env.SCHEMA_OWNERSHIP_TOKEN]
|
|
);
|
|
for (const { nspname } of result.rows) {
|
|
await client.query(`DROP SCHEMA ${quoteIdentifier(nspname)} CASCADE`);
|
|
}
|
|
} finally {
|
|
await client.end();
|
|
}
|
|
' >/dev/null
|
|
}
|
|
|
|
delete_owned_redis_keys() {
|
|
CONDITIONAL_INTEGRATION_RUN_ID=$run_id REDIS_URL=$REDIS_URL \
|
|
pnpm --filter @sammo-ts/infra exec node --input-type=module -e '
|
|
import { createClient } from "redis";
|
|
const client = createClient({ url: process.env.REDIS_URL });
|
|
const runId = process.env.CONDITIONAL_INTEGRATION_RUN_ID;
|
|
const patterns = [
|
|
`sammo:game:*:che:security-http-${runId}:*`,
|
|
`sammo:game:*:che:nation-html-${runId}:*`,
|
|
`sammo:che:battle-sim-e2e-${runId}-*:battle-sim:*`,
|
|
];
|
|
await client.connect();
|
|
try {
|
|
for (const pattern of patterns) {
|
|
for await (const keys of client.scanIterator({ MATCH: pattern, COUNT: 100 })) {
|
|
if (keys.length > 0) {
|
|
await client.del(keys);
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
await client.quit();
|
|
}
|
|
' >/dev/null
|
|
}
|
|
|
|
terminate_active_process_group() {
|
|
[ -n "$active_process_group" ] || return 0
|
|
process_group=$active_process_group
|
|
kill -TERM "-$process_group" 2>/dev/null || true
|
|
waited_seconds=0
|
|
while kill -0 "-$process_group" 2>/dev/null &&
|
|
[ "$waited_seconds" -lt "$term_grace_seconds" ]; do
|
|
sleep 1
|
|
waited_seconds=$((waited_seconds + 1))
|
|
done
|
|
if kill -0 "-$process_group" 2>/dev/null; then
|
|
echo "conditional integration process group did not stop after ${term_grace_seconds}s; sending SIGKILL" >&2
|
|
kill -KILL "-$process_group" 2>/dev/null || true
|
|
fi
|
|
wait "$process_group" 2>/dev/null || true
|
|
active_process_group=
|
|
}
|
|
|
|
print_summary() {
|
|
if [ ! -s "$summary_file" ]; then
|
|
echo "conditional integration summary: 0 passed files, 0 skipped files, 0 failed files"
|
|
return
|
|
fi
|
|
awk -F '\t' '
|
|
{
|
|
passed += $2;
|
|
skipped += $3;
|
|
failed += $4;
|
|
}
|
|
END {
|
|
printf "conditional integration summary: %d passed files, %d skipped files, %d failed files\n",
|
|
passed, skipped, failed;
|
|
}
|
|
' "$summary_file"
|
|
}
|
|
|
|
handle_exit() {
|
|
exit_status=$?
|
|
trap - EXIT HUP INT TERM
|
|
terminate_active_process_group
|
|
if [ "$cleanup_resources_started" -eq 1 ]; then
|
|
if ! drop_owned_schemas && [ "$exit_status" -eq 0 ]; then
|
|
exit_status=1
|
|
fi
|
|
if ! delete_owned_redis_keys && [ "$exit_status" -eq 0 ]; then
|
|
exit_status=1
|
|
fi
|
|
fi
|
|
print_summary
|
|
rm -rf "$report_dir"
|
|
exit "$exit_status"
|
|
}
|
|
|
|
trap handle_exit EXIT
|
|
trap 'exit 129' HUP
|
|
trap 'exit 130' INT
|
|
trap 'exit 143' TERM
|
|
|
|
validate_marker_registry() {
|
|
discovered_file="$report_dir/discovered-markers.txt"
|
|
registered_file="$report_dir/registered-markers.txt"
|
|
registry_errors_file="$report_dir/registry-errors.txt"
|
|
missing_file="$report_dir/unregistered-markers.txt"
|
|
stale_file="$report_dir/stale-markers.txt"
|
|
|
|
(
|
|
cd "$workspace_root"
|
|
rg -o --no-filename \
|
|
'process\.env\.[A-Z0-9_]+_DATABASE_URL' \
|
|
app/gateway-api/test \
|
|
app/game-api/test \
|
|
app/game-engine/test \
|
|
tools/integration-tests/test \
|
|
-g '*.integration.test.ts' |
|
|
sed 's/process\.env\.//' |
|
|
sort -u
|
|
) >"$discovered_file"
|
|
: >"$validated_registry_file"
|
|
awk -F '\t' -v supported_modes="$supported_registry_modes" \
|
|
-v validated_registry_file="$validated_registry_file" '
|
|
BEGIN {
|
|
split(supported_modes, mode_names, " ");
|
|
for (mode_index in mode_names) {
|
|
allowed_modes[mode_names[mode_index]] = 1;
|
|
}
|
|
}
|
|
/^#/ || /^[[:space:]]*$/ { next }
|
|
NF != 2 {
|
|
printf "line %d must contain exactly one tab-separated marker and mode\n", NR;
|
|
failed = 1;
|
|
next;
|
|
}
|
|
$1 !~ /^[A-Z][A-Z0-9_]*_DATABASE_URL$/ {
|
|
printf "line %d has invalid marker: %s\n", NR, $1;
|
|
failed = 1;
|
|
}
|
|
!($2 in allowed_modes) {
|
|
printf "line %d has unsupported execution mode: %s\n", NR, $2;
|
|
failed = 1;
|
|
}
|
|
($2 in allowed_modes) {
|
|
mode_counts[$2]++;
|
|
}
|
|
seen[$1]++ {
|
|
printf "line %d duplicates marker: %s\n", NR, $1;
|
|
failed = 1;
|
|
}
|
|
{
|
|
print $1 "\t" $2 > validated_registry_file;
|
|
}
|
|
END {
|
|
for (mode_name in allowed_modes) {
|
|
if (!mode_counts[mode_name]) {
|
|
printf "supported execution mode has no marker: %s\n", mode_name;
|
|
failed = 1;
|
|
}
|
|
}
|
|
exit failed;
|
|
}
|
|
' "$registry_file" >"$registry_errors_file" || {
|
|
echo "invalid conditional integration marker registry:" >&2
|
|
sed 's/^/ /' "$registry_errors_file" >&2
|
|
exit 65
|
|
}
|
|
sort -u "$validated_registry_file" -o "$validated_registry_file"
|
|
cut -f1 "$validated_registry_file" >"$registered_file"
|
|
|
|
comm -23 "$discovered_file" "$registered_file" >"$missing_file"
|
|
if [ -s "$missing_file" ]; then
|
|
echo "unregistered conditional database marker(s):" >&2
|
|
sed 's/^/ /' "$missing_file" >&2
|
|
exit 65
|
|
fi
|
|
|
|
comm -13 "$discovered_file" "$registered_file" >"$stale_file"
|
|
if [ -s "$stale_file" ]; then
|
|
echo "stale conditional database marker(s):" >&2
|
|
sed 's/^/ /' "$stale_file" >&2
|
|
exit 65
|
|
fi
|
|
}
|
|
|
|
markers_for_mode() {
|
|
mode=$1
|
|
awk -F '\t' -v mode="$mode" '$2 == mode { print $1 }' "$validated_registry_file" |
|
|
paste -sd '|' -
|
|
}
|
|
|
|
record_vitest_result() {
|
|
label=$1
|
|
result_file=$2
|
|
counts=$(node --input-type=module -e '
|
|
import fs from "node:fs";
|
|
const result = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
|
const files = Array.isArray(result.testResults) ? result.testResults : [];
|
|
const isSkipped = ({ assertionResults = [], status }) =>
|
|
status === "pending" ||
|
|
(assertionResults.length > 0 &&
|
|
assertionResults.every(({ status: assertionStatus }) =>
|
|
assertionStatus === "pending" || assertionStatus === "todo"
|
|
));
|
|
const skipped = files.filter(isSkipped).length;
|
|
const failed = files.filter(({ status }) => status === "failed").length;
|
|
const passed = files.length - skipped - failed;
|
|
process.stdout.write(`${passed}\t${skipped}\t${failed}`);
|
|
' "$result_file")
|
|
printf '%s\t%s\n' "$label" "$counts" >>"$summary_file"
|
|
skipped=$(printf '%s' "$counts" | cut -f2)
|
|
if [ "$skipped" -ne 0 ]; then
|
|
echo "$label left $skipped integration test file(s) skipped" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
run_vitest() {
|
|
package_dir=$1
|
|
label=$2
|
|
shift 2
|
|
result_file="$report_dir/$(printf '%s' "$label" | tr -cd 'a-zA-Z0-9_' | tr 'A-Z' 'a-z').json"
|
|
|
|
echo "running $label tests in $package_dir"
|
|
(
|
|
cd "$workspace_root/$package_dir"
|
|
exec setsid pnpm exec vitest run \
|
|
--no-file-parallelism \
|
|
--maxWorkers=1 \
|
|
--reporter=default \
|
|
--reporter=json \
|
|
--outputFile.json="$result_file" \
|
|
"$@"
|
|
) &
|
|
active_process_group=$!
|
|
test_status=0
|
|
wait "$active_process_group" || test_status=$?
|
|
active_process_group=
|
|
|
|
result_status=0
|
|
if [ -f "$result_file" ]; then
|
|
record_vitest_result "$label" "$result_file" || result_status=$?
|
|
else
|
|
printf '%s\t0\t0\t1\n' "$label" >>"$summary_file"
|
|
result_status=1
|
|
fi
|
|
if [ "$test_status" -eq 0 ] && [ "$result_status" -ne 0 ]; then
|
|
test_status=$result_status
|
|
fi
|
|
return "$test_status"
|
|
}
|
|
|
|
run_marked_tests() {
|
|
package_dir=$1
|
|
marker=$2
|
|
label=$3
|
|
if [ -z "$marker" ]; then
|
|
echo "no conditional database marker configured for $label" >&2
|
|
exit 65
|
|
fi
|
|
test_files=$(cd "$workspace_root/$package_dir" && rg -l "$marker" test -g '*.integration.test.ts' | sort)
|
|
if [ -z "$test_files" ]; then
|
|
echo "no $label tests found under $package_dir" >&2
|
|
exit 65
|
|
fi
|
|
# Integration test paths cannot contain whitespace in this repository.
|
|
# shellcheck disable=SC2086
|
|
run_vitest "$package_dir" "$label" $test_files
|
|
}
|
|
|
|
run_redis_only_tests() {
|
|
package_dir=app/game-api
|
|
database_marker=$1
|
|
test_files=$(
|
|
cd "$workspace_root/$package_dir"
|
|
redis_files=$(rg -l 'process\.env\.REDIS_URL' test -g '*.integration.test.ts' | sort)
|
|
# Files already selected through a database marker run once in that
|
|
# database group, where Redis is also available.
|
|
# shellcheck disable=SC2086
|
|
rg --files-without-match "$database_marker" $redis_files
|
|
)
|
|
if [ -z "$test_files" ]; then
|
|
echo "no Redis-only integration tests found under $package_dir" >&2
|
|
exit 65
|
|
fi
|
|
# shellcheck disable=SC2086
|
|
run_vitest "$package_dir" "game_api_redis" $test_files
|
|
}
|
|
|
|
export PATH
|
|
cd "$workspace_root"
|
|
validate_marker_registry
|
|
|
|
pnpm install --frozen-lockfile
|
|
pnpm --filter @sammo-ts/infra prisma:generate
|
|
pnpm --filter @sammo-ts/common build
|
|
pnpm --filter @sammo-ts/logic build
|
|
pnpm --filter @sammo-ts/infra build
|
|
pnpm --filter @sammo-ts/game-engine build
|
|
|
|
GATEWAY_MIGRATION_TEST_DATABASE_URL=$base_database_url \
|
|
pnpm --filter @sammo-ts/infra verify:migration:account-icon
|
|
|
|
cleanup_resources_started=1
|
|
create_owned_schema "$integration_schema"
|
|
create_owned_schema "$npc_possession_schema"
|
|
create_owned_schema "$scenario_schema"
|
|
|
|
database_url=$(build_database_url "$integration_schema")
|
|
export POSTGRES_SCHEMA=$integration_schema
|
|
export DATABASE_URL=$database_url
|
|
export INPUT_EVENT_DATABASE_URL=$database_url
|
|
export GENERAL_LIFECYCLE_DATABASE_URL=$database_url
|
|
export TURN_DAEMON_LEASE_DATABASE_URL=$database_url
|
|
export TURN_DIFFERENTIAL_DATABASE_URL=$database_url
|
|
export RESERVED_TURN_DATABASE_URL=$database_url
|
|
export PROFILE_SEED_CLI_DATABASE_URL=$database_url
|
|
export PROFILE_SEED_DATABASE_URL=$database_url
|
|
profile_lock_secondary_database_url=$(build_database_url "$scenario_schema")
|
|
export PROFILE_LOCK_SECONDARY_DATABASE_URL=$profile_lock_secondary_database_url
|
|
export READ_MODEL_JOURNAL_DATABASE_URL=$database_url
|
|
|
|
# The infra PostgreSQL boundary tests assert migration-owned seed rows, CHECK
|
|
# constraints, and indexes. `prisma db push` only materializes the Prisma data
|
|
# model, so provision the primary integration schema through the production
|
|
# migration chain.
|
|
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
|
|
|
|
core_database_markers=$(markers_for_mode core)
|
|
run_marked_tests packages/infra "$core_database_markers" "infra_postgresql"
|
|
run_marked_tests app/game-api "$core_database_markers" "game_api_postgresql"
|
|
run_marked_tests app/game-engine "$core_database_markers" "game_engine_postgresql"
|
|
run_marked_tests tools/integration-tests "$core_database_markers" "snapshot_postgresql"
|
|
|
|
create_owned_schema "$create_general_schema"
|
|
create_general_database_url=$(build_database_url "$create_general_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$create_general_schema
|
|
export DATABASE_URL=$create_general_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:db:push:game
|
|
)
|
|
export CREATE_GENERAL_DATABASE_URL=$create_general_database_url
|
|
run_marked_tests app/game-api \
|
|
"$(markers_for_mode create_general)" \
|
|
"create_general_postgresql"
|
|
|
|
create_owned_schema "$select_pool_schema"
|
|
select_pool_database_url=$(build_database_url "$select_pool_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$select_pool_schema
|
|
export DATABASE_URL=$select_pool_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:db:push:game
|
|
)
|
|
export SELECT_POOL_DATABASE_URL=$select_pool_database_url
|
|
run_marked_tests app/game-api \
|
|
"$(markers_for_mode select_pool)" \
|
|
"select_pool_api_postgresql"
|
|
run_marked_tests app/game-engine \
|
|
"$(markers_for_mode select_pool)" \
|
|
"select_pool_engine_postgresql"
|
|
|
|
create_owned_schema "$immediate_action_schema"
|
|
immediate_action_database_url=$(build_database_url "$immediate_action_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$immediate_action_schema
|
|
export DATABASE_URL=$immediate_action_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:db:push:game
|
|
)
|
|
export IMMEDIATE_ACTION_DATABASE_URL=$immediate_action_database_url
|
|
run_marked_tests app/game-api \
|
|
"$(markers_for_mode immediate_action)" \
|
|
"immediate_action_api_postgresql"
|
|
run_marked_tests app/game-engine \
|
|
"$(markers_for_mode immediate_action)" \
|
|
"immediate_action_postgresql"
|
|
|
|
create_owned_schema "$gateway_runtime_schema"
|
|
gateway_runtime_database_url=$(build_database_url "$gateway_runtime_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$gateway_runtime_schema
|
|
export DATABASE_URL=$gateway_runtime_database_url
|
|
export GATEWAY_DATABASE_URL=$gateway_runtime_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
|
|
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
|
|
)
|
|
export GATEWAY_RUNTIME_ACTION_DATABASE_URL=$gateway_runtime_database_url
|
|
export GATEWAY_OPERATION_DATABASE_URL=$gateway_runtime_database_url
|
|
export GATEWAY_RELEASE_DATABASE_URL=$gateway_runtime_database_url
|
|
export GATEWAY_RUNTIME_INTEGRATION_SCHEMA=$gateway_runtime_schema
|
|
run_marked_tests app/gateway-api \
|
|
"$(markers_for_mode gateway_runtime)" \
|
|
"gateway_runtime_gateway_api_postgresql"
|
|
run_marked_tests app/game-engine \
|
|
"$(markers_for_mode gateway_runtime)" \
|
|
"gateway_runtime_postgresql"
|
|
|
|
npc_possession_database_url=$(build_database_url "$npc_possession_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$npc_possession_schema
|
|
export DATABASE_URL=$npc_possession_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
|
|
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
|
|
)
|
|
export NPC_POSSESSION_DATABASE_URL=$npc_possession_database_url
|
|
run_marked_tests app/game-api \
|
|
"$(markers_for_mode npc_possession)" \
|
|
"npc_possession_postgresql"
|
|
|
|
if [ "${TURN_DIFFERENTIAL_REFERENCE:-}" = "1" ]; then
|
|
reference_workspace_root=${TURN_DIFFERENTIAL_WORKSPACE_ROOT:-"$workspace_root/.."}
|
|
reference_stack=${TURN_DIFFERENTIAL_STACK_DIR:-"$reference_workspace_root/docker_compose_files/reference"}
|
|
if [ ! -f "$reference_stack/compose.yml" ]; then
|
|
echo "reference compose file is missing; set TURN_DIFFERENTIAL_WORKSPACE_ROOT or TURN_DIFFERENTIAL_STACK_DIR" >&2
|
|
exit 69
|
|
fi
|
|
if [ ! -f "$reference_workspace_root/ref/sam/d_setting/RootDB.php" ] ||
|
|
[ ! -f "$reference_workspace_root/ref/sam/hwe/d_setting/DB.php" ]; then
|
|
echo "reference runtime database configuration is missing under $reference_workspace_root/ref/sam" >&2
|
|
exit 69
|
|
fi
|
|
if ! docker compose -f "$reference_stack/compose.yml" ps --status running --services | grep -qx db ||
|
|
! docker compose -f "$reference_stack/compose.yml" exec -T db healthcheck.sh --connect --innodb_initialized \
|
|
>/dev/null 2>&1; then
|
|
echo "TURN_DIFFERENTIAL_REFERENCE=1 requires a healthy bootstrapped reference db stack" >&2
|
|
exit 69
|
|
fi
|
|
reference_tables=$(
|
|
docker compose -f "$reference_stack/compose.yml" exec -T db sh -c '
|
|
export MYSQL_PWD="$(cat /run/secrets/db_root_password)"
|
|
mariadb --batch --skip-column-names -u root -e "
|
|
SELECT COUNT(*) FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = '\''$MARIADB_DATABASE'\'' AND TABLE_NAME = '\''member'\'';
|
|
SELECT COUNT(*) FROM information_schema.TABLES
|
|
WHERE TABLE_SCHEMA = '\''$REF_HWE_DB_NAME'\'' AND TABLE_NAME IN ('\''general'\'', '\''select_npc_token'\'');
|
|
"
|
|
' 2>/dev/null
|
|
) || {
|
|
echo "failed to inspect reference database bootstrap state" >&2
|
|
exit 69
|
|
}
|
|
if [ "$reference_tables" != "$(printf '1\n2')" ]; then
|
|
echo "reference root/HWE databases are not bootstrapped" >&2
|
|
exit 69
|
|
fi
|
|
|
|
create_owned_schema "$npc_possession_differential_schema"
|
|
npc_possession_differential_database_url=$(build_database_url "$npc_possession_differential_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$npc_possession_differential_schema
|
|
export DATABASE_URL=$npc_possession_differential_database_url
|
|
export NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL=$npc_possession_differential_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:db:push:game
|
|
)
|
|
export NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL=$npc_possession_differential_database_url
|
|
export TURN_DIFFERENTIAL_WORKSPACE_ROOT=$reference_workspace_root
|
|
export TURN_DIFFERENTIAL_STACK_DIR=$reference_stack
|
|
run_marked_tests tools/integration-tests \
|
|
"$(markers_for_mode reference_npc_possession)" \
|
|
"npc_possession_reference"
|
|
|
|
create_owned_schema "$live_sortie_schema"
|
|
live_sortie_database_url=$(build_database_url "$live_sortie_schema")
|
|
(
|
|
export POSTGRES_SCHEMA=$live_sortie_schema
|
|
export DATABASE_URL=$live_sortie_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:db:push:game
|
|
)
|
|
export POSTGRES_SCHEMA=$live_sortie_schema
|
|
export DATABASE_URL=$live_sortie_database_url
|
|
export LIVE_SORTIE_PERSISTENCE_DATABASE_URL=$live_sortie_database_url
|
|
run_marked_tests tools/integration-tests \
|
|
"$(markers_for_mode reference_live_sortie)" \
|
|
"live_sortie_postgresql"
|
|
export POSTGRES_SCHEMA=$integration_schema
|
|
export DATABASE_URL=$database_url
|
|
fi
|
|
|
|
run_redis_only_tests "$core_database_markers"
|
|
|
|
scenario_database_url=$(build_database_url "$scenario_schema")
|
|
export POSTGRES_SCHEMA=$scenario_schema
|
|
export DATABASE_URL=$scenario_database_url
|
|
pnpm --filter @sammo-ts/infra prisma:db:push:game
|
|
run_vitest app/game-engine "scenario_seed_postgresql" test/scenarioSeeder.test.ts
|
|
|
|
echo "conditional PostgreSQL and Redis integration tests passed"
|