Skip to content

Commit 32b0200

Browse files
committed
test(cli): add unit tests for more utils and output modules
Add comprehensive test coverage for: - output-requirements.mts (Conda to requirements.txt conversion) - utils/data/result.mts (CResult helpers: requireOk, mapResult, etc.) - utils/fs/find-up.mts (file search up directory tree) - utils/ipc.mts (IPC data handling) - utils/project/context.mts (project detection: package manager, framework) - utils/update/manager.mts (update checking for npm installs) - utils/update/notifier.mts (update notification formatting) - utils/validation/common.mts (common validation patterns) - utils/validation/ipc.mts (IPC message validation) Coverage increased from ~70.1% to ~71.08%.
1 parent fa74c74 commit 32b0200

File tree

9 files changed

+2540
-0
lines changed

9 files changed

+2540
-0
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/**
2+
* Unit tests for requirements output formatting.
3+
*
4+
* Purpose:
5+
* Tests the output formatting for Conda to requirements.txt conversion results.
6+
*
7+
* Test Coverage:
8+
* - outputRequirements function
9+
* - JSON output format with file and stdout
10+
* - Markdown output format with file and stdout
11+
* - Text output format with file and stdout
12+
* - Error handling
13+
*
14+
* Related Files:
15+
* - src/commands/manifest/output-requirements.mts (implementation)
16+
*/
17+
18+
import { beforeEach, describe, expect, it, vi } from 'vitest'
19+
20+
// Mock fs.
21+
const mockWriteFileSync = vi.hoisted(() => vi.fn())
22+
vi.mock('node:fs', () => ({
23+
default: {
24+
writeFileSync: mockWriteFileSync,
25+
},
26+
}))
27+
28+
// Mock logger.
29+
const mockLogger = vi.hoisted(() => ({
30+
log: vi.fn(),
31+
error: vi.fn(),
32+
warn: vi.fn(),
33+
fail: vi.fn(),
34+
success: vi.fn(),
35+
info: vi.fn(),
36+
}))
37+
vi.mock('@socketsecurity/lib/logger', () => ({
38+
getDefaultLogger: () => mockLogger,
39+
}))
40+
41+
// Mock utilities.
42+
vi.mock('../../../../src/utils/error/fail-msg-with-badge.mts', () => ({
43+
failMsgWithBadge: (msg: string, cause?: string) =>
44+
cause ? `${msg}: ${cause}` : msg,
45+
}))
46+
47+
vi.mock('../../../../src/utils/output/markdown.mts', () => ({
48+
mdHeader: (text: string, level = 1) => `${'#'.repeat(level)} ${text}`,
49+
}))
50+
51+
vi.mock('../../../../src/utils/output/result-json.mjs', () => ({
52+
serializeResultJson: (result: unknown) => JSON.stringify(result, null, 2),
53+
}))
54+
55+
import { outputRequirements } from '../../../../src/commands/manifest/output-requirements.mts'
56+
57+
import type { CResult } from '../../../../src/types.mts'
58+
59+
describe('output-requirements', () => {
60+
beforeEach(() => {
61+
vi.clearAllMocks()
62+
process.exitCode = undefined
63+
})
64+
65+
describe('outputRequirements', () => {
66+
const mockSuccessData = {
67+
content: 'name: myenv\ndependencies:\n - numpy\n - pandas',
68+
pip: 'numpy\npandas',
69+
}
70+
71+
describe('JSON output', () => {
72+
it('outputs success result as JSON to stdout', async () => {
73+
const result: CResult<typeof mockSuccessData> = {
74+
ok: true,
75+
data: mockSuccessData,
76+
}
77+
78+
await outputRequirements(result, 'json', '-')
79+
80+
expect(mockLogger.log).toHaveBeenCalledWith(
81+
expect.stringContaining('"ok": true'),
82+
)
83+
expect(mockWriteFileSync).not.toHaveBeenCalled()
84+
})
85+
86+
it('writes JSON to file when path provided', async () => {
87+
const result: CResult<typeof mockSuccessData> = {
88+
ok: true,
89+
data: mockSuccessData,
90+
}
91+
92+
await outputRequirements(result, 'json', '/output/result.json')
93+
94+
expect(mockWriteFileSync).toHaveBeenCalledWith(
95+
'/output/result.json',
96+
expect.stringContaining('"ok": true'),
97+
'utf8',
98+
)
99+
expect(mockLogger.log).not.toHaveBeenCalled()
100+
})
101+
102+
it('outputs error result as JSON', async () => {
103+
const result: CResult<typeof mockSuccessData> = {
104+
ok: false,
105+
message: 'Conversion failed',
106+
}
107+
108+
await outputRequirements(result, 'json', '-')
109+
110+
expect(mockLogger.log).toHaveBeenCalledWith(
111+
expect.stringContaining('"ok": false'),
112+
)
113+
expect(process.exitCode).toBe(1)
114+
})
115+
})
116+
117+
describe('Markdown output', () => {
118+
it('outputs converted conda file as markdown to stdout', async () => {
119+
const result: CResult<typeof mockSuccessData> = {
120+
ok: true,
121+
data: mockSuccessData,
122+
}
123+
124+
await outputRequirements(result, 'markdown', '-')
125+
126+
const loggedMd = mockLogger.log.mock.calls[0]![0]
127+
expect(loggedMd).toContain('# Converted Conda file')
128+
expect(loggedMd).toContain('environment.yml')
129+
expect(loggedMd).toContain('requirements.txt')
130+
expect(loggedMd).toContain('numpy\npandas')
131+
expect(loggedMd).toContain('```')
132+
})
133+
134+
it('writes markdown to file when path provided', async () => {
135+
const result: CResult<typeof mockSuccessData> = {
136+
ok: true,
137+
data: mockSuccessData,
138+
}
139+
140+
await outputRequirements(result, 'markdown', '/output/result.md')
141+
142+
expect(mockWriteFileSync).toHaveBeenCalledWith(
143+
'/output/result.md',
144+
expect.stringContaining('Converted Conda file'),
145+
'utf8',
146+
)
147+
expect(mockLogger.log).not.toHaveBeenCalled()
148+
})
149+
150+
it('includes pip content in code block', async () => {
151+
const result: CResult<typeof mockSuccessData> = {
152+
ok: true,
153+
data: { content: 'yaml', pip: 'flask>=2.0\nrequests' },
154+
}
155+
156+
await outputRequirements(result, 'markdown', '-')
157+
158+
const loggedMd = mockLogger.log.mock.calls[0]![0]
159+
expect(loggedMd).toContain('flask>=2.0\nrequests')
160+
})
161+
})
162+
163+
describe('Text output', () => {
164+
it('outputs pip content to stdout', async () => {
165+
const result: CResult<typeof mockSuccessData> = {
166+
ok: true,
167+
data: mockSuccessData,
168+
}
169+
170+
await outputRequirements(result, 'text', '-')
171+
172+
expect(mockLogger.log).toHaveBeenCalledWith('numpy\npandas')
173+
// Also outputs empty line.
174+
expect(mockLogger.log).toHaveBeenCalledWith('')
175+
})
176+
177+
it('writes pip content to file when path provided', async () => {
178+
const result: CResult<typeof mockSuccessData> = {
179+
ok: true,
180+
data: mockSuccessData,
181+
}
182+
183+
await outputRequirements(result, 'text', '/output/requirements.txt')
184+
185+
expect(mockWriteFileSync).toHaveBeenCalledWith(
186+
'/output/requirements.txt',
187+
'numpy\npandas',
188+
'utf8',
189+
)
190+
expect(mockLogger.log).not.toHaveBeenCalled()
191+
})
192+
})
193+
194+
describe('Error handling', () => {
195+
it('outputs error with fail message for non-JSON', async () => {
196+
const result: CResult<typeof mockSuccessData> = {
197+
ok: false,
198+
message: 'Conversion failed',
199+
cause: 'Invalid YAML format',
200+
}
201+
202+
await outputRequirements(result, 'text', '-')
203+
204+
expect(mockLogger.fail).toHaveBeenCalledWith(
205+
expect.stringContaining('Conversion failed'),
206+
)
207+
expect(process.exitCode).toBe(1)
208+
})
209+
210+
it('uses custom exit code when provided', async () => {
211+
const result: CResult<typeof mockSuccessData> = {
212+
ok: false,
213+
message: 'Failed',
214+
code: 127,
215+
}
216+
217+
await outputRequirements(result, 'text', '-')
218+
219+
expect(process.exitCode).toBe(127)
220+
})
221+
222+
it('sets exit code before processing for error result', async () => {
223+
const result: CResult<typeof mockSuccessData> = {
224+
ok: false,
225+
message: 'Failed',
226+
code: 2,
227+
}
228+
229+
await outputRequirements(result, 'json', '-')
230+
231+
expect(process.exitCode).toBe(2)
232+
})
233+
})
234+
})
235+
})

0 commit comments

Comments
 (0)