diff --git a/src/__tests__/auth.test.ts b/src/__tests__/auth.test.ts index 072c2f3..ebd5c95 100644 --- a/src/__tests__/auth.test.ts +++ b/src/__tests__/auth.test.ts @@ -188,7 +188,11 @@ describe('per-key rate quotas', () => { describe('admin endpoints', () => { const ORIGINAL = process.env.ADMIN_TOKEN beforeEach(() => { process.env.ADMIN_TOKEN = 'admin-secret' }) - afterEach(() => { process.env.ADMIN_TOKEN = ORIGINAL }) + afterEach(() => { + // Node stringifies undefined → "undefined"; delete to truly clear. + if (ORIGINAL === undefined) delete process.env.ADMIN_TOKEN + else process.env.ADMIN_TOKEN = ORIGINAL + }) async function buildAdminApp() { const app = Fastify() diff --git a/src/__tests__/pairs.test.ts b/src/__tests__/pairs.test.ts index c164fce..1265e16 100644 --- a/src/__tests__/pairs.test.ts +++ b/src/__tests__/pairs.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import Fastify from 'fastify' // vi.mock factories are hoisted — declare all mock fns with vi.hoisted() @@ -41,14 +41,12 @@ const ADMIN_KEY = 'test-admin-key-abc' const VALID_ISSUER = 'GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5' async function buildApp() { - const savedKey = process.env.ADMIN_API_KEY + // Keep ADMIN_API_KEY set through inject — the route reads it at request time. + // Restoring here (before inject) raced with parallel files mutating process.env. process.env.ADMIN_API_KEY = ADMIN_KEY const app = Fastify({ logger: false }) await registerPairsRoutes(app) await app.ready() - // restore after app init - if (savedKey === undefined) delete process.env.ADMIN_API_KEY - else process.env.ADMIN_API_KEY = savedKey return app } @@ -61,6 +59,10 @@ beforeEach(() => { mockQuery.mockReset().mockResolvedValue({ rows: [] }) }) +afterEach(() => { + delete process.env.ADMIN_API_KEY +}) + describe('POST /pairs', () => { it('adds a new pair and returns 201 with pairKey', async () => { const app = await buildApp() diff --git a/src/__tests__/usage.test.ts b/src/__tests__/usage.test.ts index f9a4c91..40ebf10 100644 --- a/src/__tests__/usage.test.ts +++ b/src/__tests__/usage.test.ts @@ -30,6 +30,12 @@ beforeEach(() => { vi.clearAllMocks() }) +const ORIGINAL_ADMIN_TOKEN = process.env.ADMIN_TOKEN +afterEach(() => { + if (ORIGINAL_ADMIN_TOKEN === undefined) delete process.env.ADMIN_TOKEN + else process.env.ADMIN_TOKEN = ORIGINAL_ADMIN_TOKEN +}) + function buildUsageApp() { process.env.ADMIN_TOKEN = 'admin-secret' const app = Fastify() diff --git a/src/__tests__/x402-quota.test.ts b/src/__tests__/x402-quota.test.ts index 813c450..9d95b0a 100644 --- a/src/__tests__/x402-quota.test.ts +++ b/src/__tests__/x402-quota.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' const PAYMENT_ADDRESS = 'GPAYMENTADDRESS123456789012345678901234567890123456789012' @@ -80,6 +80,18 @@ beforeEach(() => { mockInitialize.mockReset().mockResolvedValue(undefined) }) +const ENV_KEYS = ['ORACLE_PAYMENT_ADDRESS', 'STELLAR_NETWORK', 'REQUIRE_API_KEY'] as const +const originalEnv: Partial> = {} +beforeEach(() => { + for (const key of ENV_KEYS) originalEnv[key] = process.env[key] +}) +afterEach(() => { + for (const key of ENV_KEYS) { + if (originalEnv[key] === undefined) delete process.env[key] + else process.env[key] = originalEnv[key] + } +}) + async function buildAppWithAuth() { process.env.ORACLE_PAYMENT_ADDRESS = PAYMENT_ADDRESS process.env.STELLAR_NETWORK = 'testnet' diff --git a/tests/aggregator.property.test.ts b/tests/aggregator.property.test.ts index 1e595d5..1206a94 100644 --- a/tests/aggregator.property.test.ts +++ b/tests/aggregator.property.test.ts @@ -55,7 +55,9 @@ describe('Price aggregator property tests', () => { // this test failed before running a single iteration — the mocked // @stellar/stellar-sdk had no Networks export, which getNetworkConfig() // needs); that volume of real work needs more than the 5s default. - it('produces valid route results for random venue prices', { timeout: 30000 }, async () => { + // 30s keeps real headroom for 10k async iterations under fork load while + // staying tight enough that a genuine hang still fails the run. + it('produces valid route results for random venue prices', { timeout: 30_000 }, async () => { await fc.assert( fc.asyncProperty( fc.float({ min: 0, max: 2000, noNaN: true, noDefaultInfinity: true, noNegativeZero: true }), @@ -119,5 +121,5 @@ describe('Price aggregator property tests', () => { ), { numRuns: 10000 } ) - }, 15000) + }) }) diff --git a/vitest.config.ts b/vitest.config.ts index 1761797..85100f4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -6,5 +6,14 @@ export default defineConfig({ globals: true, include: ['src/**/*.test.ts', 'tests/**/*.test.ts'], exclude: ['dist/**', 'node_modules/**'], + // forks: each concurrent file gets its own process. This pins Vitest's + // existing default rather than changing it - the flake was env leakage + // within a reused fork, which setupFiles settles. + pool: 'forks', + setupFiles: ['./vitest.setup.ts'], + // resetModules()+@stellar/stellar-sdk reimport (networkVenueConfig) needs + // a little headroom over the 5s default. Kept deliberately tight: a loose + // ceiling hides a genuine hang as a slow pass. + testTimeout: 20_000, }, -}) \ No newline at end of file +}) diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..b939bf6 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,34 @@ +/** + * Enforce process.env isolation across the suite. + * + * Root cause of intermittent auth.test.ts / pairs.test.ts (and rotating + * victims like networkVenueConfig.test.ts) failures: Vitest 4's default pool + * is already `forks`, so files do not run concurrently in one process. What + * leaks instead is `process.env` *within* a fork - a reused child process runs + * several files sequentially, so a key one file sets is still set when the + * next file starts, and any suite that assumes a clean environment fails + * depending on the order the files happen to be scheduled in. + * + * This setup file snapshots `process.env` before every test and restores it + * afterwards so mutations cannot leak to the next test in the same worker. + * Combined with `pool: 'forks'` in vitest.config.ts (separate process per + * concurrent file), cross-file leakage is eliminated without giving up + * file parallelism. + */ +import { beforeEach, afterEach } from 'vitest' + +let envSnapshot: Record + +beforeEach(() => { + envSnapshot = { ...process.env } +}) + +afterEach(() => { + for (const key of Object.keys(process.env)) { + if (!(key in envSnapshot)) delete process.env[key] + } + for (const [key, value] of Object.entries(envSnapshot)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } +})