fix: resolve test environment loading race condition in CI
- Move getTestConfig() calls from module level to test execution time - Add CI-specific debug logging to diagnose environment loading issues - Add verification step in CI workflow to check .env.test availability - Ensure environment variables are loaded before tests access config The issue was that config was being accessed at module import time, which could happen before the global setup runs in some CI environments.
This commit is contained in:
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@@ -19,6 +19,15 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
# Verify test environment setup
|
||||||
|
- name: Verify test environment
|
||||||
|
run: |
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "Checking for .env.test file:"
|
||||||
|
ls -la .env.test || echo ".env.test not found!"
|
||||||
|
echo "First few lines of .env.test:"
|
||||||
|
head -5 .env.test || echo "Cannot read .env.test"
|
||||||
|
|
||||||
# Run tests with coverage and multiple reporters
|
# Run tests with coverage and multiple reporters
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
run: npm run test:ci
|
run: npm run test:ci
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { beforeEach, afterEach, vi } from 'vitest';
|
import { beforeEach, afterEach, vi } from 'vitest';
|
||||||
import { loadTestEnvironment, getTestConfig, getTestTimeout } from './test-env';
|
import { loadTestEnvironment, getTestConfig, getTestTimeout } from './test-env';
|
||||||
|
|
||||||
|
// CI Debug: Log environment loading in CI only
|
||||||
|
if (process.env.CI === 'true') {
|
||||||
|
console.log('[CI-DEBUG] Global setup starting, NODE_ENV:', process.env.NODE_ENV);
|
||||||
|
}
|
||||||
|
|
||||||
// Load test environment configuration
|
// Load test environment configuration
|
||||||
loadTestEnvironment();
|
loadTestEnvironment();
|
||||||
|
|
||||||
|
if (process.env.CI === 'true') {
|
||||||
|
console.log('[CI-DEBUG] Global setup complete, N8N_API_URL:', process.env.N8N_API_URL);
|
||||||
|
}
|
||||||
|
|
||||||
// Get test configuration
|
// Get test configuration
|
||||||
const testConfig = getTestConfig();
|
const testConfig = getTestConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,26 @@ import { existsSync } from 'fs';
|
|||||||
|
|
||||||
// Load test environment variables
|
// Load test environment variables
|
||||||
export function loadTestEnvironment(): void {
|
export function loadTestEnvironment(): void {
|
||||||
|
// CI Debug logging
|
||||||
|
const isCI = process.env.CI === 'true';
|
||||||
|
|
||||||
// Load base test environment
|
// Load base test environment
|
||||||
const testEnvPath = path.resolve(process.cwd(), '.env.test');
|
const testEnvPath = path.resolve(process.cwd(), '.env.test');
|
||||||
|
|
||||||
|
if (isCI) {
|
||||||
|
console.log('[CI-DEBUG] Looking for .env.test at:', testEnvPath);
|
||||||
|
console.log('[CI-DEBUG] File exists?', existsSync(testEnvPath));
|
||||||
|
}
|
||||||
|
|
||||||
if (existsSync(testEnvPath)) {
|
if (existsSync(testEnvPath)) {
|
||||||
dotenv.config({ path: testEnvPath });
|
const result = dotenv.config({ path: testEnvPath });
|
||||||
|
if (isCI && result.error) {
|
||||||
|
console.error('[CI-DEBUG] Failed to load .env.test:', result.error);
|
||||||
|
} else if (isCI && result.parsed) {
|
||||||
|
console.log('[CI-DEBUG] Successfully loaded', Object.keys(result.parsed).length, 'env vars from .env.test');
|
||||||
|
}
|
||||||
|
} else if (isCI) {
|
||||||
|
console.warn('[CI-DEBUG] .env.test file not found, will use defaults only');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load local test overrides (for sensitive values)
|
// Load local test overrides (for sensitive values)
|
||||||
|
|||||||
@@ -19,10 +19,14 @@ import {
|
|||||||
} from '@tests/helpers/env-helpers';
|
} from '@tests/helpers/env-helpers';
|
||||||
|
|
||||||
describe('Test Environment Configuration Example', () => {
|
describe('Test Environment Configuration Example', () => {
|
||||||
const config = getTestConfig();
|
let config: ReturnType<typeof getTestConfig>;
|
||||||
const logger = createTestLogger('test-env-example');
|
let logger: ReturnType<typeof createTestLogger>;
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
// Initialize config inside beforeAll to ensure environment is loaded
|
||||||
|
config = getTestConfig();
|
||||||
|
logger = createTestLogger('test-env-example');
|
||||||
|
|
||||||
logger.info('Test suite starting with configuration:', {
|
logger.info('Test suite starting with configuration:', {
|
||||||
environment: config.nodeEnv,
|
environment: config.nodeEnv,
|
||||||
database: config.database.path,
|
database: config.database.path,
|
||||||
@@ -35,20 +39,23 @@ describe('Test Environment Configuration Example', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should be in test mode', () => {
|
it('should be in test mode', () => {
|
||||||
|
const testConfig = getTestConfig();
|
||||||
expect(isTestMode()).toBe(true);
|
expect(isTestMode()).toBe(true);
|
||||||
expect(config.nodeEnv).toBe('test');
|
expect(testConfig.nodeEnv).toBe('test');
|
||||||
expect(config.isTest).toBe(true);
|
expect(testConfig.isTest).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have proper database configuration', () => {
|
it('should have proper database configuration', () => {
|
||||||
expect(config.database.path).toBeDefined();
|
const testConfig = getTestConfig();
|
||||||
expect(config.database.rebuildOnStart).toBe(false);
|
expect(testConfig.database.path).toBeDefined();
|
||||||
expect(config.database.seedData).toBe(true);
|
expect(testConfig.database.rebuildOnStart).toBe(false);
|
||||||
|
expect(testConfig.database.seedData).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have mock API configuration', () => {
|
it('should have mock API configuration', () => {
|
||||||
expect(config.api.url).toMatch(/mock-api/);
|
const testConfig = getTestConfig();
|
||||||
expect(config.api.key).toBe('test-api-key-12345');
|
expect(testConfig.api.url).toMatch(/mock-api/);
|
||||||
|
expect(testConfig.api.key).toBe('test-api-key-12345');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect test timeouts', { timeout: getTestTimeout('unit') }, async () => {
|
it('should respect test timeouts', { timeout: getTestTimeout('unit') }, async () => {
|
||||||
@@ -60,7 +67,8 @@ describe('Test Environment Configuration Example', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should support environment overrides', () => {
|
it('should support environment overrides', () => {
|
||||||
const originalLogLevel = config.logging.level;
|
const testConfig = getTestConfig();
|
||||||
|
const originalLogLevel = testConfig.logging.level;
|
||||||
|
|
||||||
const result = withEnvOverrides({
|
const result = withEnvOverrides({
|
||||||
LOG_LEVEL: 'debug',
|
LOG_LEVEL: 'debug',
|
||||||
@@ -73,7 +81,8 @@ describe('Test Environment Configuration Example', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result).toBe('success');
|
expect(result).toBe('success');
|
||||||
expect(config.logging.level).toBe(originalLogLevel);
|
const configAfter = getTestConfig();
|
||||||
|
expect(configAfter.logging.level).toBe(originalLogLevel);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate unique test database paths', () => {
|
it('should generate unique test database paths', () => {
|
||||||
@@ -87,15 +96,17 @@ describe('Test Environment Configuration Example', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should construct mock API URLs', () => {
|
it('should construct mock API URLs', () => {
|
||||||
|
const testConfig = getTestConfig();
|
||||||
const baseUrl = getMockApiUrl();
|
const baseUrl = getMockApiUrl();
|
||||||
const endpointUrl = getMockApiUrl('/nodes');
|
const endpointUrl = getMockApiUrl('/nodes');
|
||||||
|
|
||||||
expect(baseUrl).toBe(config.api.url);
|
expect(baseUrl).toBe(testConfig.api.url);
|
||||||
expect(endpointUrl).toBe(`${config.api.url}/nodes`);
|
expect(endpointUrl).toBe(`${testConfig.api.url}/nodes`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.skipIf(!isFeatureEnabled('mockExternalApis'))('should check feature flags', () => {
|
it.skipIf(!isFeatureEnabled('mockExternalApis'))('should check feature flags', () => {
|
||||||
expect(config.features.mockExternalApis).toBe(true);
|
const testConfig = getTestConfig();
|
||||||
|
expect(testConfig.features.mockExternalApis).toBe(true);
|
||||||
expect(isFeatureEnabled('mockExternalApis')).toBe(true);
|
expect(isFeatureEnabled('mockExternalApis')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -135,9 +146,10 @@ describe('Test Environment Configuration Example', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have proper logging configuration', () => {
|
it('should have proper logging configuration', () => {
|
||||||
expect(config.logging.level).toBe('error');
|
const testConfig = getTestConfig();
|
||||||
expect(config.logging.debug).toBe(false);
|
expect(testConfig.logging.level).toBe('error');
|
||||||
expect(config.logging.showStack).toBe(true);
|
expect(testConfig.logging.debug).toBe(false);
|
||||||
|
expect(testConfig.logging.showStack).toBe(true);
|
||||||
|
|
||||||
// Logger should respect configuration
|
// Logger should respect configuration
|
||||||
logger.debug('This should not appear in test output');
|
logger.debug('This should not appear in test output');
|
||||||
@@ -145,26 +157,30 @@ describe('Test Environment Configuration Example', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should have performance thresholds', () => {
|
it('should have performance thresholds', () => {
|
||||||
expect(config.performance.thresholds.apiResponse).toBe(100);
|
const testConfig = getTestConfig();
|
||||||
expect(config.performance.thresholds.dbQuery).toBe(50);
|
expect(testConfig.performance.thresholds.apiResponse).toBe(100);
|
||||||
expect(config.performance.thresholds.nodeParse).toBe(200);
|
expect(testConfig.performance.thresholds.dbQuery).toBe(50);
|
||||||
|
expect(testConfig.performance.thresholds.nodeParse).toBe(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable caching and rate limiting in tests', () => {
|
it('should disable caching and rate limiting in tests', () => {
|
||||||
expect(config.cache.enabled).toBe(false);
|
const testConfig = getTestConfig();
|
||||||
expect(config.cache.ttl).toBe(0);
|
expect(testConfig.cache.enabled).toBe(false);
|
||||||
expect(config.rateLimiting.max).toBe(0);
|
expect(testConfig.cache.ttl).toBe(0);
|
||||||
expect(config.rateLimiting.window).toBe(0);
|
expect(testConfig.rateLimiting.max).toBe(0);
|
||||||
|
expect(testConfig.rateLimiting.window).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should configure test paths', () => {
|
it('should configure test paths', () => {
|
||||||
expect(config.paths.fixtures).toBe('./tests/fixtures');
|
const testConfig = getTestConfig();
|
||||||
expect(config.paths.data).toBe('./tests/data');
|
expect(testConfig.paths.fixtures).toBe('./tests/fixtures');
|
||||||
expect(config.paths.snapshots).toBe('./tests/__snapshots__');
|
expect(testConfig.paths.data).toBe('./tests/data');
|
||||||
|
expect(testConfig.paths.snapshots).toBe('./tests/__snapshots__');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support MSW configuration', () => {
|
it('should support MSW configuration', () => {
|
||||||
expect(config.mocking.msw.enabled).toBe(true);
|
const testConfig = getTestConfig();
|
||||||
expect(config.mocking.msw.apiDelay).toBe(0);
|
expect(testConfig.mocking.msw.enabled).toBe(true);
|
||||||
|
expect(testConfig.mocking.msw.apiDelay).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user