TESTDBPORT=55432 # isolated container port — dev DB is 5433
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.
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}