Skip to content

Commit 972e5cf

Browse files
committed
test: isolate theme tests to prevent module state pollution
Split tests requiring module-level isolation into separate test suite: - Created .config/vitest.config.isolated.mts with isolate: true - Moved themes.test.ts and logger.test.ts to test/isolated/ - Updated main vitest config to exclude isolated tests - Modified test script to run both main and isolated suites - Added separate test-isolated job in CI workflow This prevents theme state pollution between test files while keeping the main test suite fast with parallel execution. Fixes third-strike test failure for "should default to socket theme".
1 parent 27c36b3 commit 972e5cf

6 files changed

Lines changed: 175 additions & 8 deletions

File tree

.config/vitest.config.isolated.mts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @fileoverview Vitest configuration for isolated tests
3+
* Tests that require full isolation due to shared module state
4+
*/
5+
6+
import path from 'node:path'
7+
import { fileURLToPath } from 'node:url'
8+
import { defineConfig } from 'vitest/config'
9+
10+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
11+
const projectRoot = path.resolve(__dirname, '..')
12+
13+
// Normalize paths for cross-platform glob patterns (forward slashes on Windows)
14+
const toGlobPath = (pathLike: string): string => pathLike.replaceAll('\\', '/')
15+
16+
export default defineConfig({
17+
cacheDir: path.resolve(projectRoot, '.cache/vitest-isolated'),
18+
resolve: {
19+
preserveSymlinks: false,
20+
extensions: ['.mts', '.ts', '.mjs', '.js', '.json'],
21+
alias: {
22+
'#env/ci': path.resolve(projectRoot, 'src/env/ci.ts'),
23+
'#env': path.resolve(projectRoot, 'src/env'),
24+
'#constants': path.resolve(projectRoot, 'src/constants'),
25+
'#lib': path.resolve(projectRoot, 'src/lib'),
26+
'#packages': path.resolve(projectRoot, 'src/lib/packages'),
27+
'#types': path.resolve(projectRoot, 'src/types.ts'),
28+
'#utils': path.resolve(projectRoot, 'src/utils'),
29+
cacache: path.resolve(projectRoot, 'src/external/cacache'),
30+
'make-fetch-happen': path.resolve(
31+
projectRoot,
32+
'src/external/make-fetch-happen',
33+
),
34+
'fast-sort': path.resolve(projectRoot, 'src/external/fast-sort'),
35+
pacote: path.resolve(projectRoot, 'src/external/pacote'),
36+
'@socketregistry/scripts': path.resolve(projectRoot, 'scripts'),
37+
'@socketsecurity/lib': path.resolve(projectRoot, 'src'),
38+
},
39+
},
40+
test: {
41+
globalSetup: [path.resolve(__dirname, 'vitest-global-setup.mts')],
42+
globals: false,
43+
environment: 'node',
44+
include: [
45+
toGlobPath(
46+
path.resolve(projectRoot, 'test/isolated/**/*.test.{js,ts,mjs,mts}'),
47+
),
48+
],
49+
exclude: ['**/node_modules/**', '**/dist/**'],
50+
reporters: ['default'],
51+
// Full isolation for tests that modify shared module state
52+
pool: 'threads',
53+
poolOptions: {
54+
threads: {
55+
singleThread: true,
56+
maxThreads: 1,
57+
minThreads: 1,
58+
isolate: true,
59+
useAtomics: true,
60+
},
61+
},
62+
testTimeout: 10_000,
63+
hookTimeout: 10_000,
64+
sequence: {
65+
concurrent: false,
66+
},
67+
},
68+
})

.config/vitest.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default defineConfig({
5656
exclude: [
5757
'**/node_modules/**',
5858
'**/dist/**',
59+
toGlobPath(path.resolve(projectRoot, 'test/isolated/**')),
5960
...(process.env.INCLUDE_NPM_TESTS
6061
? []
6162
: [toGlobPath(path.resolve(projectRoot, 'test/npm/**'))]),

.github/workflows/ci.yml

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ jobs:
7878
node_modules
7979
key: build-${{ github.sha }}-${{ runner.os }}
8080

81-
# Test matrix - reuses build artifacts
81+
# Test matrix - reuses build artifacts (main test suite)
8282
test:
8383
name: Test
8484
needs: [lint, build]
@@ -117,8 +117,41 @@ jobs:
117117
if: runner.os == 'Windows'
118118
run: pnpm install --frozen-lockfile
119119

120-
- name: Run tests
121-
run: pnpm run test --fast --all
120+
- name: Run main tests
121+
run: pnpm exec vitest --config .config/vitest.config.mts run
122+
123+
# Isolated test suite - runs separately with full isolation
124+
test-isolated:
125+
name: Test (Isolated)
126+
needs: [lint, build]
127+
runs-on: ubuntu-latest
128+
timeout-minutes: 10
129+
steps:
130+
- name: Checkout code
131+
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
132+
133+
- name: Setup pnpm
134+
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
135+
with:
136+
version: 10
137+
138+
- name: Setup Node.js
139+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
140+
with:
141+
node-version: 22
142+
cache: 'pnpm'
143+
144+
- name: Restore build artifacts
145+
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
146+
with:
147+
path: |
148+
dist
149+
node_modules
150+
key: build-${{ github.sha }}-Linux
151+
fail-on-cache-miss: true
152+
153+
- name: Run isolated tests
154+
run: pnpm exec vitest --config .config/vitest.config.isolated.mts run
122155

123156
# Type check - reuses build artifacts
124157
type-check:
@@ -156,7 +189,7 @@ jobs:
156189
# Status check job - used as required check in branch protection
157190
ci-success:
158191
name: CI Success
159-
needs: [lint, build, test, type-check]
192+
needs: [lint, build, test, test-isolated, type-check]
160193
if: always()
161194
runs-on: ubuntu-latest
162195
steps:
@@ -165,6 +198,7 @@ jobs:
165198
if [ "${{ needs.lint.result }}" != "success" ] || \
166199
[ "${{ needs.build.result }}" != "success" ] || \
167200
[ "${{ needs.test.result }}" != "success" ] || \
201+
[ "${{ needs.test-isolated.result }}" != "success" ] || \
168202
[ "${{ needs.type-check.result }}" != "success" ]; then
169203
echo "One or more jobs failed"
170204
exit 1

scripts/test.mjs

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,11 @@ async function runBuild() {
199199
return 0
200200
}
201201

202-
async function runTests(options, positionals = []) {
202+
async function runTests(
203+
options,
204+
positionals = [],
205+
configPath = '.config/vitest.config.mts',
206+
) {
203207
const { all, coverage, force, staged, update } = options
204208
const runAll = all || force
205209

@@ -217,7 +221,7 @@ async function runTests(options, positionals = []) {
217221
const vitestCmd = WIN32 ? 'vitest.cmd' : 'vitest'
218222
const vitestPath = path.join(nodeModulesBinPath, vitestCmd)
219223

220-
const vitestArgs = ['--config', '.config/vitest.config.mts', 'run']
224+
const vitestArgs = ['--config', configPath, 'run']
221225

222226
// Add coverage if requested
223227
if (coverage) {
@@ -311,6 +315,57 @@ async function runTests(options, positionals = []) {
311315
return result.code
312316
}
313317

318+
async function runIsolatedTests(options) {
319+
const { coverage } = options
320+
321+
logger.step('Running isolated tests')
322+
323+
// Prepare vitest command
324+
const vitestCmd = WIN32 ? 'vitest.cmd' : 'vitest'
325+
const vitestPath = path.join(nodeModulesBinPath, vitestCmd)
326+
327+
const vitestArgs = ['--config', '.config/vitest.config.isolated.mts', 'run']
328+
329+
// Add coverage if requested
330+
if (coverage) {
331+
vitestArgs.push('--coverage')
332+
}
333+
334+
const spawnOptions = {
335+
cwd: rootPath,
336+
env: {
337+
...process.env,
338+
NODE_OPTIONS:
339+
`${process.env.NODE_OPTIONS || ''} --max-old-space-size=${process.env.CI ? 8192 : 4096} --unhandled-rejections=warn`.trim(),
340+
},
341+
stdio: 'inherit',
342+
}
343+
344+
// Use dotenvx to load test environment
345+
const dotenvxCmd = WIN32 ? 'dotenvx.cmd' : 'dotenvx'
346+
const dotenvxPath = path.join(nodeModulesBinPath, dotenvxCmd)
347+
348+
// Always use direct execution for isolated tests (simpler, more predictable)
349+
const result = await runCommandWithOutput(
350+
dotenvxPath,
351+
['-q', 'run', '-f', '.env.test', '--', vitestPath, ...vitestArgs],
352+
{
353+
...spawnOptions,
354+
stdio: ['inherit', 'pipe', 'pipe'],
355+
},
356+
)
357+
358+
// Print output
359+
if (result.stdout) {
360+
process.stdout.write(result.stdout)
361+
}
362+
if (result.stderr) {
363+
process.stderr.write(result.stderr)
364+
}
365+
366+
return result.code
367+
}
368+
314369
async function main() {
315370
try {
316371
// Parse arguments
@@ -420,14 +475,23 @@ async function main() {
420475
}
421476
}
422477

423-
// Run tests
478+
// Run main tests
424479
exitCode = await runTests(
425480
{ ...values, coverage: withCoverage },
426481
positionals,
427482
)
428483

429484
if (exitCode !== 0) {
430-
logger.error('Tests failed')
485+
logger.error('Main tests failed')
486+
process.exitCode = exitCode
487+
return
488+
}
489+
490+
// Run isolated tests
491+
exitCode = await runIsolatedTests({ coverage: withCoverage })
492+
493+
if (exitCode !== 0) {
494+
logger.error('Isolated tests failed')
431495
process.exitCode = exitCode
432496
} else {
433497
logger.success('All tests passed!')

0 commit comments

Comments
 (0)