test: isolate conditional integration runner

This commit is contained in:
2026-07-31 07:59:48 +00:00
parent 425c258bf5
commit 2378bfee92
5 changed files with 419 additions and 70 deletions
@@ -19,7 +19,8 @@ import { createGameApiServer } from '../src/server.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
const integration = describe.skipIf(!databaseUrl || !process.env.REDIS_URL);
const profileId = process.env.POSTGRES_SCHEMA ?? 'conditional_integration';
const profileName = `che:nation-html-${process.pid}`;
const runId = process.env.CONDITIONAL_INTEGRATION_RUN_ID ?? String(process.pid);
const profileName = `che:nation-html-${runId}`;
const userId = `nation-html-user-${process.pid}`;
const fixtureId = 900_000 + (process.pid % 50_000);
const secret = 'nation-html-http-secret';
@@ -57,6 +58,18 @@ const restoreEnv = (): void => {
}
};
const deleteProfileRedisKeys = async (): Promise<void> => {
if (!redis) {
return;
}
for await (const key of redis.client.scanIterator({
MATCH: `sammo:game:*:${profileName}:*`,
COUNT: 100,
})) {
await redis.client.del(key);
}
};
integration('nation HTML purification over HTTP transport', () => {
beforeAll(async () => {
uploadDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sammo-nation-html-http-'));
@@ -150,6 +163,7 @@ integration('nation HTML purification over HTTP transport', () => {
await db?.nation.deleteMany({ where: { id: fixtureId } });
await db?.worldState.deleteMany({ where: { id: fixtureId } });
await disconnectDb?.();
await deleteProfileRedisKeys();
await redis?.disconnect();
if (uploadDir) {
await fs.rm(uploadDir, { recursive: true, force: true });
@@ -19,7 +19,8 @@ import { createGameApiServer } from '../src/server.js';
const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL;
const integration = describe.skipIf(!databaseUrl || !process.env.REDIS_HOST || !process.env.REDIS_PORT);
const profileId = process.env.POSTGRES_SCHEMA ?? 'conditional_integration';
const profileName = `che:security-http-${process.pid}`;
const runId = process.env.CONDITIONAL_INTEGRATION_RUN_ID ?? String(process.pid);
const profileName = `che:security-http-${runId}`;
const userId = `security-http-user-${process.pid}`;
const generalId = 990_001;
const secret = 'security-http-e2e-secret';
@@ -56,6 +57,18 @@ const restoreEnv = (): void => {
}
};
const deleteProfileRedisKeys = async (): Promise<void> => {
if (!redis) {
return;
}
for await (const key of redis.client.scanIterator({
MATCH: `sammo:game:*:${profileName}:*`,
COUNT: 100,
})) {
await redis.client.del(key);
}
};
const buildPayload = (suffix: string, sanctions: GameSessionTokenPayload['sanctions']): GameSessionTokenPayload => ({
version: 1,
profile: profileName,
@@ -144,6 +157,7 @@ integration('game API security over HTTP transport', () => {
await server?.app.close();
await db?.general.deleteMany({ where: { id: generalId } });
await disconnectDb?.();
await deleteProfileRedisKeys();
await redis?.disconnect();
if (uploadDir) {
await fs.rm(uploadDir, { recursive: true, force: true });
+21 -6
View File
@@ -15,8 +15,14 @@ pnpm test:integration:conditional
```
조건부 runner는 환경 변수 존재 여부만으로 안전성을 보장하지 않습니다.
대상 host, port, database, schema와 Redis prefix가 해당 worktree 전용인지
확인해 주세요.
대상 host, port, database와 Redis prefix가 해당 worktree 전용인지 확인해
주세요. Runner는 실행 ID가 포함된 game schema를 생성하고 성공, 테스트 실패,
`HUP`, `INT`, `TERM` 종료에서 자신이 생성한 schema만 삭제합니다. 사용자가
schema 환경 변수를 지정한 경우에도 이미 존재하는 schema는 거부합니다.
HTTP transport fixture의 Redis key도 실행 ID를 profile namespace에 포함하고,
runner 종료 시 그 실행 ID에 속한 key만 삭제합니다. 공유 Redis 전체에
`FLUSHDB``FLUSHALL`을 실행하지 않습니다.
`SIGKILL`은 cleanup trap을 실행할 수 없으므로 자동 정리를 보장하지 않습니다.
## 준비
@@ -61,14 +67,19 @@ runtime role을 삭제하고 PID와 명령행 및 daemon 종료를 확인한 뒤
경매 timer race
실제 포함 suite는 `tools/run-conditional-integration.sh`, 각 package의
`package.json`, `*.integration.test.ts`를 기준으로 확인합니다.
`package.json`, `*.integration.test.ts`를 기준으로 확인합니다. DB 조건부
환경 변수는 `tools/conditional-integration-registry.tsv`에서 명시적으로
관리합니다. 새 `*_DATABASE_URL` gate가 registry에 없거나 registry 항목이
더 이상 테스트에 존재하지 않으면 runner가 테스트 실행 전에 실패합니다.
관리자 시간 조정의 PostgreSQL 경계는
`runtimeClockShiftPersistence.integration.test.ts`, gateway action
`PARTIAL → APPLIED` 경계는 `gatewayRuntimeAction.integration.test.ts`입니다.
후자 `GATEWAY_RUNTIME_ACTION_DATABASE_URL`, 전자
`INPUT_EVENT_DATABASE_URL`이 없으면 skip되므로 결과에서 실제 실행 여부를
따로 확인해 주세요.
Runner는 후자 `GATEWAY_RUNTIME_ACTION_DATABASE_URL`, 전자
`INPUT_EVENT_DATABASE_URL`을 같은 격리 schema URL로 주입합니다. 장수 생성,
선택 pool, 즉시 장수 action처럼 별도 marker를 사용하는 테스트도 registry를
통해 포함합니다. DB와 Redis가 함께 필요한 파일은 DB group에서 한 번만
실행하고 Redis-only 파일만 별도 group에서 실행합니다.
## 안전 경계
@@ -85,5 +96,9 @@ runtime role을 삭제하고 PID와 명령행 및 daemon 종료를 확인한 뒤
PostgreSQL·Redis 경계를 증명하지 않습니다. Full suite 실패는 변경 worktree와
변경 없는 `main`에서 각각 재현해 회귀와 baseline을 구분합니다.
조건부 runner는 마지막 줄에 통과, skip, 실패한 test file 수를 출력하며,
하나라도 file 전체가 skip되면 성공으로 처리하지 않습니다. 실행이 중단된 경우도
이미 완료된 group의 집계와 schema cleanup 결과를 확인해 주세요.
Ref 호환성 판정은 [차등 검증](architecture/turn-state-differential-testing.md),
UI는 [프론트엔드 호환 검증](frontend-legacy-parity.md)을 함께 사용합니다.
@@ -0,0 +1,13 @@
# Environment variable Execution mode
CREATE_GENERAL_DATABASE_URL create_general
GATEWAY_RUNTIME_ACTION_DATABASE_URL gateway_runtime
GENERAL_LIFECYCLE_DATABASE_URL core
IMMEDIATE_ACTION_DATABASE_URL immediate_action
INPUT_EVENT_DATABASE_URL core
LIVE_SORTIE_PERSISTENCE_DATABASE_URL reference_live_sortie
NPC_POSSESSION_DATABASE_URL npc_possession
NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL reference_npc_possession
RESERVED_TURN_DATABASE_URL core
SELECT_POOL_DATABASE_URL select_pool
TURN_DAEMON_LEASE_DATABASE_URL core
TURN_DIFFERENTIAL_DATABASE_URL core
1 # Environment variable Execution mode
2 CREATE_GENERAL_DATABASE_URL create_general
3 GATEWAY_RUNTIME_ACTION_DATABASE_URL gateway_runtime
4 GENERAL_LIFECYCLE_DATABASE_URL core
5 IMMEDIATE_ACTION_DATABASE_URL immediate_action
6 INPUT_EVENT_DATABASE_URL core
7 LIVE_SORTIE_PERSISTENCE_DATABASE_URL reference_live_sortie
8 NPC_POSSESSION_DATABASE_URL npc_possession
9 NPC_POSSESSION_DIFFERENTIAL_DATABASE_URL reference_npc_possession
10 RESERVED_TURN_DATABASE_URL core
11 SELECT_POOL_DATABASE_URL select_pool
12 TURN_DAEMON_LEASE_DATABASE_URL core
13 TURN_DIFFERENTIAL_DATABASE_URL core
+355 -62
View File
@@ -10,12 +10,17 @@ 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
@@ -32,20 +37,46 @@ set +a
: "${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required in $env_file}"
: "${REDIS_URL:?REDIS_URL is required in $env_file}"
integration_schema=${CONDITIONAL_INTEGRATION_SCHEMA:-conditional_integration}
scenario_schema=${SCENARIO_SEED_INTEGRATION_SCHEMA:-conditional_scenario_seed}
npc_possession_schema=${NPC_POSSESSION_INTEGRATION_SCHEMA:-conditional_$(date +%s)_$$_npc_possession_integration}
npc_possession_differential_schema=conditional_$(date +%s)_$$_npc_possession_differential
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
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" "$npc_possession_differential_schema"; do
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"
owned_schemas=
active_process_group=
build_database_url() {
SCHEMA_NAME=$1 node --input-type=module -e '
const required = [
@@ -72,29 +103,260 @@ build_database_url() {
'
}
base_database_url=$(build_database_url public)
create_owned_schema() {
schema=$1
SCHEMA_NAME=$schema 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 });
await client.connect();
try {
await client.query(`CREATE SCHEMA "${process.env.SCHEMA_NAME}"`);
} finally {
await client.end();
}
'
owned_schemas="${owned_schemas}${schema}
"
}
drop_owned_schemas() {
cleanup_failed=0
for schema in $owned_schemas; do
if ! SCHEMA_NAME=$schema 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 });
await client.connect();
try {
await client.query(`DROP SCHEMA IF EXISTS "${process.env.SCHEMA_NAME}" CASCADE`);
} finally {
await client.end();
}
' >/dev/null; then
cleanup_failed=1
fi
done
return "$cleanup_failed"
}
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}:*`,
];
await client.connect();
try {
for (const pattern of patterns) {
for await (const key of client.scanIterator({ MATCH: pattern, COUNT: 100 })) {
await client.del(key);
}
}
} finally {
await client.quit();
}
' >/dev/null
}
terminate_active_process_group() {
[ -n "$active_process_group" ] || return 0
kill -TERM "-$active_process_group" 2>/dev/null || true
wait "$active_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 ! 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
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"
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/game-api/test \
app/game-engine/test \
tools/integration-tests/test \
-g '*.integration.test.ts' |
sed 's/process\.env\.//' |
sort -u
) >"$discovered_file"
awk -F '\t' '!/^#/ && NF == 2 { print $1 }' "$registry_file" | sort -u >"$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 }' "$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
test_files=$(cd "$workspace_root/$package_dir" && rg -l "$marker" test -g '*.test.ts' | sort)
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
echo "running $label tests in $package_dir"
(
cd "$workspace_root/$package_dir"
printf '%s\n' "$test_files" |
xargs -r pnpm exec vitest run --no-file-parallelism --maxWorkers=1
)
# 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"
pnpm install --frozen-lockfile
pnpm --filter @sammo-ts/infra prisma:generate
pnpm --filter @sammo-ts/common build
pnpm --filter @sammo-ts/infra build
validate_marker_registry
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
@@ -106,24 +368,74 @@ export RESERVED_TURN_DATABASE_URL=$database_url
pnpm --filter @sammo-ts/infra prisma:db:push:game
database_markers='INPUT_EVENT_DATABASE_URL|GENERAL_LIFECYCLE_DATABASE_URL|TURN_DAEMON_LEASE_DATABASE_URL|RESERVED_TURN_DATABASE_URL'
run_marked_tests app/game-api "$database_markers" "PostgreSQL"
run_marked_tests app/game-engine "$database_markers" "PostgreSQL"
run_marked_tests tools/integration-tests \
'TURN_DIFFERENTIAL_DATABASE_URL|INPUT_EVENT_DATABASE_URL' \
"PostgreSQL snapshot"
core_database_markers=$(markers_for_mode core)
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-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:db:push:gateway
)
export GATEWAY_RUNTIME_ACTION_DATABASE_URL=$gateway_runtime_database_url
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
export NPC_POSSESSION_DATABASE_URL=$npc_possession_database_url
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
run_marked_tests app/game-api \
'NPC_POSSESSION_DATABASE_URL' \
"NPC possession PostgreSQL"
)
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/.."}
@@ -161,64 +473,45 @@ if [ "${TURN_DIFFERENTIAL_REFERENCE:-}" = "1" ]; 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")
(
cleanup_npc_possession_differential_schema() {
printf 'DROP SCHEMA IF EXISTS "%s" CASCADE;\n' "$npc_possession_differential_schema" |
DATABASE_URL=$npc_possession_differential_database_url \
pnpm --filter @sammo-ts/infra exec prisma db execute --stdin >/dev/null
}
handle_npc_possession_differential_exit() {
exit_status=$?
trap - EXIT HUP INT TERM
if ! cleanup_npc_possession_differential_schema && [ "$exit_status" -eq 0 ]; then
exit_status=1
fi
exit "$exit_status"
}
trap handle_npc_possession_differential_exit EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
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
export TURN_DIFFERENTIAL_WORKSPACE_ROOT=$reference_workspace_root
export TURN_DIFFERENTIAL_STACK_DIR=$reference_stack
pnpm --filter @sammo-ts/infra prisma:db:push:game
cd "$workspace_root/tools/integration-tests"
pnpm exec vitest run --no-file-parallelism --maxWorkers=1 \
test/npcPossessionSelectionReference.integration.test.ts
)
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"
live_sortie_schema=${LIVE_SORTIE_PERSISTENCE_SCHEMA:-conditional_live_sortie_persistence}
case "$live_sortie_schema" in
''|[!a-z_]*|*[!a-z0-9_]*)
echo "live sortie persistence schema must be a lowercase PostgreSQL identifier" >&2
exit 64
;;
esac
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
pnpm --filter @sammo-ts/infra prisma:db:push:game
run_marked_tests tools/integration-tests \
'LIVE_SORTIE_PERSISTENCE_DATABASE_URL' \
"live sortie PostgreSQL persistence"
"$(markers_for_mode reference_live_sortie)" \
"live_sortie_postgresql"
export POSTGRES_SCHEMA=$integration_schema
export DATABASE_URL=$database_url
fi
run_marked_tests app/game-api 'process\.env\.REDIS_URL' "Redis"
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
cd "$workspace_root/app/game-engine"
pnpm exec vitest run --no-file-parallelism --maxWorkers=1 test/scenarioSeeder.test.ts
)
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"