Missing passThroughEnv Skips Tests While CI Stays Green
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
Without passThroughEnv on a turbo.json test task, turbo won't forward CI's DATABASE_URL to the test process, and a describe.skip guard hides it, letting 24 RLS tests never run while CI stays green.
Conclusion
Without passThroughEnv on a turbo.json test task, turbo won’t forward DATABASE_URL to vitest’s child process, even when CI’s postgres service is right there providing it. The test suite doesn’t throw when it can’t get a connection URL — it self-skips via describe.skip, and vitest doesn’t treat a skip as a failure. CI stayed green at exit code 0 while 24 RLS tests sat unexecuted for weeks.
Symptom
24 RLS tests had been added alongside a CI postgres service that was already running, and the CI Test job stayed green the whole time. But that green wasn’t “passed” — it was “all skipped.” The cause was one missing line in turbo.json’s test task.
// turbo.json before the fix (excerpt)
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
No env, no passThroughEnv. To keep task caching reproducible, turbo refuses to forward any environment variable to a child process unless it’s explicitly declared. Even though the CI workflow set DATABASE_URL on the job, the vitest process launched by turbo run test never saw it.
Root cause
The test infrastructure was built to treat a missing DATABASE_URL as “not configured,” not as an error. vitest’s globalSetup is where that gets decided.
// packages/db/__tests__/_setup/global-setup.ts (excerpt)
export async function setup(): Promise<void> {
const url = process.env.DATABASE_URL;
if (!url) {
console.warn(
"[rls-tests] DATABASE_URL is not set, skipping RLS tests." +
" Run docker compose up -d postgres and set DATABASE_URL.",
);
process.env.RLS_TESTS_SKIP = "1";
return;
}
// H1: guard against accidentally connecting to prod/staging
assertTestDatabase(url);
// ...apply migrations...
}
Because this test harness runs DROP SCHEMA public CASCADE, choosing to set RLS_TESTS_SKIP and return instead of throwing when the URL is missing is a reasonable safety fallback for an unconfigured local machine. Every test file reads that flag.
// packages/db/__tests__/_setup/db.ts
export function getConnectionUrl(): string | null {
if (process.env.RLS_TESTS_SKIP === "1") return null;
return process.env.DATABASE_URL ?? null;
}
// packages/db/__tests__/rls/tenant-isolation.test.ts (excerpt)
const url = getConnectionUrl();
const describeOrSkip = url ? describe : describe.skip;
describeOrSkip("RLS tenant_isolation (school_id-based isolation)", () => {
// ...
When url is null, every test in the file runs under describe.skip instead of describe, and vitest never counts a skipped test as a failure. A fallback meant to be a courtesy for developers who forgot to start postgres locally ended up hiding a completely different cause in CI — turbo simply failing to forward the environment variable to the child process — behind the exact same skip path.
Two causes stacked here. The first was the passThroughEnv gap above. The second was that a separate variable, KIMITERRACE_TEST_DB_OK (a flag meant to satisfy assertTestDatabase’s H1 guard in CI), added earlier in the same effort, also wasn’t set in CI — and it disappeared down the same “unset → safe skip” path.
The fix
Add env and passThroughEnv to the test task in turbo.json, so DATABASE_URL both becomes part of the cache key and actually reaches the child process.
// turbo.json after the fix (excerpt)
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"env": ["DATABASE_URL"],
"passThroughEnv": ["DATABASE_URL"]
}
env alone isn’t enough. env declares that a variable’s value should be part of the task’s cache key — so the cache invalidates when it changes — while passThroughEnv declares that the variable is actually forwarded to the child process. Both are needed: cache correctness on one side, runtime visibility on the other.
Once this fix let CI’s Test job execute the 24 RLS tests for the first time, a chain of dormant bugs in the test code surfaced immediately. 0000_initial_baseline.sql was missing CREATE TYPE for 8 enums (user_role, publish_scope, and others), so CREATE TABLE "users" failed with type "user_role" does not exist — a bug nobody had hit simply because the tests had never run.
-- packages/db/drizzle/0000_initial_baseline.sql (top, after the fix)
CREATE TYPE "public"."user_role" AS ENUM('school_admin', 'teacher', 'student', 'guardian');--> statement-breakpoint
CREATE TYPE "public"."publish_scope" AS ENUM('school', 'class', 'homeroom', 'private');--> statement-breakpoint
-- ...8 enums total
CREATE TABLE "schools" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
Running the suite for real also turned up a schema mismatch in the test code itself (an invalid publish_scope value) and an RLS policy bug where an unset missing_ok mode on current_setting returned an empty string '' instead of NULL, triggering a ''::uuid cast error — both fixed in the same commit once they were finally visible.
Takeaway
A “skip safely when unconfigured” guard protects against a developer forgetting local setup, but it does nothing for a broken environment-variable handoff in CI. Both situations look identical from the test’s point of view — “the value isn’t set” — so the runner takes the same skip path either way. With a system like turbo that requires explicit per-task environment declarations, setting the variable on the CI job definition isn’t enough by itself; it only reaches the test process once env/passThroughEnv are declared too. A green CI run doesn’t prove anything passed — “zero tests executed” renders in exactly the same color. Without a habit of checking the actual skip count in CI output, this kind of decay can sit unnoticed indefinitely.
よくある質問
Q1Why was CI green while zero tests ran?
turbo.json's test task had no passThroughEnv, so turbo never forwarded CI's DATABASE_URL to the vitest process. Tests didn't throw on a missing DB URL — they self-skipped via describe.skip, and vitest doesn't count a skip as a failure. CI exited 0 with every test skipped.
Q2Was the CI postgres service itself down?
No, it ran fine. The cause wasn't a missing database — turbo just wasn't forwarding CI's environment variable to the test process. To keep caching reproducible, turbo only passes variables a task explicitly declares via env or passThroughEnv. DATABASE_URL wasn't declared, so vitest saw it as unset.
Q3Did running the tests for real surface other bugs?
Yes. Once the tests actually ran, 0000_initial_baseline.sql turned out to be missing CREATE TYPE statements for 8 enums, so CREATE TABLE "users" failed with type "user_role" does not exist. A second bug had been hiding behind the first, masked by the same env-var gap that hid the skipped tests.
確認した環境
- turbo ^2.9.16 / vitest ^3.2.6 (monorepo)
- Resolved in a fix commit on 2026-05-29
この記事の根拠
- JSONファイル 19〜24行目コミット 3c747af
- SQLファイル 1〜8行目コミット 3c747af
- TypeScriptファイル 54〜63行目コミット 941e5aa
- TypeScriptファイル 1〜14行目コミット 22c93be
- TypeScriptファイル 1〜9行目コミット da0605a
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。