Skip to content

Commit c03d5d7

Browse files
committed
Extract dependency check logic to pure function, remove mock.module
linux-dependency-error.test.ts used mock.module() at module top-level to replace generate-seccomp-filter.js. Bun's mock.module() is process-global and is not undone by mock.restore(). On Linux CI, where filesystem readdir order differs from macOS, this file loaded before integration.test.ts and poisoned its static import of generateSeccompFilter, causing assertPrecompiledBpfInUse() to receive null. The test was mocking the module graph to control four booleans that feed into ~18 lines of error/warning classification. Extract that classification as dependencyStatusToCheck(status) and test it directly with plain inputs. checkLinuxDependencies keeps its signature and now composes getLinuxDependencyStatus with the pure mapper, also removing the duplicated whichSync/getPreGeneratedBpfPath calls between the two functions. The deleted getLinuxDependencyStatus tests were verifying that {x: f() !== null} returns true when f() is mocked non-null; the deleted 'uses custom seccomp paths' test's mock ignored the path argument and asserted only Array.isArray on the result.
1 parent 512fab6 commit c03d5d7

2 files changed

Lines changed: 77 additions & 151 deletions

File tree

src/sandbox/linux-sandbox-utils.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -392,28 +392,36 @@ export function getLinuxDependencyStatus(seccompConfig?: {
392392
}
393393

394394
/**
395-
* Check sandbox dependencies and return structured result
395+
* Pure mapping from dependency status to errors/warnings.
396+
* Separated from getLinuxDependencyStatus so the classification logic
397+
* can be unit-tested without mocking filesystem or binary lookups.
396398
*/
397-
export function checkLinuxDependencies(seccompConfig?: {
398-
bpfPath?: string
399-
applyPath?: string
400-
}): SandboxDependencyCheck {
399+
export function dependencyStatusToCheck(
400+
status: LinuxDependencyStatus,
401+
): SandboxDependencyCheck {
401402
const errors: string[] = []
402403
const warnings: string[] = []
403404

404-
if (whichSync('bwrap') === null)
405-
errors.push('bubblewrap (bwrap) not installed')
406-
if (whichSync('socat') === null) errors.push('socat not installed')
405+
if (!status.hasBwrap) errors.push('bubblewrap (bwrap) not installed')
406+
if (!status.hasSocat) errors.push('socat not installed')
407407

408-
const hasBpf = getPreGeneratedBpfPath(seccompConfig?.bpfPath) !== null
409-
const hasApply = getApplySeccompBinaryPath(seccompConfig?.applyPath) !== null
410-
if (!hasBpf || !hasApply) {
408+
if (!status.hasSeccompBpf || !status.hasSeccompApply) {
411409
warnings.push('seccomp not available - unix socket access not restricted')
412410
}
413411

414412
return { warnings, errors }
415413
}
416414

415+
/**
416+
* Check sandbox dependencies and return structured result
417+
*/
418+
export function checkLinuxDependencies(seccompConfig?: {
419+
bpfPath?: string
420+
applyPath?: string
421+
}): SandboxDependencyCheck {
422+
return dependencyStatusToCheck(getLinuxDependencyStatus(seccompConfig))
423+
}
424+
417425
/**
418426
* Initialize the Linux network bridge for sandbox networking
419427
*
Lines changed: 58 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,94 @@
1-
import { describe, test, expect, beforeEach, afterAll, mock } from 'bun:test'
2-
3-
// Mock state - these control what the mocked functions return
4-
let mockBwrapInstalled = true
5-
let mockSocatInstalled = true
6-
let mockBpfPath: string | null = null
7-
let mockApplyPath: string | null = null
8-
9-
// Store original Bun.which to restore later
10-
const originalBunWhich = globalThis.Bun.which
11-
12-
// Mock Bun.which directly - this avoids mock.module which affects other test files
13-
globalThis.Bun.which = ((bin: string): string | null => {
14-
if (bin === 'bwrap') {
15-
return mockBwrapInstalled ? '/usr/bin/bwrap' : null
16-
}
17-
if (bin === 'socat') {
18-
return mockSocatInstalled ? '/usr/bin/socat' : null
19-
}
20-
// For other binaries, use the original implementation
21-
return originalBunWhich(bin)
22-
}) as typeof globalThis.Bun.which
23-
24-
// Mock seccomp path functions - controls whether seccomp binaries are "found"
25-
void mock.module('../../src/sandbox/generate-seccomp-filter.js', () => ({
26-
getPreGeneratedBpfPath: () => mockBpfPath,
27-
getApplySeccompBinaryPath: () => mockApplyPath,
28-
generateSeccompFilter: () => null,
29-
cleanupSeccompFilter: () => {},
30-
}))
31-
32-
// Dynamic import AFTER mocking - this is required for mocks to take effect
33-
const { checkLinuxDependencies, getLinuxDependencyStatus } = await import(
34-
'../../src/sandbox/linux-sandbox-utils.js'
35-
)
36-
37-
// Restore original Bun.which and module mocks after all tests in this file
38-
afterAll(() => {
39-
globalThis.Bun.which = originalBunWhich
40-
mock.restore()
41-
})
42-
43-
describe('checkLinuxDependencies', () => {
44-
// Reset all mocks to "everything installed" state before each test
45-
beforeEach(() => {
46-
mockBwrapInstalled = true
47-
mockSocatInstalled = true
48-
mockBpfPath = '/path/to/filter.bpf'
49-
mockApplyPath = '/path/to/apply-seccomp'
50-
})
51-
1+
import { describe, test, expect } from 'bun:test'
2+
import {
3+
dependencyStatusToCheck,
4+
type LinuxDependencyStatus,
5+
} from '../../src/sandbox/linux-sandbox-utils.js'
6+
7+
const allPresent: LinuxDependencyStatus = {
8+
hasBwrap: true,
9+
hasSocat: true,
10+
hasSeccompBpf: true,
11+
hasSeccompApply: true,
12+
}
13+
14+
describe('dependencyStatusToCheck', () => {
5215
test('returns no errors or warnings when all dependencies present', () => {
53-
const result = checkLinuxDependencies()
16+
const result = dependencyStatusToCheck(allPresent)
5417

5518
expect(result.errors).toEqual([])
5619
expect(result.warnings).toEqual([])
5720
})
5821

5922
test('returns error when bwrap missing', () => {
60-
mockBwrapInstalled = false
61-
62-
const result = checkLinuxDependencies()
23+
const result = dependencyStatusToCheck({ ...allPresent, hasBwrap: false })
6324

64-
expect(result.errors).toContain('bubblewrap (bwrap) not installed')
65-
expect(result.errors.length).toBe(1)
25+
expect(result.errors).toEqual(['bubblewrap (bwrap) not installed'])
26+
expect(result.warnings).toEqual([])
6627
})
6728

6829
test('returns error when socat missing', () => {
69-
mockSocatInstalled = false
70-
71-
const result = checkLinuxDependencies()
30+
const result = dependencyStatusToCheck({ ...allPresent, hasSocat: false })
7231

73-
expect(result.errors).toContain('socat not installed')
74-
expect(result.errors.length).toBe(1)
32+
expect(result.errors).toEqual(['socat not installed'])
33+
expect(result.warnings).toEqual([])
7534
})
7635

7736
test('returns multiple errors when both bwrap and socat missing', () => {
78-
mockBwrapInstalled = false
79-
mockSocatInstalled = false
80-
81-
const result = checkLinuxDependencies()
37+
const result = dependencyStatusToCheck({
38+
...allPresent,
39+
hasBwrap: false,
40+
hasSocat: false,
41+
})
8242

8343
expect(result.errors).toContain('bubblewrap (bwrap) not installed')
8444
expect(result.errors).toContain('socat not installed')
8545
expect(result.errors.length).toBe(2)
8646
})
8747

88-
test('returns warning (not error) when seccomp missing', () => {
89-
mockBpfPath = null
90-
mockApplyPath = null
91-
92-
const result = checkLinuxDependencies()
48+
test('returns warning (not error) when seccomp bpf missing', () => {
49+
const result = dependencyStatusToCheck({
50+
...allPresent,
51+
hasSeccompBpf: false,
52+
})
9353

94-
expect(result.warnings).toContain(
54+
expect(result.errors).toEqual([])
55+
expect(result.warnings).toEqual([
9556
'seccomp not available - unix socket access not restricted',
96-
)
57+
])
9758
})
9859

99-
test('returns warning when only bpf file present (no apply binary)', () => {
100-
mockBpfPath = '/path/to/filter.bpf'
101-
mockApplyPath = null
102-
103-
const result = checkLinuxDependencies()
60+
test('returns warning when seccomp apply binary missing', () => {
61+
const result = dependencyStatusToCheck({
62+
...allPresent,
63+
hasSeccompApply: false,
64+
})
10465

10566
expect(result.errors).toEqual([])
106-
expect(result.warnings.length).toBe(1)
67+
expect(result.warnings).toEqual([
68+
'seccomp not available - unix socket access not restricted',
69+
])
10770
})
10871

109-
// This verifies the config parameter is actually passed through
110-
test('uses custom seccomp paths when provided', () => {
111-
// Default paths return null (not found)
112-
mockBpfPath = null
113-
mockApplyPath = null
114-
115-
// But we're passing custom paths - the mock ignores them,
116-
// so this still returns warnings. The point is it doesn't crash
117-
// and the structure is correct. Real path validation happens in the mock.
118-
const result = checkLinuxDependencies({
119-
bpfPath: '/custom/path.bpf',
120-
applyPath: '/custom/apply',
72+
test('returns single warning when both seccomp pieces missing', () => {
73+
const result = dependencyStatusToCheck({
74+
...allPresent,
75+
hasSeccompBpf: false,
76+
hasSeccompApply: false,
12177
})
12278

123-
expect(Array.isArray(result.errors)).toBe(true)
124-
expect(Array.isArray(result.warnings)).toBe(true)
125-
})
126-
})
127-
128-
describe('getLinuxDependencyStatus', () => {
129-
beforeEach(() => {
130-
mockBwrapInstalled = true
131-
mockSocatInstalled = true
132-
mockBpfPath = '/path/to/filter.bpf'
133-
mockApplyPath = '/path/to/apply-seccomp'
134-
})
135-
136-
// All deps installed = all flags true
137-
test('reports all available when everything installed', () => {
138-
const status = getLinuxDependencyStatus()
139-
140-
expect(status.hasBwrap).toBe(true)
141-
expect(status.hasSocat).toBe(true)
142-
expect(status.hasSeccompBpf).toBe(true)
143-
expect(status.hasSeccompApply).toBe(true)
144-
})
145-
146-
// Each missing dep should show as false independently
147-
test('reports bwrap unavailable when not installed', () => {
148-
mockBwrapInstalled = false
149-
150-
const status = getLinuxDependencyStatus()
151-
152-
expect(status.hasBwrap).toBe(false)
153-
expect(status.hasSocat).toBe(true) // others unaffected
154-
})
155-
156-
test('reports socat unavailable when not installed', () => {
157-
mockSocatInstalled = false
158-
159-
const status = getLinuxDependencyStatus()
160-
161-
expect(status.hasSocat).toBe(false)
162-
expect(status.hasBwrap).toBe(true) // others unaffected
79+
expect(result.errors).toEqual([])
80+
expect(result.warnings.length).toBe(1)
16381
})
16482

165-
test('reports seccomp unavailable when files missing', () => {
166-
mockBpfPath = null
167-
mockApplyPath = null
168-
169-
const status = getLinuxDependencyStatus()
83+
test('reports both errors and warnings when everything missing', () => {
84+
const result = dependencyStatusToCheck({
85+
hasBwrap: false,
86+
hasSocat: false,
87+
hasSeccompBpf: false,
88+
hasSeccompApply: false,
89+
})
17090

171-
expect(status.hasSeccompBpf).toBe(false)
172-
expect(status.hasSeccompApply).toBe(false)
173-
expect(status.hasBwrap).toBe(true) // others unaffected
174-
expect(status.hasSocat).toBe(true)
91+
expect(result.errors.length).toBe(2)
92+
expect(result.warnings.length).toBe(1)
17593
})
17694
})

0 commit comments

Comments
 (0)