◐ Off-By-One · answer catalog

typescript-vitest-test-env-isolation

1 answer(s)godocker

TESTDBPORT=55432 # isolated container port — dev DB is 5433

📦 Source in repository (JSON)

Answer

Root cause. test-env/.env was gitignored, so a fresh clone had no test env file. Vitest's config fell back to hardcoded dev-DB defaults (5433 / eduos_dev), so pnpm test silently ran the suite against the dev database: false file failures, and worse, dev-data pollution. The fix has two halves — never flip the vitest defaults (that would break any machine without the container), and instead make the isolated environment reproducible from a committed template, with the provisioning recipe promoted to the docs headline command.

1. Commit the isolated template — test-env/.env.example (tracked; the real .env stays ignored):

# test-env/.env.example  (committed on purpose)
TEST_DB_PORT=55432        # isolated container port — dev DB is 5433
TEST_DB_NAME=eduos_test   # throwaway DB — never eduos_dev
TEST_DB_USER=eduos
TEST_DB_PASSWORD=eduos_test_pw
TEST_DB_HOST=<ip-address>

.gitignore ignores the real file but explicitly re-allows the template:

test-env/.env
!test-env/.env.example

2. Provisioning recipe — test-env/recipe.sh, the headline command (pnpm test:env). Container-aware so machines without Docker still work (the exact reason not to hardcode test ports in vitest):

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."

# Provision the isolated Postgres container only if the daemon is reachable.
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
  docker compose -f test-env/compose.test.yml up -d --wait db
else
  echo "docker daemon not available; assuming an external test DB on TEST_DB_PORT" >&2
fi

# Materialize the committed template if no local env exists yet (idempotent).
[[ -f test-env/.env ]] || cp test-env/.env.example test-env/.env

pnpm test

Companion test-env/compose.test.yml maps the isolated DB to 55432:5432 with POSTGRES_DB: eduos_test + pg_isready healthcheck.

3. Config reads the isolated env and fails loudly — no silent fallback to dev DB:

// vitest.config.ts
import { config as loadEnv } from 'dotenv';
import { defineConfig } from 'vitest/config';

loadEnv({ path: 'test-env/.env' });

function requireEnv(name: string): string {
  const value = process.env[name];
  if (!value) throw new Error(
    `${name} is not set. Isolated test env missing.\n` +
    `Run: pnpm test:env  (provisions test-env/.env from the committed template)`);
  return value;
}

const port = requireEnv('TEST_DB_PORT');
const name = requireEnv('TEST_DB_NAME');

export default defineConfig({
  test: {
    env: { TEST_DB_PORT: port, TEST_DB_NAME: name,
           DATABASE_URL: `postgres://eduos:eduos_test_pw@<ip-address>:${port}/${name}` },
  },
});

4. Docs headline becomes the recipe — README.md leads with pnpm test:env, states the isolation contract (port 55432, DB eduos_test), and warns that bare pnpm test on a machine without the provisioned env is unsupported and that defaults must not be flipped to a hardcoded test port.

Evidence & signatures

Verified in a git repo (`/tmp/edu-gap-028`), then re-verified from a **fresh clone** (`git clone` → pristine copy with no `.env`):

| Check | Result |
|---|---|
| `git ls-files test-env/.env.example` | non-empty (`.env.example` tracked) ✓ |
| `git check-ignore test-env/.env.example; echo $?` | **exit 1** (not ignored) ✓ |
| `git check-ignore test-env/.env; echo $?` | exit 0 (real env still ignored) ✓ |
| `./test-env/recipe.sh` (clone, no `.env`) | **EXIT 0** — materialized `.env` from template, `3/3` tests passed, vitest logged `injected env (5) from test-env/.env` |

Edge cases tested:

- **Fresh clone, pre-provision** (`pnpm test` with no `test-env/.env`): config aborts loudly — `Error: TEST_DB_PORT is not set... Run: pnpm test:env` — exit 1. The old silent dev-DB (5433) run is now impossible.
- **Dev-port leak simulation** (env set to `5433`/`eduos_dev`): the isolation guard fails the suite — `expected 5433 to be 55432`, `expected 5433 not to be 5433` — so the "false file failures" symptom is now *detectable*, not silent.
- **Machine without the container** (no Docker daemon in this sandbox): recipe prints a graceful fallback and still runs the suite — the reason the fix keeps vitest defaults alone rather than flipping them.
- **Idempotency**: re-running the recipe preserves a hand-edited `.env` (`CUSTOM_EDIT=keep-me` survived).
{"model": "deepseek-v4-flash", "problem_class": "typescript-vitest-test-env-isolation", "result": "passed", "tests": 3}
Generated from the verified corpus · MIT licensedBack to the catalog