#!/usr/bin/env bash # # test/reset-app-db.sh — restore the APPLICATION database to the demo state. # # The screen-capture utility (`make screens`, test/e2e/screens) runs this # first so every capture comes from the same rows: walkthrough fixtures, # manual experiments, drifted timestamps, and, because database-generated # IDs are never pinned (Decision 30), freshly generated identifiers would # otherwise show up as changed screens on pages nobody touched (maintainer # decision 2026-08-30). # # The demo state is built once into a snapshot database (_demo): # migrate, demo seed with the operator's own person (--with-operator), the # boot data state, and the row clocks pinned (pin-timestamps.sql). Every # reset then drops the application database and recreates it FROM that # snapshot, so rows and their IDs are identical between resets. The snapshot # is rebuilt when it is missing, when the migrations, the seed, or the clock # pin have changed since it was built (their fingerprint is the snapshot's # database comment), or on `--rebuild`. # # This is the one deliberate exception to the rule that nothing under test/ # touches `member_console`: reset-test-db.sh refuses that database so # `make test` can never clobber a developer's working state; this script # exists to clobber it on purpose, and only when asked. The running app # reconnects on its next query; browser sessions that referenced the old # rows are invalid until their next sign-in. # # Usage: cd test && ./reset-app-db.sh [--rebuild] set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "$here/.." && pwd)" env_file="$here/.env" if [[ ! -f "$env_file" ]]; then echo "error: $env_file not found. Run ./bootstrap-stack.sh first." >&2 exit 1 fi set -a # shellcheck source=/dev/null source "$env_file" set +a for var in POSTGRES_ADMIN_DSN MC_DB_DSN; do if [[ -z "${!var:-}" ]]; then echo "error: $var is not set in $env_file." >&2 exit 1 fi done # The database reset re-creates every row from the demo seed (or, on # rebuild, from a fresh seed run), and ids are UUIDv7 -- never reused -- so # a session minted before the reset holds a person id that no longer exists # afterward. flush_sessions empties the Valkey session store so no such # stale session survives a reset; this Valkey instance exists only for # member-console sessions in the test stack, so a full flush loses nothing # else. Tried in order: a local valkey-cli, a local redis-cli (protocol- # compatible), or docker compose exec into the valkey service when neither # CLI is installed on the host. flush_sessions() { echo "Flushing the session store (Valkey) so no pre-reset session survives..." # The store is password-protected (valkey-password is required # configuration), so every path here authenticates. local auth=() [[ -n "${VALKEY_PASSWORD:-}" ]] && auth=(-a "$VALKEY_PASSWORD" --no-auth-warning) if command -v valkey-cli >/dev/null 2>&1; then valkey-cli -h localhost -p "$VALKEY_PORT" "${auth[@]}" FLUSHALL >/dev/null elif command -v redis-cli >/dev/null 2>&1; then redis-cli -h localhost -p "$VALKEY_PORT" "${auth[@]}" FLUSHALL >/dev/null else (cd "$here" && docker compose exec -T valkey valkey-cli "${auth[@]}" FLUSHALL >/dev/null) fi } path="${MC_DB_DSN#*://}"; path="${path#*/}"; name="${path%%\?*}" if [[ -z "$name" ]]; then echo "error: cannot read a database name from MC_DB_DSN." >&2 exit 1 fi snapshot="${name}_demo" # The snapshot's DSN is the application's with the database name (the last # path segment) swapped; the user name may equal the database name, so a # plain substitution would rename the wrong part. dsn_prefix="${MC_DB_DSN%/*}" dsn_tail="${MC_DB_DSN##*/}" dsn_query="${dsn_tail#"${name}"}" snapshot_dsn="${dsn_prefix}/${snapshot}${dsn_query}" # The snapshot is current when it was built from the migrations, the seed, # and the clock pin the repository holds now; the fingerprint of those # sources is stored as the snapshot database's comment. fingerprint="$(cat "$repo_root"/internal/db/migrations/*.sql "$repo_root"/internal/demoseed/*.go "$here/pin-timestamps.sql" | md5sum | cut -c1-32)" rebuild=0 if [[ "${1:-}" == "--rebuild" ]]; then rebuild=1 else current="$(psql "$POSTGRES_ADMIN_DSN" -tAc "SELECT coalesce(shobj_description(oid, 'pg_database'), '') FROM pg_database WHERE datname = '${snapshot}'")" if [[ "$current" != "$fingerprint" ]]; then echo "Demo snapshot '${snapshot}' is missing or was built from older migrations or seed; rebuilding it." rebuild=1 fi fi if [[ "$rebuild" == 1 ]]; then echo "Building the demo snapshot '${snapshot}' (migrate, seed with the operator, pin clocks)..." psql "$POSTGRES_ADMIN_DSN" -v ON_ERROR_STOP=1 --quiet </dev/null) "$here/seed-demo.sh" --with-operator >/dev/null psql "$MC_DB_DSN" -v ON_ERROR_STOP=1 --quiet </dev/null psql "$POSTGRES_ADMIN_DSN" -v ON_ERROR_STOP=1 --quiet -c "COMMENT ON DATABASE ${snapshot} IS '${fingerprint}'" echo "Demo snapshot '${snapshot}' built; application database '${name}' is at the demo state." flush_sessions exit 0 fi echo "Restoring application database '${name}' from the demo snapshot '${snapshot}'..." psql "$POSTGRES_ADMIN_DSN" -v ON_ERROR_STOP=1 --quiet <