test: add PostgreSQL migration integration coverage #4

Merged
Zayin_Admin merged 1 commits from phase-3/migration-integration-tests into main 2026-07-21 14:50:20 +00:00
16 changed files with 1690 additions and 96 deletions
+24
View File
@@ -47,3 +47,27 @@ jobs:
- name: Exercise PostgreSQL and S3 infrastructure - name: Exercise PostgreSQL and S3 infrastructure
run: bash scripts/smoke-infrastructure.sh run: bash scripts/smoke-infrastructure.sh
migration-smoke:
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .node-version
- name: Activate pinned pnpm
run: |
corepack enable
corepack prepare [email protected] --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Exercise transactional migrations against PostgreSQL
run: pnpm test:migrations:integration
+9
View File
@@ -9,6 +9,15 @@ Before opening a change, run:
pnpm verify pnpm verify
``` ```
Migration-runner changes also require the disposable live PostgreSQL suite:
```bash
pnpm test:migrations:integration
```
See the [testing guide](docs/development/testing.md) for the distinction between unit and live
evidence and for credential-safe troubleshooting.
When adding or changing a module manifest, also run `pnpm modules:validate` directly for focused, When adding or changing a module manifest, also run `pnpm modules:validate` directly for focused,
path-aware discovery errors. It is included in `pnpm verify`. path-aware discovery errors. It is included in `pnpm verify`.
+9 -5
View File
@@ -13,9 +13,10 @@ contracts, deterministic schema aggregation, and a cloneable backend-only module
artifacts now have module-owned Drizzle contracts, deterministic offline discovery, explicit artifacts now have module-owned Drizzle contracts, deterministic offline discovery, explicit
destructive-change declarations, and exact checksums. Core now provides read-only applied-history destructive-change declarations, and exact checksums. Core now provides read-only applied-history
planning and a locked, whole-batch transactional PostgreSQL runner with exact external destructive planning and a locked, whole-batch transactional PostgreSQL runner with exact external destructive
approval matching. Live PostgreSQL runner integration coverage, production approval-provider approval matching. A hermetic live PostgreSQL suite now proves history, rollback, statement
composition, identity, HTTP routing, jobs, physical storage provisioning, surfaces, and feature breakpoints, advisory-lock concurrency and release, exact destructive approvals, and sanitization
modules remain later vertical slices. against the production adapter. Production approval-provider composition, identity, HTTP routing,
jobs, physical storage provisioning, surfaces, and feature modules remain later vertical slices.
## Quick start ## Quick start
@@ -28,6 +29,7 @@ pnpm infrastructure:up
pnpm modules:validate pnpm modules:validate
pnpm migrations:validate pnpm migrations:validate
pnpm migrations:plan pnpm migrations:plan
pnpm test:migrations:integration
pnpm verify pnpm verify
``` ```
@@ -36,8 +38,9 @@ pnpm verify
advisory lock and one transaction. Review the plan before applying. Destructive migrations are advisory lock and one transaction. Review the plan before applying. Destructive migrations are
blocked by default and require an exact externally supplied approval; there is no force flag. blocked by default and require an exact externally supplied approval; there is no force flag.
The runner currently has deterministic unit coverage through a database port. Live PostgreSQL The runner has both deterministic unit coverage through a narrow database port and a separate live
migration-runner tests are deferred and are not yet claimed by `pnpm verify`. PostgreSQL integration suite. The live command creates and destroys disposable Docker
infrastructure; it remains separate from the Docker-free `pnpm verify` gate.
Run `pnpm infrastructure:smoke` for an isolated end-to-end PostgreSQL and authenticated S3 check. Run `pnpm infrastructure:smoke` for an isolated end-to-end PostgreSQL and authenticated S3 check.
@@ -54,6 +57,7 @@ environment.
- [Platform architecture](docs/architecture/platform-architecture.md) - [Platform architecture](docs/architecture/platform-architecture.md)
- [Repository boundaries](docs/architecture/repository-boundaries.md) - [Repository boundaries](docs/architecture/repository-boundaries.md)
- [Repository workflow](docs/development/repository-workflow.md) - [Repository workflow](docs/development/repository-workflow.md)
- [Testing](docs/development/testing.md)
- [Module development](docs/module-development/README.md) - [Module development](docs/module-development/README.md)
- [Architecture decisions](docs/architecture/decisions/README.md) - [Architecture decisions](docs/architecture/decisions/README.md)
File diff suppressed because it is too large Load Diff
@@ -122,10 +122,14 @@ messages, machine-specific paths, causes, stacks, or partial execution results.
are reported without hiding the primary failure and without copying underlying exception text. are reported without hiding the primary failure and without copying underlying exception text.
The developer commands read `DATABASE_URL` from the environment. A local uncommitted `.env` may The developer commands read `DATABASE_URL` from the environment. A local uncommitted `.env` may
supply it, but commands never print it. This slice verifies database behavior through a narrow supply it, but commands never print it. This decision's implementation slice verified database
in-memory database port and unit tests. A real PostgreSQL integration suite for lock contention, behavior through a narrow in-memory database port and unit tests; it deliberately deferred live
DDL rollback, history creation, and driver behavior is deliberately deferred rather than claimed PostgreSQL evidence to a follow-up slice rather than claiming it at acceptance time.
by the current verification gate.
Implementation follow-up (2026-07-21): `pnpm test:migrations:integration` now runs the production
adapter and migration services against disposable PostgreSQL for lock contention, DDL rollback,
history creation, approvals, and sanitized driver failures. The Docker-backed command remains
separate from the code-only `pnpm verify` gate.
The transaction and single-client choices follow node-postgres's The transaction and single-client choices follow node-postgres's
[transaction guidance](https://node-postgres.com/features/transactions). The non-blocking lock and [transaction guidance](https://node-postgres.com/features/transactions). The non-blocking lock and
@@ -162,6 +166,6 @@ that invocation's schema or history changes committed. Destructive migrations ar
a trusted approval provider is composed, by design. a trusted approval provider is composed, by design.
The all-or-nothing batch can hold locks for longer than per-migration transactions and excludes The all-or-nothing batch can hold locks for longer than per-migration transactions and excludes
operations PostgreSQL forbids in a transaction. The current unit-tested database port gives fast, operations PostgreSQL forbids in a transaction. The narrow database port continues to provide fast,
deterministic coverage but cannot prove real server behavior. The next infrastructure-focused deterministic unit coverage; the dated implementation follow-up above records the separate live
slice must add live PostgreSQL tests before production operation is considered complete. PostgreSQL evidence added after this decision was accepted.
+3 -2
View File
@@ -69,8 +69,9 @@ validated metadata, explicit destructive-change declarations, and exact SHA-256
now compare that inventory with its fixed PostgreSQL history table, produce a read-only plan, and now compare that inventory with its fixed PostgreSQL history table, produce a read-only plan, and
apply the complete pending batch under a session advisory lock and one transaction. Destructive apply the complete pending batch under a session advisory lock and one transaction. Destructive
migrations require an exact externally supplied approval bound to module, identity, and checksum. migrations require an exact externally supplied approval bound to module, identity, and checksum.
Hermetic integration coverage now exercises those contracts through the production adapter against
the pinned PostgreSQL container, including real DDL rollback and advisory-lock concurrency.
Target architecture, not yet implemented: production approval-provider composition, live Target architecture, not yet implemented: production approval-provider composition, Better Auth,
PostgreSQL migration-runner integration coverage, Better Auth,
permission enforcement, physical storage-bucket provisioning, Surface SDK, richer public resource permission enforcement, physical storage-bucket provisioning, Surface SDK, richer public resource
and action contracts, API server, dashboard, Builder, and reference feature modules. and action contracts, API server, dashboard, Builder, and reference feature modules.
+18 -6
View File
@@ -24,6 +24,7 @@ pnpm migrations:plan # read-only comparison with PostgreSQL applied history
pnpm migrations:apply # locked, transactional application of the pending batch pnpm migrations:apply # locked, transactional application of the pending batch
pnpm typecheck # strict TypeScript pnpm typecheck # strict TypeScript
pnpm test # Vitest pnpm test # Vitest
pnpm test:migrations:integration # production runner against disposable PostgreSQL
pnpm build # Turborepo package build graph pnpm build # Turborepo package build graph
pnpm verify # every code-only gate; never plan or apply pnpm verify # every code-only gate; never plan or apply
``` ```
@@ -63,13 +64,24 @@ The default command composition supplies no destructive approvals. There is no `
or source-controlled approval escape hatch; deployments must later compose a trusted external or source-controlled approval escape hatch; deployments must later compose a trusted external
provider that supplies an exact module ID, migration identity, and checksum triple. provider that supplies an exact module ID, migration identity, and checksum triple.
The migration runner currently has comprehensive unit coverage through its narrow database port. Migration-runner unit tests cover orchestration through its narrow database port. Run the separate
Live PostgreSQL tests for real DDL rollback, advisory-lock contention, history-table behavior, and live suite when changing migration behavior:
driver cleanup are deliberately deferred and are not part of `pnpm verify` or CI yet.
CI runs `pnpm ci:verify`, which adds Compose configuration validation to `pnpm verify`. A separate ```bash
CI job runs the live infrastructure smoke test so code-only local verification does not require pnpm test:migrations:integration
Docker. The explicit suffix avoids pnpm's built-in `ci` install command. ```
It requires Docker with Compose, starts the pinned PostgreSQL image with a generated throwaway
credential and a Docker-assigned loopback port, creates an isolated database for each scenario,
and removes its containers, network, databases, and volume afterward. It exercises the production
adapter and real PostgreSQL transactions, history, advisory locks, approvals, and sanitized errors.
It remains outside the Docker-free `pnpm verify` command.
CI runs `pnpm ci:verify`, which adds Compose configuration validation to `pnpm verify`. Separate
`infrastructure-smoke` and `migration-smoke` jobs run the live service and migration suites. The
exact pull-request context to add to `main` branch protection after merge is
`CI / migration-smoke (pull_request)`. The explicit `ci:verify` suffix avoids pnpm's built-in `ci`
install command.
## Development infrastructure ## Development infrastructure
+60
View File
@@ -0,0 +1,60 @@
# Testing
Ekklesia separates fast code-level evidence from live infrastructure evidence.
## Unit and repository verification
```bash
pnpm test
pnpm verify
```
These commands exercise package tests, repository boundaries, type checking, offline module and
migration discovery, formatting, linting, and builds. Migration-runner unit tests use a narrow
database/session port to prove deterministic planning, approval, transaction orchestration,
cleanup, and sanitized errors. They do not claim to prove PostgreSQL behavior.
`pnpm verify` is intentionally Docker-free. It never plans or applies migrations against a live
database.
## Live PostgreSQL migration evidence
Docker and the Docker Compose plugin are required for the live suite:
```bash
pnpm test:migrations:integration
```
The command starts the repository's pinned PostgreSQL image in a unique Compose project, waits for
bounded health readiness, runs the production PostgreSQL adapter and migration services, and then
removes the container, network, and volume. Each scenario also receives a safely named disposable
database. Direct host runs use a Docker-assigned random port bound only to `127.0.0.1`; a
containerized Gitea job joins the isolated Compose network and uses the PostgreSQL service name.
The live suite proves fresh read-only planning, initial and incremental application, genuine
no-op behavior, exact applied checksums and history, edited or corrupt history rejection,
whole-batch DDL rollback, Drizzle breakpoint execution, real advisory-lock concurrency and
timeouts, lock release after failure, exact destructive approvals, and public-error sanitization.
It does not make migrations automatic during Core startup.
CI runs the same command in the dedicated `migration-smoke` job. After this change is merged, add
the following pull-request status context to `main` branch protection:
```text
CI / migration-smoke (pull_request)
```
## Safe troubleshooting
Confirm the prerequisites without printing configuration:
```bash
docker info
docker compose version
```
Then rerun `pnpm test:migrations:integration`. The harness emits fixed failure messages and does
not dump PostgreSQL logs, SQL, container inspection output, or the generated database URL. Do not
enable query logging, print the test environment, run `docker compose config` without redaction,
or paste Docker inspection output into an issue. If the Docker daemon itself stopped during
cleanup, restore it before rerunning so the harness can create a fresh uniquely named project.
+5 -3
View File
@@ -275,9 +275,11 @@ ID, exact identity, and exact checksum. Module metadata cannot grant approval, a
wildcard, force, or approve-all option. The default developer command supplies no approvals and wildcard, force, or approve-all option. The default developer command supplies no approvals and
therefore refuses destructive pending migrations. therefore refuses destructive pending migrations.
Runner behavior is unit-tested through a narrow database port. Live PostgreSQL coverage of DDL Runner behavior is unit-tested through a narrow database port. The separate
rollback, advisory-lock contention, and driver behavior is a deferred infrastructure slice; the `pnpm test:migrations:integration` command now exercises the production adapter against disposable
current verification suite does not claim that coverage. PostgreSQL, including DDL rollback, advisory-lock contention, history behavior, approvals, and
sanitized failures. It remains outside the Docker-free `pnpm verify` gate; see the
[testing guide](../development/testing.md).
Core completes discovery before importing any server entrypoint. It then loads and verifies every Core completes discovery before importing any server entrypoint. It then loads and verifies every
runtime definition before running hooks, registers all modules dependency-first into an unpublished runtime definition before running hooks, registers all modules dependency-first into an unpublished
+1
View File
@@ -27,6 +27,7 @@
"migrations:validate": "turbo run build && node core/dist/cli/validate-migrations.js", "migrations:validate": "turbo run build && node core/dist/cli/validate-migrations.js",
"modules:validate": "turbo run build && node core/dist/cli/validate-modules.js", "modules:validate": "turbo run build && node core/dist/cli/validate-modules.js",
"test": "turbo run test test:root", "test": "turbo run test test:root",
"test:migrations:integration": "bash scripts/test-migrations-integration.sh",
"test:root": "vitest run --config vitest.config.ts", "test:root": "vitest run --config vitest.config.ts",
"test:watch": "vitest", "test:watch": "vitest",
"typecheck": "turbo run typecheck typecheck:root", "typecheck": "turbo run typecheck typecheck:root",
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=false
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER=""
COMPOSE_JOB_NETWORK_NAME=""
COMPOSE_JOB_NETWORK_CONNECTED=false
compose_job_network_initialize() {
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=false
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER=""
COMPOSE_JOB_NETWORK_NAME=""
COMPOSE_JOB_NETWORK_CONNECTED=false
if [[ ! -f /.dockerenv ]]; then
return 0
fi
if [[ -z "${HOSTNAME:-}" ]]; then
return 1
fi
COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER=true
COMPOSE_JOB_NETWORK_CURRENT_CONTAINER="$HOSTNAME"
}
compose_job_network_connect() {
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" != "true" ]]; then
return 1
fi
if (($# == 0)); then
return 1
fi
local service="$1"
shift
if [[ -z "$service" || $# -eq 0 ]]; then
return 1
fi
local service_container_output
if ! service_container_output="$("$@" ps -q "$service" 2>/dev/null)"; then
return 1
fi
local service_containers=()
while IFS= read -r container_id; do
if [[ -n "$container_id" ]]; then
service_containers+=("$container_id")
fi
done <<<"$service_container_output"
if ((${#service_containers[@]} != 1)); then
return 1
fi
local service_network_output
if ! service_network_output="$(
docker inspect \
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
"${service_containers[0]}" 2>/dev/null
)"; then
return 1
fi
local service_networks=()
while IFS= read -r network_name; do
if [[ -n "$network_name" ]]; then
service_networks+=("$network_name")
fi
done <<<"$service_network_output"
if ((${#service_networks[@]} != 1)); then
return 1
fi
COMPOSE_JOB_NETWORK_NAME="${service_networks[0]}"
local current_network_output
if ! current_network_output="$(
docker inspect \
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" 2>/dev/null
)"; then
return 1
fi
while IFS= read -r network_name; do
if [[ "$network_name" == "$COMPOSE_JOB_NETWORK_NAME" ]]; then
COMPOSE_JOB_NETWORK_CONNECTED=true
return 0
fi
done <<<"$current_network_output"
COMPOSE_JOB_NETWORK_CONNECTED=true
if ! docker network connect \
"$COMPOSE_JOB_NETWORK_NAME" \
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1; then
docker network disconnect \
"$COMPOSE_JOB_NETWORK_NAME" \
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1 || true
COMPOSE_JOB_NETWORK_CONNECTED=false
return 1
fi
}
compose_job_network_disconnect() {
if [[ "$COMPOSE_JOB_NETWORK_CONNECTED" != "true" ]]; then
return 0
fi
if ! docker network disconnect \
"$COMPOSE_JOB_NETWORK_NAME" \
"$COMPOSE_JOB_NETWORK_CURRENT_CONTAINER" >/dev/null 2>&1; then
return 1
fi
COMPOSE_JOB_NETWORK_CONNECTED=false
}
+10 -55
View File
@@ -5,6 +5,9 @@ set -Eeuo pipefail
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repository_root" cd "$repository_root"
# shellcheck source=scripts/lib/compose-job-network.sh
source "$repository_root/scripts/lib/compose-job-network.sh"
command -v docker >/dev/null || { command -v docker >/dev/null || {
echo "Docker is required for the infrastructure smoke test." >&2 echo "Docker is required for the infrastructure smoke test." >&2
exit 1 exit 1
@@ -50,17 +53,9 @@ compose=(
--env-file .env.example --env-file .env.example
--file infrastructure/compose.yaml --file infrastructure/compose.yaml
) )
running_in_container=false if ! compose_job_network_initialize; then
current_container="" echo "The current Docker job container could not be identified." >&2
compose_network=""
connected_to_compose_network=false
if [[ -f /.dockerenv ]]; then
running_in_container=true
if [[ -z "${HOSTNAME:-}" ]]; then
echo "HOSTNAME is required when the smoke test runs inside a Docker container." >&2
exit 1 exit 1
fi
current_container="$HOSTNAME"
fi fi
cleanup() { cleanup() {
@@ -70,17 +65,12 @@ cleanup() {
if ((exit_status != 0)); then if ((exit_status != 0)); then
"${compose[@]}" logs --no-color >&2 || true "${compose[@]}" logs --no-color >&2 || true
fi fi
if [[ "$connected_to_compose_network" == "true" ]]; then if ! compose_job_network_disconnect; then
if ! cleanup_output="$( echo "Infrastructure smoke network cleanup failed." >&2
docker network disconnect "$compose_network" "$current_container" 2>&1
)"; then
echo "Infrastructure smoke network cleanup failed:" >&2
echo "$cleanup_output" >&2
if ((exit_status == 0)); then if ((exit_status == 0)); then
exit_status=1 exit_status=1
fi fi
fi fi
fi
if ! cleanup_output="$( if ! cleanup_output="$(
"${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1 "${compose[@]}" down --volumes --remove-orphans --timeout 10 2>&1
)"; then )"; then
@@ -133,46 +123,11 @@ if "${compose[@]}" exec -T \
exit 1 exit 1
fi fi
if [[ "$running_in_container" == "true" ]]; then if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" == "true" ]]; then
object_storage_container="$("${compose[@]}" ps -q object-storage)" if ! compose_job_network_connect object-storage "${compose[@]}"; then
if [[ -z "$object_storage_container" ]]; then echo "The infrastructure smoke job could not join the isolated storage network." >&2
echo "Compose did not return the object-storage container ID." >&2
exit 1 exit 1
fi fi
network_output="$(
docker inspect \
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
"$object_storage_container"
)"
object_storage_networks=()
while IFS= read -r network_name; do
if [[ -n "$network_name" ]]; then
object_storage_networks+=("$network_name")
fi
done <<<"$network_output"
if ((${#object_storage_networks[@]} != 1)); then
echo "Expected object-storage to use exactly one Compose network." >&2
exit 1
fi
compose_network="${object_storage_networks[0]}"
current_network_output="$(
docker inspect \
--format '{{range $networkName, $_ := .NetworkSettings.Networks}}{{println $networkName}}{{end}}' \
"$current_container"
)"
already_connected=false
while IFS= read -r network_name; do
if [[ "$network_name" == "$compose_network" ]]; then
already_connected=true
break
fi
done <<<"$current_network_output"
if [[ "$already_connected" != "true" ]]; then
docker network connect "$compose_network" "$current_container"
connected_to_compose_network=true
fi
s3_endpoint="http://object-storage:8333" s3_endpoint="http://object-storage:8333"
else else
s3_binding="$("${compose[@]}" port object-storage 8333)" s3_binding="$("${compose[@]}" port object-storage 8333)"
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repository_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repository_root"
# shellcheck source=scripts/lib/compose-job-network.sh
source "$repository_root/scripts/lib/compose-job-network.sh"
command -v docker >/dev/null || {
echo "Docker is required for the PostgreSQL migration integration suite." >&2
exit 1
}
command -v pnpm >/dev/null || {
echo "pnpm is required for the PostgreSQL migration integration suite." >&2
exit 1
}
if ! docker compose version >/dev/null 2>&1; then
echo "Docker Compose is required for the PostgreSQL migration integration suite." >&2
exit 1
fi
if ! compose_job_network_initialize; then
echo "The current Docker job container could not be identified." >&2
exit 1
fi
raw_project="ekklesia-migration-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ATTEMPT:-0}-${GITHUB_JOB:-job}-$$"
project="${raw_project//[^a-zA-Z0-9_-]/-}"
project="${project,,}"
export POSTGRES_DB="ekklesia_migration_test"
export POSTGRES_USER="ekklesia_migration_test"
export POSTGRES_PASSWORD="temporary-$project-${RANDOM}-${RANDOM}"
export POSTGRES_PORT="0"
compose=(
docker compose
--project-name "$project"
--env-file .env.example
--file infrastructure/compose.yaml
)
cleanup() {
local exit_status=$?
trap - EXIT INT TERM
if ! compose_job_network_disconnect; then
echo "PostgreSQL migration test network cleanup failed." >&2
if ((exit_status == 0)); then
exit_status=1
fi
fi
if ! "${compose[@]}" down --volumes --remove-orphans --timeout 10 >/dev/null 2>&1; then
echo "PostgreSQL migration test infrastructure cleanup failed." >&2
if ((exit_status == 0)); then
exit_status=1
fi
fi
exit "$exit_status"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! "${compose[@]}" config --quiet >/dev/null 2>&1; then
echo "PostgreSQL migration test infrastructure configuration is invalid." >&2
exit 1
fi
if ! "${compose[@]}" up --detach --wait --wait-timeout 120 postgres >/dev/null 2>&1; then
echo "Disposable PostgreSQL did not become ready." >&2
exit 1
fi
if [[ "$COMPOSE_JOB_NETWORK_RUNNING_IN_CONTAINER" == "true" ]]; then
if ! compose_job_network_connect postgres "${compose[@]}"; then
echo "The migration test job could not join the isolated PostgreSQL network." >&2
exit 1
fi
database_host="postgres"
database_port="5432"
else
if ! postgres_binding="$("${compose[@]}" port postgres 5432 2>/dev/null)"; then
echo "The disposable PostgreSQL loopback port could not be determined." >&2
exit 1
fi
if [[ ! "$postgres_binding" =~ ^127\.0\.0\.1:([0-9]+)$ ]]; then
echo "The disposable PostgreSQL port is not bound safely to loopback." >&2
exit 1
fi
database_host="127.0.0.1"
database_port="${BASH_REMATCH[1]}"
if ((database_port < 1 || database_port > 65535)); then
echo "The disposable PostgreSQL loopback port is invalid." >&2
exit 1
fi
fi
EKKLESIA_MIGRATION_TEST_DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$database_host:$database_port/$POSTGRES_DB" \
pnpm exec vitest run --config vitest.migrations.integration.config.ts
+70 -14
View File
@@ -21,6 +21,17 @@ interface PackageDocument {
scripts?: Record<string, string>; scripts?: Record<string, string>;
} }
interface WorkflowJob {
needs?: string;
steps?: WorkflowStep[];
"timeout-minutes"?: number;
}
interface WorkflowStep {
run?: string;
uses?: string;
}
interface TurboDocument { interface TurboDocument {
tasks?: Record<string, unknown>; tasks?: Record<string, unknown>;
} }
@@ -64,19 +75,28 @@ describe("development infrastructure", () => {
"utf8", "utf8",
); );
const workflow = parse(source) as { const workflow = parse(source) as {
jobs?: { jobs?: Record<string, WorkflowJob>;
"infrastructure-smoke"?: { steps?: Array<{ run?: string }> }; name?: string;
verify?: { steps?: Array<{ run?: string }> };
}; };
}; const verify = workflow.jobs?.verify;
const commands = workflow.jobs?.verify?.steps?.flatMap((step) => step.run ?? []) ?? []; const infrastructure = workflow.jobs?.["infrastructure-smoke"];
const infrastructureCommands = const migration = workflow.jobs?.["migration-smoke"];
workflow.jobs?.["infrastructure-smoke"]?.steps?.flatMap((step) => step.run ?? []) ?? []; const commands = verify?.steps?.flatMap((step) => step.run ?? []) ?? [];
const infrastructureCommands = infrastructure?.steps?.flatMap((step) => step.run ?? []) ?? [];
const migrationCommands = migration?.steps?.flatMap((step) => step.run ?? []) ?? [];
const verifyActions = verify?.steps?.flatMap((step) => step.uses ?? []) ?? [];
const migrationActions = migration?.steps?.flatMap((step) => step.uses ?? []) ?? [];
expect(workflow.jobs?.verify).toBeDefined(); expect(workflow.name).toBe("CI");
expect(verify).toBeDefined();
expect(commands).toContain("pnpm install --frozen-lockfile"); expect(commands).toContain("pnpm install --frozen-lockfile");
expect(commands).toContain("pnpm ci:verify"); expect(commands).toContain("pnpm ci:verify");
expect(infrastructureCommands).toContain("bash scripts/smoke-infrastructure.sh"); expect(infrastructureCommands).toContain("bash scripts/smoke-infrastructure.sh");
expect(migration?.needs).toBe("verify");
expect(migration?.["timeout-minutes"]).toBe(15);
expect(migrationActions).toEqual(verifyActions);
expect(migrationCommands).toContain("pnpm install --frozen-lockfile");
expect(migrationCommands).toContain("pnpm test:migrations:integration");
}); });
it("routes containerized S3 smoke checks through the isolated Compose network", async () => { it("routes containerized S3 smoke checks through the isolated Compose network", async () => {
@@ -84,18 +104,54 @@ describe("development infrastructure", () => {
path.join(repositoryRoot, "scripts", "smoke-infrastructure.sh"), path.join(repositoryRoot, "scripts", "smoke-infrastructure.sh"),
"utf8", "utf8",
); );
const helper = await readFile(
path.join(repositoryRoot, "scripts", "lib", "compose-job-network.sh"),
"utf8",
);
expect(source).toContain("[[ -f /.dockerenv ]]"); expect(source).toContain('source "$repository_root/scripts/lib/compose-job-network.sh"');
expect(source).toContain(`object_storage_container="$("\${compose[@]}" ps -q object-storage)"`); expect(source).toContain(`compose_job_network_connect object-storage "\${compose[@]}"`);
expect(source).toContain("docker inspect");
expect(source).toContain('docker network connect "$compose_network" "$current_container"');
expect(source).toContain('docker network disconnect "$compose_network" "$current_container"');
expect(source).toContain('s3_endpoint="http://object-storage:8333"'); expect(source).toContain('s3_endpoint="http://object-storage:8333"');
expect(source).toContain('s3_endpoint="http://127.0.0.1:$s3_port"'); expect(source).toContain('s3_endpoint="http://127.0.0.1:$s3_port"');
expect(source).not.toContain("0.0.0.0:${S3_PORT"); expect(source).not.toContain("0.0.0.0:${S3_PORT");
expect(source.indexOf("docker network disconnect")).toBeLessThan( expect(source.indexOf("compose_job_network_disconnect")).toBeLessThan(
source.indexOf(`"\${compose[@]}" down`), source.indexOf(`"\${compose[@]}" down`),
); );
expect(helper).toContain("[[ ! -f /.dockerenv ]]");
expect(helper).toContain('"$@" ps -q "$service"');
expect(helper).toContain("docker inspect");
expect(helper).toContain("docker network connect");
expect(helper).toContain("docker network disconnect");
});
it("provisions isolated PostgreSQL for the live migration suite", async () => {
const packageSource = await readFile(path.join(repositoryRoot, "package.json"), "utf8");
const packageDocument = JSON.parse(packageSource) as PackageDocument;
const source = await readFile(
path.join(repositoryRoot, "scripts", "test-migrations-integration.sh"),
"utf8",
);
expect(packageDocument.scripts?.["test:migrations:integration"]).toBe(
"bash scripts/test-migrations-integration.sh",
);
expect(source).toContain('source "$repository_root/scripts/lib/compose-job-network.sh"');
expect(source).toContain('export POSTGRES_PORT="0"');
expect(source).toContain("up --detach --wait --wait-timeout 120 postgres");
expect(source).toContain(`compose_job_network_connect postgres "\${compose[@]}"`);
expect(source).toContain('database_host="postgres"');
expect(source).toContain('database_port="5432"');
expect(source).toContain('database_host="127.0.0.1"');
expect(source).toContain(
'EKKLESIA_MIGRATION_TEST_DATABASE_URL="postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$database_host:$database_port/$POSTGRES_DB"',
);
expect(source).toContain("--config vitest.migrations.integration.config.ts");
expect(source).not.toContain("0.0.0.0");
expect(source).not.toContain('compose[@]}" logs');
expect(source.indexOf("compose_job_network_disconnect")).toBeLessThan(
source.indexOf("down --volumes --remove-orphans"),
);
}); });
it("includes every official platform layer in the workspace", async () => { it("includes every official platform layer in the workspace", async () => {
+6 -1
View File
@@ -3,6 +3,11 @@
"compilerOptions": { "compilerOptions": {
"noEmit": true "noEmit": true
}, },
"include": ["scripts/**/*.ts", "tests/**/*.ts", "vitest.config.ts"], "include": [
"scripts/**/*.ts",
"tests/**/*.ts",
"vitest.config.ts",
"vitest.migrations.integration.config.ts"
],
"exclude": ["node_modules", "tests/fixtures"] "exclude": ["node_modules", "tests/fixtures"]
} }
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
fileParallelism: false,
hookTimeout: 30_000,
include: ["core/tests/postgres-migration.integration.ts"],
passWithNoTests: false,
testTimeout: 30_000,
},
});