Skip to content

Commit 63049f4

Browse files
committed
fix(test): replace vi.mock('node:fs') with vi.spyOn for thread-safe tests
vi.mock('node:fs') auto-mocking fails intermittently with vitest's threads pool + sharding. Switch source files to namespace fs imports and tests to vi.spyOn so mocks intercept at runtime without module replacement. Also remove dead processAsset function and unused computeFileHash import from download-assets.mjs.
1 parent d87f995 commit 63049f4

File tree

14 files changed

+102
-207
lines changed

14 files changed

+102
-207
lines changed

packages/cli/scripts/download-assets.mjs

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger'
2525
import { downloadSocketBtmRelease } from '@socketsecurity/lib/releases/socket-btm'
2626
import { spawn } from '@socketsecurity/lib/spawn'
2727

28-
import {
29-
computeFileHash,
30-
generateHeader,
31-
} from './utils/socket-btm-releases.mjs'
32-
3328
const __dirname = path.dirname(fileURLToPath(import.meta.url))
3429
const rootPath = path.join(__dirname, '..')
3530
const logger = getDefaultLogger()
@@ -99,14 +94,7 @@ const ASSETS = {
9994
* Download a single asset.
10095
*/
10196
async function downloadAsset(config) {
102-
const {
103-
description,
104-
download,
105-
extract,
106-
name,
107-
process: processConfig,
108-
type,
109-
} = config
97+
const { description, download, extract, name, type } = config
11098

11199
try {
112100
logger.group(`Extracting ${name} from socket-btm releases...`)
@@ -132,8 +120,6 @@ async function downloadAsset(config) {
132120
// Process based on asset type.
133121
if (type === 'archive' && extract) {
134122
await extractArchive(assetPath, extract, name)
135-
} else if (type === 'processed' && processConfig) {
136-
await processAsset(assetPath, processConfig, name)
137123
}
138124

139125
logger.groupEnd()
@@ -204,78 +190,6 @@ async function extractArchive(tarGzPath, extractConfig, assetName) {
204190
await fs.writeFile(versionPath, tag, 'utf-8')
205191
}
206192

207-
/**
208-
* Process and transform asset (e.g., add header to JS file).
209-
*/
210-
async function processAsset(assetPath, processConfig, assetName) {
211-
const { outputPath } = processConfig
212-
213-
// Check if extraction needed by comparing version.
214-
const assetDir = path.dirname(assetPath)
215-
const sourceVersionPath = path.join(assetDir, '.version')
216-
const outputVersionPath = path.join(
217-
path.dirname(outputPath),
218-
`${path.basename(outputPath, path.extname(outputPath))}.version`,
219-
)
220-
221-
if (
222-
existsSync(outputVersionPath) &&
223-
existsSync(outputPath) &&
224-
existsSync(sourceVersionPath)
225-
) {
226-
const cachedVersion = (await fs.readFile(outputVersionPath, 'utf8')).trim()
227-
const sourceVersion = (await fs.readFile(sourceVersionPath, 'utf8')).trim()
228-
if (cachedVersion === sourceVersion) {
229-
logger.info(`${assetName} already up to date`)
230-
return
231-
}
232-
233-
logger.info(`${assetName} version changed, re-extracting...`)
234-
}
235-
236-
// Read the downloaded asset.
237-
let content = await fs.readFile(assetPath, 'utf-8')
238-
239-
// Compute source hash for cache validation.
240-
const sourceHash = await computeFileHash(assetPath)
241-
242-
// Get tag from source version file.
243-
if (!existsSync(sourceVersionPath)) {
244-
throw new Error(
245-
`Source version file not found: ${sourceVersionPath}. ` +
246-
'Please download assets first using the build system.',
247-
)
248-
}
249-
250-
const tag = (await fs.readFile(sourceVersionPath, 'utf8')).trim()
251-
if (!tag || tag.length === 0) {
252-
throw new Error(
253-
`Invalid version file content at ${sourceVersionPath}. ` +
254-
'Please re-download assets.',
255-
)
256-
}
257-
258-
// Generate output file with header.
259-
const header = generateHeader({
260-
assetName: path.basename(assetPath),
261-
scriptName: 'scripts/download-assets.mjs',
262-
sourceHash,
263-
tag,
264-
})
265-
266-
const output = `${header}
267-
268-
${content}
269-
`
270-
271-
// Ensure build directory exists before writing.
272-
await fs.mkdir(path.dirname(outputPath), { recursive: true })
273-
await fs.writeFile(outputPath, output, 'utf-8')
274-
275-
// Write version file.
276-
await fs.writeFile(outputVersionPath, tag, 'utf-8')
277-
}
278-
279193
/**
280194
* Download multiple assets (parallel by default, sequential opt-in).
281195
*

packages/cli/src/constants/agents.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Functions for package manager version requirements and execution paths.
44
*/
55

6-
import { existsSync } from 'node:fs'
6+
import fs from 'node:fs'
77
import path from 'node:path'
88

99
import { whichReal } from '@socketsecurity/lib/bin'
@@ -63,7 +63,7 @@ export async function getNpmExecPath(): Promise<string> {
6363
// Check npm in the same directory as node.
6464
const nodeDir = path.dirname(process.execPath)
6565
const npmInNodeDir = path.join(nodeDir, NPM)
66-
if (existsSync(npmInNodeDir)) {
66+
if (fs.existsSync(npmInNodeDir)) {
6767
return npmInNodeDir
6868
}
6969
// Fall back to whichReal.

packages/cli/src/utils/ecosystem/environment.mts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* - Configuring concurrent execution limits
2525
*/
2626

27-
import { existsSync, readFileSync } from 'node:fs'
27+
import fs from 'node:fs'
2828
import path from 'node:path'
2929

3030
import browserslist from 'browserslist'
@@ -257,13 +257,13 @@ const LOCKS: Record<string, Agent> = {
257257
function resolveBinPathSync(binPath: string): string {
258258
// Simple implementation that tries to resolve a bin path to its actual entry point.
259259
// This is used on Windows to resolve shims like `npm` or `npm.cmd` to their .js entry point.
260-
if (!existsSync(binPath)) {
260+
if (!fs.existsSync(binPath)) {
261261
return binPath
262262
}
263263

264264
try {
265265
// Try to read the file synchronously
266-
const content = readFileSync(binPath, 'utf8')
266+
const content = fs.readFileSync(binPath, 'utf8')
267267
// Look for common patterns in npm/node shims:
268268
// - node "C:\path\to\npm-cli.js" "$@"
269269
// - "%_prog%" "%dp0%\node_modules\npm\bin\npm-cli.js" %*
@@ -309,28 +309,28 @@ function preferWindowsCmdShim(binPath: string, binName: string): string {
309309
const cmdShim = path.join(path.dirname(binPath), `${binName}.cmd`)
310310

311311
// Ensure shim exists, otherwise fallback to binPath
312-
return existsSync(cmdShim) ? cmdShim : binPath
312+
return fs.existsSync(cmdShim) ? cmdShim : binPath
313313
}
314314

315315
async function getAgentExecPath(agent: Agent): Promise<string> {
316316
const binName = binByAgent.get(agent)!
317317
if (binName === NPM) {
318318
// Try to use getNpmExecPath() first, but verify it exists.
319319
const npmPath = preferWindowsCmdShim(await getNpmExecPath(), NPM)
320-
if (existsSync(npmPath)) {
320+
if (fs.existsSync(npmPath)) {
321321
return npmPath
322322
}
323323
// If getNpmExecPath() doesn't exist, try common locations.
324324
// Check npm in the same directory as node.
325325
const nodeDir = path.dirname(process.execPath)
326326
if (WIN32) {
327327
const npmCmdInNodeDir = path.join(nodeDir, `${NPM}.cmd`)
328-
if (existsSync(npmCmdInNodeDir)) {
328+
if (fs.existsSync(npmCmdInNodeDir)) {
329329
return npmCmdInNodeDir
330330
}
331331
}
332332
const npmInNodeDir = path.join(nodeDir, NPM)
333-
if (existsSync(npmInNodeDir)) {
333+
if (fs.existsSync(npmInNodeDir)) {
334334
return preferWindowsCmdShim(npmInNodeDir, NPM)
335335
}
336336
// Fall back to which.
@@ -343,7 +343,7 @@ async function getAgentExecPath(agent: Agent): Promise<string> {
343343
if (binName === PNPM) {
344344
// Try to use getPnpmExecPath() first, but verify it exists.
345345
const pnpmPath = await getPnpmExecPath()
346-
if (existsSync(pnpmPath)) {
346+
if (fs.existsSync(pnpmPath)) {
347347
return pnpmPath
348348
}
349349
// Fall back to which.
@@ -452,7 +452,7 @@ export async function detectPackageEnvironment({
452452
)
453453
: await findUp(PACKAGE_JSON, { cwd })
454454
const pkgPath =
455-
pkgJsonPath && existsSync(pkgJsonPath)
455+
pkgJsonPath && fs.existsSync(pkgJsonPath)
456456
? path.dirname(pkgJsonPath)
457457
: undefined
458458
const pkgJson = pkgPath ? await readPackageJson(pkgPath) : undefined

packages/cli/src/utils/npm/paths.mts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync } from 'node:fs'
1+
import fs from 'node:fs'
22
import Module from 'node:module'
33
import path from 'node:path'
44

@@ -81,7 +81,7 @@ export function getNpmRequire(): NodeJS.Require {
8181
const npmNmPath = path.join(npmDirPath, `${NODE_MODULES}/npm`)
8282
_npmRequire = Module.createRequire(
8383
path.join(
84-
existsSync(npmNmPath) ? npmNmPath : npmDirPath,
84+
fs.existsSync(npmNmPath) ? npmNmPath : npmDirPath,
8585
'<dummy-basename>',
8686
),
8787
)

packages/cli/src/utils/pnpm/lockfile.mts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { existsSync } from 'node:fs'
1+
import fs from 'node:fs'
22

33
import yaml from 'js-yaml'
44
import semver from 'semver'
@@ -88,7 +88,9 @@ export function parsePnpmLockfileVersion(version: unknown): SemVer | undefined {
8888
export async function readPnpmLockfile(
8989
lockfilePath: string,
9090
): Promise<string | undefined> {
91-
return existsSync(lockfilePath) ? await readFileUtf8(lockfilePath) : undefined
91+
return fs.existsSync(lockfilePath)
92+
? await readFileUtf8(lockfilePath)
93+
: undefined
9294
}
9395

9496
export function stripLeadingPnpmDepPathSlash(depPath: string): string {

packages/cli/src/utils/process/os.mts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
* - File permission management
2727
*/
2828

29-
import { existsSync, promises as fs, readFileSync } from 'node:fs'
29+
import fs from 'node:fs'
3030

3131
import { getDefaultLogger } from '@socketsecurity/lib/logger'
3232
import { spawn } from '@socketsecurity/lib/spawn'
@@ -142,8 +142,8 @@ function detectMusl(): boolean {
142142

143143
// Method 1: Check /etc/os-release for Alpine.
144144
try {
145-
if (existsSync('/etc/os-release')) {
146-
const osRelease = readFileSync('/etc/os-release', 'utf8')
145+
if (fs.existsSync('/etc/os-release')) {
146+
const osRelease = fs.readFileSync('/etc/os-release', 'utf8')
147147
if (osRelease.includes('Alpine') || osRelease.includes('alpine')) {
148148
cachedLibc = 'musl'
149149
return true
@@ -156,8 +156,8 @@ function detectMusl(): boolean {
156156
// Method 2: Check if ldd references musl.
157157
try {
158158
if (
159-
existsSync('/lib/ld-musl-x86_64.so.1') ||
160-
existsSync('/lib/ld-musl-aarch64.so.1')
159+
fs.existsSync('/lib/ld-musl-x86_64.so.1') ||
160+
fs.existsSync('/lib/ld-musl-aarch64.so.1')
161161
) {
162162
cachedLibc = 'musl'
163163
return true
@@ -168,8 +168,8 @@ function detectMusl(): boolean {
168168

169169
// Method 3: Check /proc/version for musl indicators.
170170
try {
171-
if (existsSync('/proc/version')) {
172-
const version = readFileSync('/proc/version', 'utf8')
171+
if (fs.existsSync('/proc/version')) {
172+
const version = fs.readFileSync('/proc/version', 'utf8')
173173
if (version.includes('musl')) {
174174
cachedLibc = 'musl'
175175
return true
@@ -250,7 +250,7 @@ async function ensureExecutable(filePath: string): Promise<void> {
250250
}
251251

252252
try {
253-
await fs.chmod(filePath, 0o755)
253+
await fs.promises.chmod(filePath, 0o755)
254254
logger.log('Set executable permissions')
255255
} catch (e) {
256256
logger.warn(

packages/cli/src/utils/socket/json.mts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* - Supports both read and write operations
1919
*/
2020

21-
import { existsSync, promises as fs, readFileSync } from 'node:fs'
21+
import fs from 'node:fs'
2222
import path from 'node:path'
2323

2424
import { debugDirNs, debugNs } from '@socketsecurity/lib/debug'
@@ -127,14 +127,14 @@ export async function readSocketJson(
127127
defaultOnError = false,
128128
): Promise<CResult<SocketJson>> {
129129
const sockJsonPath = path.join(cwd, SOCKET_JSON)
130-
if (!existsSync(sockJsonPath)) {
130+
if (!fs.existsSync(sockJsonPath)) {
131131
debugNs('notice', `miss: ${SOCKET_JSON} not found at ${cwd}`)
132132
return { ok: true, data: getDefaultSocketJson() }
133133
}
134134

135135
let json = null
136136
try {
137-
json = await fs.readFile(sockJsonPath, 'utf8')
137+
json = await fs.promises.readFile(sockJsonPath, 'utf8')
138138
} catch (e) {
139139
if (defaultOnError) {
140140
logger.warn(`Failed to read ${SOCKET_JSON}, using default`)
@@ -188,13 +188,13 @@ export function readSocketJsonSync(
188188
defaultOnError = false,
189189
): CResult<SocketJson> {
190190
const sockJsonPath = path.join(cwd, SOCKET_JSON)
191-
if (!existsSync(sockJsonPath)) {
191+
if (!fs.existsSync(sockJsonPath)) {
192192
debugNs('notice', `miss: ${SOCKET_JSON} not found at ${cwd}`)
193193
return { ok: true, data: getDefaultSocketJson() }
194194
}
195195
let jsonContent = null
196196
try {
197-
jsonContent = readFileSync(sockJsonPath, 'utf8')
197+
jsonContent = fs.readFileSync(sockJsonPath, 'utf8')
198198
} catch (e) {
199199
if (defaultOnError) {
200200
logger.warn(`Failed to read ${SOCKET_JSON}, using default`)
@@ -262,7 +262,7 @@ export async function writeSocketJson(
262262
}
263263

264264
const filepath = path.join(cwd, SOCKET_JSON)
265-
await fs.writeFile(filepath, `${jsonContent}\n`, 'utf8')
265+
await fs.promises.writeFile(filepath, `${jsonContent}\n`, 'utf8')
266266

267267
return { ok: true, data: undefined }
268268
}

packages/cli/test/unit/constants/agents.test.mts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,17 @@
1313
* - constants/agents.mts (implementation)
1414
*/
1515

16+
import fs from 'node:fs'
17+
1618
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1719

1820
// Mock dependencies using hoisted mocks.
1921
const mockWhichReal = vi.hoisted(() => vi.fn())
20-
const mockExistsSync = vi.hoisted(() => vi.fn())
2122

2223
vi.mock('@socketsecurity/lib/bin', () => ({
2324
whichReal: mockWhichReal,
2425
}))
2526

26-
vi.mock('node:fs', async () => {
27-
const actual = await vi.importActual('node:fs')
28-
return {
29-
...actual,
30-
existsSync: mockExistsSync,
31-
}
32-
})
33-
3427
import {
3528
BUN,
3629
getMinimumVersionByAgent,
@@ -46,8 +39,11 @@ import {
4639
} from '../../../src/constants/agents.mts'
4740

4841
describe('agents constants', () => {
42+
let mockExistsSync: ReturnType<typeof vi.spyOn>
43+
4944
beforeEach(() => {
5045
vi.clearAllMocks()
46+
mockExistsSync = vi.spyOn(fs, 'existsSync')
5147
})
5248

5349
afterEach(() => {

0 commit comments

Comments
 (0)