Skip to content

Commit 588c614

Browse files
committed
test(dlx): cover spawn-coana to 100%
Adds unit tests for spawnCoana, spawnCoanaDlx, and spawnCoanaVfs covering local binary/script execution, SEA-VFS routing, env-var mixin (API token, org slug, proxy), DLX fallback, stderr/cause error extraction, and the auto-dispatch decision tree.
1 parent a6bd2f1 commit 588c614

1 file changed

Lines changed: 353 additions & 0 deletions

File tree

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
/**
2+
* Unit tests for utils/dlx/spawn-coana.
3+
*
4+
* Covers spawnCoana, spawnCoanaDlx, and spawnCoanaVfs paths.
5+
*
6+
* Related Files:
7+
* - src/utils/dlx/spawn-coana.mts
8+
*/
9+
10+
import { beforeEach, describe, expect, it, vi } from 'vitest'
11+
12+
const mockSpawn = vi.hoisted(() => vi.fn())
13+
const mockSpawnDlx = vi.hoisted(() => vi.fn())
14+
const mockSpawnToolVfs = vi.hoisted(() => vi.fn())
15+
const mockResolveCoana = vi.hoisted(() => vi.fn())
16+
const mockDetectExecutableType = vi.hoisted(() => vi.fn())
17+
const mockAreExternalToolsAvailable = vi.hoisted(() => vi.fn(() => false))
18+
const mockIsSeaBinary = vi.hoisted(() => vi.fn(() => false))
19+
const mockGetCliVersion = vi.hoisted(() => vi.fn(() => '1.2.3'))
20+
const mockGetDefaultApiToken = vi.hoisted(() => vi.fn(() => undefined))
21+
const mockGetDefaultProxyUrl = vi.hoisted(() => vi.fn(() => undefined))
22+
const mockGetDefaultOrgSlug = vi.hoisted(() =>
23+
vi.fn(async () => ({ ok: false, message: 'no org' })),
24+
)
25+
const mockGetErrorCause = vi.hoisted(() => vi.fn((e: unknown) => String(e)))
26+
27+
vi.mock('@socketsecurity/lib/spawn', () => ({
28+
spawn: mockSpawn,
29+
}))
30+
31+
vi.mock('@socketsecurity/lib/dlx/detect', () => ({
32+
detectExecutableType: mockDetectExecutableType,
33+
}))
34+
35+
vi.mock('../../../../src/utils/dlx/spawn.mts', () => ({
36+
spawnDlx: mockSpawnDlx,
37+
spawnToolVfs: mockSpawnToolVfs,
38+
}))
39+
40+
vi.mock('../../../../src/utils/dlx/resolve-binary.mts', () => ({
41+
resolveCoana: mockResolveCoana,
42+
}))
43+
44+
vi.mock('../../../../src/utils/dlx/vfs-extract.mts', () => ({
45+
areExternalToolsAvailable: mockAreExternalToolsAvailable,
46+
}))
47+
48+
vi.mock('../../../../src/commands/ci/fetch-default-org-slug.mts', () => ({
49+
getDefaultOrgSlug: mockGetDefaultOrgSlug,
50+
}))
51+
52+
vi.mock('../../../../src/env/cli-version.mts', () => ({
53+
getCliVersion: mockGetCliVersion,
54+
}))
55+
56+
vi.mock('../../../../src/utils/error/errors.mts', () => ({
57+
getErrorCause: mockGetErrorCause,
58+
}))
59+
60+
vi.mock('../../../../src/utils/sea/detect.mts', () => ({
61+
isSeaBinary: mockIsSeaBinary,
62+
}))
63+
64+
vi.mock('../../../../src/utils/socket/sdk.mts', () => ({
65+
getDefaultApiToken: mockGetDefaultApiToken,
66+
getDefaultProxyUrl: mockGetDefaultProxyUrl,
67+
}))
68+
69+
import {
70+
spawnCoana,
71+
spawnCoanaDlx,
72+
spawnCoanaVfs,
73+
} from '../../../../src/utils/dlx/spawn-coana.mts'
74+
75+
describe('spawnCoanaDlx', () => {
76+
beforeEach(() => {
77+
vi.clearAllMocks()
78+
mockGetCliVersion.mockReturnValue('1.2.3')
79+
mockGetDefaultApiToken.mockReturnValue(undefined)
80+
mockGetDefaultProxyUrl.mockReturnValue(undefined)
81+
mockGetDefaultOrgSlug.mockResolvedValue({ ok: false, message: 'no org' })
82+
mockIsSeaBinary.mockReturnValue(false)
83+
mockAreExternalToolsAvailable.mockReturnValue(false)
84+
})
85+
86+
it('runs a local coana binary when resolution is local + binary', async () => {
87+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
88+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
89+
mockSpawn.mockResolvedValue({ stdout: Buffer.from('hello') })
90+
91+
const result = await spawnCoanaDlx(['scan'], 'my-org', undefined, undefined)
92+
93+
expect(mockSpawn).toHaveBeenCalledWith(
94+
'/local/coana',
95+
['scan'],
96+
expect.objectContaining({ stdio: 'inherit' }),
97+
)
98+
expect(result).toEqual({ ok: true, data: 'hello' })
99+
})
100+
101+
it('runs the local coana.js via node when not a binary', async () => {
102+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana.js' })
103+
mockDetectExecutableType.mockReturnValue({ type: 'script' })
104+
mockSpawn.mockResolvedValue({ stdout: Buffer.from('') })
105+
106+
await spawnCoanaDlx([], undefined, undefined, undefined)
107+
108+
expect(mockSpawn).toHaveBeenCalledWith(
109+
'node',
110+
['/local/coana.js'],
111+
expect.any(Object),
112+
)
113+
})
114+
115+
it('mixes in SOCKET_CLI_API_TOKEN when getDefaultApiToken returns a value', async () => {
116+
mockGetDefaultApiToken.mockReturnValue('tok-xyz')
117+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
118+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
119+
mockSpawn.mockResolvedValue({ stdout: undefined })
120+
121+
await spawnCoanaDlx([], 'org', undefined, undefined)
122+
123+
const call = mockSpawn.mock.calls[0]
124+
expect(call[2].env.SOCKET_CLI_API_TOKEN).toBe('tok-xyz')
125+
expect(call[2].env.SOCKET_ORG_SLUG).toBe('org')
126+
})
127+
128+
it('uses default org slug when none passed and getDefaultOrgSlug succeeds', async () => {
129+
mockGetDefaultOrgSlug.mockResolvedValue({ ok: true, data: 'auto-org' })
130+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
131+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
132+
mockSpawn.mockResolvedValue({ stdout: undefined })
133+
134+
await spawnCoanaDlx([], undefined, undefined, undefined)
135+
136+
const call = mockSpawn.mock.calls[0]
137+
expect(call[2].env.SOCKET_ORG_SLUG).toBe('auto-org')
138+
})
139+
140+
it('mixes in SOCKET_CLI_API_PROXY when getDefaultProxyUrl returns a value', async () => {
141+
mockGetDefaultProxyUrl.mockReturnValue('http://proxy:8080')
142+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
143+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
144+
mockSpawn.mockResolvedValue({ stdout: undefined })
145+
146+
await spawnCoanaDlx([], 'org', undefined, undefined)
147+
148+
const call = mockSpawn.mock.calls[0]
149+
expect(call[2].env.SOCKET_CLI_API_PROXY).toBe('http://proxy:8080')
150+
})
151+
152+
it('honors a custom stdio passed via spawnExtra', async () => {
153+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
154+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
155+
mockSpawn.mockResolvedValue({ stdout: undefined })
156+
157+
await spawnCoanaDlx([], 'org', undefined, { stdio: 'pipe' } as never)
158+
159+
expect(mockSpawn).toHaveBeenCalledWith(
160+
'/local/coana',
161+
[],
162+
expect.objectContaining({ stdio: 'pipe' }),
163+
)
164+
})
165+
166+
it('falls back to spawnDlx when resolution.type is "dlx"', async () => {
167+
mockResolveCoana.mockReturnValue({
168+
type: 'dlx',
169+
details: { name: '@coana-tech/cli', version: '1.0.0' },
170+
})
171+
mockSpawnDlx.mockResolvedValue({
172+
spawnPromise: Promise.resolve({ stdout: Buffer.from('dlx-out') }),
173+
})
174+
175+
const result = await spawnCoanaDlx([], 'org', undefined, undefined)
176+
177+
expect(mockSpawnDlx).toHaveBeenCalled()
178+
expect(result).toEqual({ ok: true, data: 'dlx-out' })
179+
})
180+
181+
it('uses coanaVersion override when provided', async () => {
182+
mockResolveCoana.mockReturnValue({
183+
type: 'dlx',
184+
details: { name: '@coana-tech/cli', version: '1.0.0' },
185+
})
186+
mockSpawnDlx.mockResolvedValue({
187+
spawnPromise: Promise.resolve({ stdout: undefined }),
188+
})
189+
190+
await spawnCoanaDlx([], 'org', { coanaVersion: '2.0.0' }, undefined)
191+
192+
expect(mockSpawnDlx).toHaveBeenCalledWith(
193+
expect.objectContaining({ version: '2.0.0' }),
194+
expect.any(Array),
195+
expect.any(Object),
196+
undefined,
197+
)
198+
})
199+
200+
it('throws when resolveCoana returns an unexpected type', async () => {
201+
mockResolveCoana.mockReturnValue({
202+
type: 'github-release',
203+
details: {} as never,
204+
})
205+
206+
const result = await spawnCoanaDlx([], 'org', undefined, undefined)
207+
208+
expect(result.ok).toBe(false)
209+
})
210+
211+
it('returns ok:false with error message when spawn rejects', async () => {
212+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
213+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
214+
const err = Object.assign(new Error('spawn failed'), {
215+
stderr: 'stderr text',
216+
})
217+
mockSpawn.mockRejectedValue(err)
218+
219+
const result = await spawnCoanaDlx([], 'org', undefined, undefined)
220+
221+
expect(result.ok).toBe(false)
222+
expect((result as { message?: string }).message).toBe('stderr text')
223+
})
224+
225+
it('uses getErrorCause when no stderr present on rejection', async () => {
226+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
227+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
228+
mockSpawn.mockRejectedValue(new Error('boom'))
229+
mockGetErrorCause.mockReturnValue('error-cause-msg')
230+
231+
const result = await spawnCoanaDlx([], 'org', undefined, undefined)
232+
233+
expect(result.ok).toBe(false)
234+
expect((result as { message?: string }).message).toBe('error-cause-msg')
235+
})
236+
})
237+
238+
describe('spawnCoanaVfs', () => {
239+
beforeEach(() => {
240+
vi.clearAllMocks()
241+
mockGetCliVersion.mockReturnValue('1.2.3')
242+
mockGetDefaultApiToken.mockReturnValue(undefined)
243+
mockGetDefaultProxyUrl.mockReturnValue(undefined)
244+
mockGetDefaultOrgSlug.mockResolvedValue({ ok: false, message: 'no org' })
245+
})
246+
247+
it('spawns coana through spawnToolVfs and returns stdout', async () => {
248+
mockSpawnToolVfs.mockResolvedValue({
249+
spawnPromise: Promise.resolve({ stdout: Buffer.from('vfs-out') }),
250+
})
251+
252+
const result = await spawnCoanaVfs(['args'], undefined, undefined)
253+
254+
expect(mockSpawnToolVfs).toHaveBeenCalledWith(
255+
'coana',
256+
['args'],
257+
expect.any(Object),
258+
undefined,
259+
)
260+
expect(result).toEqual({ ok: true, data: 'vfs-out' })
261+
})
262+
263+
it('returns empty string when stdout missing', async () => {
264+
mockSpawnToolVfs.mockResolvedValue({
265+
spawnPromise: Promise.resolve({ stdout: undefined }),
266+
})
267+
268+
const result = await spawnCoanaVfs([], undefined, undefined)
269+
expect(result).toEqual({ ok: true, data: '' })
270+
})
271+
272+
it('mixes API token / org slug / proxy into spawn env', async () => {
273+
mockGetDefaultApiToken.mockReturnValue('tok')
274+
mockGetDefaultOrgSlug.mockResolvedValue({ ok: true, data: 'org-1' })
275+
mockGetDefaultProxyUrl.mockReturnValue('http://proxy')
276+
mockSpawnToolVfs.mockResolvedValue({
277+
spawnPromise: Promise.resolve({ stdout: undefined }),
278+
})
279+
280+
await spawnCoanaVfs([], undefined, undefined)
281+
282+
const opts = mockSpawnToolVfs.mock.calls[0][2]
283+
expect(opts.env.SOCKET_CLI_API_TOKEN).toBe('tok')
284+
expect(opts.env.SOCKET_ORG_SLUG).toBe('org-1')
285+
expect(opts.env.SOCKET_CLI_API_PROXY).toBe('http://proxy')
286+
expect(opts.env.SOCKET_CLI_VERSION).toBe('1.2.3')
287+
})
288+
289+
it('returns ok:false with stderr when spawnToolVfs throws with stderr', async () => {
290+
const err = Object.assign(new Error('vfs boom'), { stderr: 'stderr-vfs' })
291+
mockSpawnToolVfs.mockRejectedValue(err)
292+
293+
const result = await spawnCoanaVfs([], undefined, undefined)
294+
expect(result.ok).toBe(false)
295+
expect((result as { message?: string }).message).toBe('stderr-vfs')
296+
})
297+
298+
it('returns ok:false with cause when spawnToolVfs throws without stderr', async () => {
299+
mockSpawnToolVfs.mockRejectedValue(new Error('plain'))
300+
mockGetErrorCause.mockReturnValue('plain-cause')
301+
302+
const result = await spawnCoanaVfs([], undefined, undefined)
303+
expect(result.ok).toBe(false)
304+
expect((result as { message?: string }).message).toBe('plain-cause')
305+
})
306+
})
307+
308+
describe('spawnCoana (auto-dispatch)', () => {
309+
beforeEach(() => {
310+
vi.clearAllMocks()
311+
mockGetCliVersion.mockReturnValue('1.2.3')
312+
mockGetDefaultApiToken.mockReturnValue(undefined)
313+
mockGetDefaultProxyUrl.mockReturnValue(undefined)
314+
mockGetDefaultOrgSlug.mockResolvedValue({ ok: false, message: 'no org' })
315+
})
316+
317+
it('routes to spawnCoanaVfs when SEA + external tools available', async () => {
318+
mockIsSeaBinary.mockReturnValue(true)
319+
mockAreExternalToolsAvailable.mockReturnValue(true)
320+
mockSpawnToolVfs.mockResolvedValue({
321+
spawnPromise: Promise.resolve({ stdout: Buffer.from('via-vfs') }),
322+
})
323+
324+
const result = await spawnCoana(['x'], 'org', undefined, undefined)
325+
326+
expect(mockSpawnToolVfs).toHaveBeenCalled()
327+
expect(result).toEqual({ ok: true, data: 'via-vfs' })
328+
})
329+
330+
it('routes to spawnCoanaDlx when not in SEA mode', async () => {
331+
mockIsSeaBinary.mockReturnValue(false)
332+
mockAreExternalToolsAvailable.mockReturnValue(false)
333+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
334+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
335+
mockSpawn.mockResolvedValue({ stdout: Buffer.from('via-dlx') })
336+
337+
const result = await spawnCoana(['x'], 'org', undefined, undefined)
338+
339+
expect(mockSpawn).toHaveBeenCalled()
340+
expect(result).toEqual({ ok: true, data: 'via-dlx' })
341+
})
342+
343+
it('routes to spawnCoanaDlx when SEA but external tools missing', async () => {
344+
mockIsSeaBinary.mockReturnValue(true)
345+
mockAreExternalToolsAvailable.mockReturnValue(false)
346+
mockResolveCoana.mockReturnValue({ type: 'local', path: '/local/coana' })
347+
mockDetectExecutableType.mockReturnValue({ type: 'binary' })
348+
mockSpawn.mockResolvedValue({ stdout: Buffer.from('') })
349+
350+
await spawnCoana(['x'], 'org', undefined, undefined)
351+
expect(mockSpawn).toHaveBeenCalled()
352+
})
353+
})

0 commit comments

Comments
 (0)