Two integration test files shared the same pinned TESTBASEURL, but only one guarded against the server being down.
I reproduced the failure and verified the fix pattern locally with Vitest 5 / Node 22, then wrote the full solution to ~/solution.md. Here it is:
ECONNREFUSED when no live server is runningTwo integration test files shared the same pinned TEST_BASE_URL, but only one guarded against the server being down.
api.test.ts ran a live-server probe and wrapped its suite in describe.skipIf(!serverAvailable).health.test.ts performed its fetch during test collection (top level / describe body), before any skipIf could apply.With TEST_BASE_URL pointing at a dead port, that top-level fetch rejected with TypeError: fetch failed → connect ECONNREFUSED, so Vitest reported a failed suite instead of skipping. The suite skipped in api.test.ts only because that file never touched the network at collection time.
Correct contract: probe once, never throw from the probe, gate every networked suite on its result.
Reproduced before the fix:
TypeError: fetch failed
Caused by: Error: connect ECONNREFUSED <ip-address>:39999
Extract one shared, network-safe probe and use it in health.test.ts and api.test.ts; unit-test the probe with an injected fetch so its tests never touch the network.
apps/server/src/__tests__/helpers/mafia-server.ts (new)export interface ProbeOptions {
baseUrl?: string;
timeoutMs?: number;
/** Injectable for network-free unit tests. Defaults to global fetch. */
fetchImpl?: typeof fetch;
}
/** Fallback used when TEST_BASE_URL is not set. */
export const DEFAULT_TEST_BASE_URL = 'http://<ip-address>:3000';
export function resolveTestBaseUrl(explicit?: string): string {
return explicit ?? process.env.TEST_BASE_URL ?? DEFAULT_TEST_BASE_URL;
}
/**
* Returns true when an HTTP server is reachable at `baseUrl`.
* Never throws: connection refused, DNS failure and timeouts all resolve `false`.
*
* Any HTTP response counts as "live" (even 4xx/5xx) so a running-but-broken
* server makes the integration tests run and fail loudly, rather than silently skip.
*/
export async function probeMafiaServer(options: ProbeOptions = {}): Promise<boolean> {
const {
baseUrl = resolveTestBaseUrl(),
timeoutMs = 1000,
fetchImpl = globalThis.fetch,
} = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
await fetchImpl(new URL('/health', baseUrl).toString(), {
method: 'GET',
signal: controller.signal,
});
return true;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
let cached: Promise<boolean> | undefined;
/** Cached wrapper so each test file probes the server at most once. */
export function isMafiaServerAvailable(options?: ProbeOptions): Promise<boolean> {
cached ??= probeMafiaServer(options);
return cached;
}
/** Test-only reset for the cache. */
export function __resetMafiaServerProbe(): void {
cached = undefined;
}
apps/server/src/__tests__/helpers/mafia-server.test.ts (new)import { describe, it, expect, vi } from 'vitest';
import { probeMafiaServer } from './mafia-server';
describe('probeMafiaServer (network-free)', () => {
it('returns true when the server responds (even non-2xx)', async () => {
const fetchImpl = vi.fn().mockResolvedValue({ ok: false, status: 503 });
await expect(
probeMafiaServer({ baseUrl: 'http://<ip-address>:1', fetchImpl: fetchImpl as never }),
).resolves.toBe(true);
expect(fetchImpl).toHaveBeenCalledOnce();
});
it('returns false on connection refused', async () => {
const fetchImpl = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
await expect(
probeMafiaServer({ baseUrl: 'http://<ip-address>:1', fetchImpl: fetchImpl as never }),
).resolves.toBe(false);
});
it('returns false when the probe times out', async () => {
const fetchImpl = vi.fn().mockImplementation((_url: string, init?: RequestInit) =>
new Promise((_resolve, reject) => {
init?.signal?.addEventListener('abort', () =>
reject(new DOMException('Aborted', 'AbortError')),
);
}),
);
await expect(
probeMafiaServer({ baseUrl: 'http://<ip-address>:1', timeoutMs: 10, fetchImpl: fetchImpl as never }),
).resolves.toBe(false);
});
});
apps/server/src/__tests__/health.test.ts (guard added)import { describe, it, expect } from 'vitest';
import { isMafiaServerAvailable, resolveTestBaseUrl } from './helpers/mafia-server';
const serverAvailable = await isMafiaServerAvailable();
const baseUrl = resolveTestBaseUrl();
describe.skipIf(!serverAvailable)('health integration', () => {
it('GET /health returns 200', async () => {
const res = await fetch(new URL('/health', baseUrl));
expect(res.status).toBe(200);
});
});
apps/server/src/__tests__/api.test.ts (use the shared helper)import { describe, it, expect } from 'vitest';
import { isMafiaServerAvailable, resolveTestBaseUrl } from './helpers/mafia-server';
const serverAvailable = await isMafiaServerAvailable();
const baseUrl = resolveTestBaseUrl();
describe.skipIf(!serverAvailable)('api integration', () => {
it('GET /api returns 200', async () => {
const res = await fetch(new URL('/api', baseUrl));
expect(res.status).toBe(200);
});
});
Top-level
awaitrequires Vitest's ESM transform (default for*.test.ts). If a file is forced to CJS, move the probe into a one-timeglobalSetupthat writesprocess.env.SERVER_AVAILABLE, thendescribe.skipIf(process.env.SERVER_AVAILABLE !== 'true').
cd apps/server
# 1) Dead port: integration suites skip, probe unit tests pass, no ECONNREFUSED
TEST_BASE_URL=http://<ip-address>:39999 pnpm vitest run
# 2) Live server: integration suites run and pass (start server first)
TEST_BASE_URL=http://<ip-address>:3000 pnpm vitest run
# 3) Build and lint
pnpm build
pnpm lint
Expected (original fix commit a6d8dab):
| Scenario | Result |
|---|---|
| Dead-port run | 313 passed, 12 skipped |
| Live-server run | 325 passed |
pnpm build |
pass |
pnpm lint |
pass |
Independent confirmation (this environment, Vitest 5 / Node 22):
- Dead port: 3 passed | 2 skipped, no ECONNREFUSED. The unguarded "before" variant in the same run failed with TypeError: fetch failed / connect ECONNREFUSED.
- Live local server: 5 passed (all suites ran).
false, so a missing server degrades to skip.AbortController + timeoutMs prevents CI hangs.fetchImpl injection tests the guard logic itself.# Evidence - Problem class: typescript-vitest-live-server-integration-skip-guard - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-24T08:35:32.409Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A TypeScript Vitest integration suite had one file with a shared live-server probe and describe.skipIf guard while health.test.ts fetched the pinned server URL unconditionally. On a clean-style environment with TEST_BASE_URL pointed at a dead port, the suite failed with ECONNREFUSED in health.test.ts even though api.test.ts skipped. The fix was to extract the probe into a shared helper and apply the same guard to health.test.ts, with network-free unit tests for the probe. Verification: dead-port run 313 passed and 12 skipped; live run 325 passed; build and lint passed.", "environment": "pnpm workspace; Vitest; Node runtime; no server listening on the configured test URL", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "typescript-vitest-live-server-integration-skip-guard", "provider": "openrouter", "solved_at": "2026-09-24T08:35:32.410Z", "version": ""}I reproduced the failure and verified the fix pattern locally with Vitest 5 / Node 22, then wrote the full solution to ~/solution.md. Here it is:
ECONNREFUSED when no live server is runningTwo integration test files shared the same pinned TEST_BASE_URL, but only one guarded against the server being down.
api.test.ts ran a live-server probe and wrapped its suite in describe.skipIf(!serverAvailable).health.test.ts performed its fetch during test collection (top level / describe body), before any skipIf could apply.With TEST_BASE_URL pointing at a dead port, that top-level fetch rejected with TypeError: fetch failed → connect ECONNREFUSED, so Vitest reported a failed suite instead of skipping. The suite skipped in api.test.ts only because that file never touched the network at collection time.
Correct contract: probe once, never throw from the probe, gate every networked suite on its result.
Reproduced before the fix:
TypeError: fetch failed
Caused by: Error: connect ECONNREFUSED <ip-address>:39999
Extract one shared, network-safe probe and use it in health.test.ts and api.test.ts; unit-test the probe with an injected fetch so its tests never touch the network.
apps/server/src/__tests__/helpers/mafia-server.ts (new)export interface ProbeOptions {
baseUrl?: string;
timeoutMs?: number;
/** Injectable for network-free unit tests. Defaults to global fetch. */
fetchImpl?: typeof fetch;
}
/** Fallback used when TEST_BASE_URL is not set. */
export const DEFAULT_TEST_BASE_URL = 'http://<ip-address>:3000';
export function resolveTestBaseUrl(explicit?: string): string {
return explicit ?? process.env.TEST_BASE_URL ?? DEFAULT_TEST_BASE_URL;
}
/**
* Returns true when an HTTP server is reachable at `baseUrl`.
* Never throws: connection refused, DNS failure and timeouts all resolve `false`.
*
* Any HTTP response counts as "live" (even 4xx/5xx) so a running-but-broken
* server makes the integration tests run and fail loudly, rather than silently skip.
*/
export async function probeMafiaServer(options: ProbeOptions = {}): Promise<boolean> {
const {
baseUrl = resolveTestBaseUrl(),
timeoutMs = 1000,
fetchImpl = globalThis.fetch,
} = options;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
await fetchImpl(new URL('/health', baseUrl).toString(), {
method: 'GET',
signal: controller.signal,
});
return true;
} catch {
return false;
} finally {
clearTimeout(timer);
}
}
let cached: Promise<boolean> | undefined;
/** Cached wrapper so each test file probes the server at most once. */
export function isMafiaServerAvailable(options?: ProbeOptions): Promise<boolean> {
cached ??= probeMafiaServer(options);
return cached;
}
/** Test-only reset for the cache. */
export function __resetMafiaServerProbe(): void {
cached = undefined;
}
apps/server/src/__tests__/helpers/mafia-server.test.ts (new)import { describe, it, expect, vi } from 'vitest';
import { probeMafiaServer } from './mafia-server';
describe('probeMafiaServer (network-free)', () => {
it('returns true when the server responds (even non-2xx)', async () => {
const fetchImpl = vi.fn().mockResolvedValue({ ok: false, status: 503 });
await expect(
probeMafiaServer({ baseUrl: 'http://<ip-address>:1', fetchImpl: fetchImpl as never }),
).resolves.toBe(true);
expect(fetchImpl).toHaveBeenCalledOnce();
});
it('returns false on connection refused', async () => {
const fetchImpl = vi.fn().mockRejectedValue(new TypeError('fetch failed'));
await expect(
probeMafiaServer({ baseUrl: 'http://<ip-address>:1', fetchImpl: fetchImpl as never }),
).resolves.toBe(false);
});
it('returns false when the probe times out', async () => {
const fetchImpl = vi.fn().mockImplementation((_url: string, init?: RequestInit) =>
new Promise((_resolve, reject) => {
init?.signal?.addEventListener('abort', () =>
reject(new DOMException('Aborted', 'AbortError')),
);
}),
);
await expect(
probeMafiaServer({ baseUrl: 'http://<ip-address>:1', timeoutMs: 10, fetchImpl: fetchImpl as never }),
).resolves.toBe(false);
});
});
apps/server/src/__tests__/health.test.ts (guard added)import { describe, it, expect } from 'vitest';
import { isMafiaServerAvailable, resolveTestBaseUrl } from './helpers/mafia-server';
const serverAvailable = await isMafiaServerAvailable();
const baseUrl = resolveTestBaseUrl();
describe.skipIf(!serverAvailable)('health integration', () => {
it('GET /health returns 200', async () => {
const res = await fetch(new URL('/health', baseUrl));
expect(res.status).toBe(200);
});
});
apps/server/src/__tests__/api.test.ts (use the shared helper)import { describe, it, expect } from 'vitest';
import { isMafiaServerAvailable, resolveTestBaseUrl } from './helpers/mafia-server';
const serverAvailable = await isMafiaServerAvailable();
const baseUrl = resolveTestBaseUrl();
describe.skipIf(!serverAvailable)('api integration', () => {
it('GET /api returns 200', async () => {
const res = await fetch(new URL('/api', baseUrl));
expect(res.status).toBe(200);
});
});
Top-level
awaitrequires Vitest's ESM transform (default for*.test.ts). If a file is forced to CJS, move the probe into a one-timeglobalSetupthat writesprocess.env.SERVER_AVAILABLE, thendescribe.skipIf(process.env.SERVER_AVAILABLE !== 'true').
cd apps/server
# 1) Dead port: integration suites skip, probe unit tests pass, no ECONNREFUSED
TEST_BASE_URL=http://<ip-address>:39999 pnpm vitest run
# 2) Live server: integration suites run and pass (start server first)
TEST_BASE_URL=http://<ip-address>:3000 pnpm vitest run
# 3) Build and lint
pnpm build
pnpm lint
Expected (original fix commit a6d8dab):
| Scenario | Result |
|---|---|
| Dead-port run | 313 passed, 12 skipped |
| Live-server run | 325 passed |
pnpm build |
pass |
pnpm lint |
pass |
Independent confirmation (this environment, Vitest 5 / Node 22):
- Dead port: 3 passed | 2 skipped, no ECONNREFUSED. The unguarded "before" variant in the same run failed with TypeError: fetch failed / connect ECONNREFUSED.
- Live local server: 5 passed (all suites ran).
false, so a missing server degrades to skip.AbortController + timeoutMs prevents CI hangs.fetchImpl injection tests the guard logic itself.# Evidence - Problem class: typescript-vitest-live-server-integration-skip-guard - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-24T08:35:32.409Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A TypeScript Vitest integration suite had one file with a shared live-server probe and describe.skipIf guard while health.test.ts fetched the pinned server URL unconditionally. On a clean-style environment with TEST_BASE_URL pointed at a dead port, the suite failed with ECONNREFUSED in health.test.ts even though api.test.ts skipped. The fix was to extract the probe into a shared helper and apply the same guard to health.test.ts, with network-free unit tests for the probe. Verification: dead-port run 313 passed and 12 skipped; live run 325 passed; build and lint passed.", "environment": "pnpm workspace; Vitest; Node runtime; no server listening on the configured test URL", "language": "typescript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "typescript-vitest-live-server-integration-skip-guard", "provider": "openrouter", "solved_at": "2026-09-24T08:35:32.410Z", "version": ""}