ReviewOS

also looking at this

stacks/bun-query-builder

test: raise instead of skipping when a provided live service is missing

#1187
Merged glennmichael123 wants to merge test/loud-service-gates into main
4 files +194 -9
packages/bun-query-builder/test/gate.test.tsadded+89-0
Changes to packages/bun-query-builder/test/gate.test.ts
@@ -0,0 +1,89 @@
1/**
2 * The live-service gate raises where the service is provided, skips where it
3 * is optional.
4 *
5 * `probePostgres()` used to answer `false` for every possible failure, so the
6 * 14 files gated on it vanished into "skip" whenever Postgres was unreachable
7 * for any reason — and a run with all of them gone still exits 0. That is how
8 * this repository once shipped a release whose two most severe guards never
9 * executed: the probe built its URL from `process.env.USER`, auth failed on
10 * every runner, and CI was green throughout. Fixing the URL fixed that cause.
11 * This pins the mechanism.
12 *
13 * Both directions are controlled, because only the pair is evidence: a gate
14 * that always raises would pass the first case here while making the suite
15 * unrunnable for anyone without Postgres, which is the behaviour the skip
16 * exists to provide.
17 *
18 * Each case runs the real gate in a child `bun test`, against a port nothing
19 * answers on, and reads the exit status — the thing CI actually keys on.
20 */
21
22import { describe, expect, it } from 'bun:test'
23import process from 'node:process'
24import { resolve } from 'node:path'
25
26// Any file whose only precondition is a live Postgres. Its own assertions are
27// irrelevant here; what matters is whether the gate let the run pass.
28const GATED = 'packages/bun-query-builder/test/client.json-contains.pg.test.ts'
29const REPO = resolve(import.meta.dir, '../../..')
30const UNREACHABLE = 'postgresql://postgres:postgres@127.0.0.1:59999/test_db'
31
32function runGated(env: Record<string, string | undefined>): { code: number | null, output: string } {
33 const child = Bun.spawnSync([process.execPath, 'test', GATED], {
34 cwd: REPO,
35 env: {
36 ...process.env,
37 // Cleared so the parent environment cannot decide the case under test:
38 // a developer machine has no CI, a runner has it set, and this file has
39 // to produce both regardless of where it runs.
40 CI: undefined,
41 QB_REQUIRE_LIVE: undefined,
42 DATABASE_URL: UNREACHABLE,
43 POSTGRES_URL: UNREACHABLE,
44 ...env,
45 } as Record<string, string>,
46 stdout: 'pipe',
47 stderr: 'pipe',
48 })
49
50 return {
51 code: child.exitCode,
52 output: `${child.stdout.toString()}${child.stderr.toString()}`,
53 }
54}
55
56describe('the live-Postgres gate', () => {
57 it('fails the run when a provided Postgres is unreachable', () => {
58 const { code, output } = runGated({ CI: '1' })
59
60 // The assertion that matters is the status, not the text: a skipped suite
61 // exits 0, and 0 is what CI reads as "the guards ran and passed".
62 expect(code).not.toBe(0)
63 // And it has to say what was missing, or the red run sends someone hunting
64 // through 14 files for a failure that is really a dead service.
65 expect(output).toContain('live Postgres')
66 expect(output).toContain('59999')
67 }, 30_000)
68
69 it('skips, and passes, where Postgres is merely optional', () => {
70 const { code, output } = runGated({})
71
72 expect(code).toBe(0)
73 expect(output).toContain('skipping live Postgres tests')
74 }, 30_000)
75
76 it('lets a CI-like environment without services opt out', () => {
77 // A fork running the suite with no service containers: CI is set, but
78 // nothing provides Postgres, so the gate must stay a skip.
79 const { code } = runGated({ CI: '1', QB_REQUIRE_LIVE: '0' })
80
81 expect(code).toBe(0)
82 }, 30_000)
83
84 it('lets a developer opt in to CI strictness', () => {
85 const { code } = runGated({ QB_REQUIRE_LIVE: '1' })
86
87 expect(code).not.toBe(0)
88 }, 30_000)
89})