Skip to content

Commit dfb70ff

Browse files
committed
refactor(cli): audit fixes — validate timeoutMs, extract mock factory, add comments
1 parent 1164898 commit dfb70ff

8 files changed

Lines changed: 137 additions & 135 deletions

File tree

ui/cli/src/client/lifecycle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const executeCommand = async (options: ExecuteOptions): Promise<void> =>
5656
let connectTimeoutId: ReturnType<typeof setTimeout> | undefined
5757
try {
5858
const connectPromise = client.connect()
59+
// Prevent unhandled rejection when timeout wins the race and connect rejects later
5960
connectPromise.catch(() => undefined)
6061
await Promise.race([
6162
connectPromise,

ui/cli/src/client/ws-adapter.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import type { WebSocketLike } from 'ui-common'
2-
import type { WebSocketReadyState } from 'ui-common'
1+
import type { WebSocketLike, WebSocketReadyState } from 'ui-common'
32
import type { WebSocket as WsWebSocket } from 'ws'
43

54
import { Buffer } from 'node:buffer'

ui/cli/src/output/table.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { type ResponsePayload, ResponseStatus } from 'ui-common'
55

66
export const outputTable = (payload: ResponsePayload): void => {
77
if (payload.hashIdsSucceeded != null && payload.hashIdsSucceeded.length > 0) {
8-
process.stdout.write(
9-
chalk.green(`✓ Succeeded (${payload.hashIdsSucceeded.length.toString()}):\n`)
10-
)
8+
process.stdout.write(chalk.green(`✓ Succeeded (${String(payload.hashIdsSucceeded.length)}):\n`))
119
const table = new Table({ head: [chalk.white('Hash ID')] })
1210
for (const id of payload.hashIdsSucceeded) {
1311
table.push([id])
@@ -16,7 +14,7 @@ export const outputTable = (payload: ResponsePayload): void => {
1614
}
1715

1816
if (payload.hashIdsFailed != null && payload.hashIdsFailed.length > 0) {
19-
process.stderr.write(chalk.red(`✗ Failed (${payload.hashIdsFailed.length.toString()}):\n`))
17+
process.stderr.write(chalk.red(`✗ Failed (${String(payload.hashIdsFailed.length)}):\n`))
2018
if (payload.responsesFailed != null && payload.responsesFailed.length > 0) {
2119
const table = new Table({ head: [chalk.white('Hash ID'), chalk.white('Error')] })
2220
for (const entry of payload.responsesFailed) {

ui/cli/tests/lifecycle.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @file Unit tests for CLI lifecycle and error types
3+
* @description Tests for connection lifecycle management and error handling
4+
*/
5+
16
import assert from 'node:assert'
27
import { describe, it } from 'node:test'
38

ui/cli/tests/output.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @file Unit tests for CLI output formatters (JSON and table)
3+
* @description Tests for JSON and table output formatting functions
4+
*/
5+
16
import assert from 'node:assert'
27
import { describe, it } from 'node:test'
38
import { ResponseStatus } from 'ui-common'

ui/cli/tests/ws-adapter.test.ts

Lines changed: 58 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @file Unit tests for the WebSocket adapter (ws → WebSocketLike)
3+
* @description Tests for converting ws library WebSocket to WebSocketLike interface
4+
*/
5+
16
import type { WebSocket } from 'ws'
27

38
import assert from 'node:assert'
@@ -17,17 +22,19 @@ interface MockWs {
1722
send: (data: string) => void
1823
}
1924

25+
const createMockWs = (): MockWs => ({
26+
close: () => undefined,
27+
onclose: null,
28+
onerror: null,
29+
onmessage: null,
30+
onopen: null,
31+
readyState: WebSocketReadyState.OPEN,
32+
send: () => undefined,
33+
})
34+
2035
await describe('WS Adapter', async () => {
2136
await it('should convert Buffer data to string in onmessage', () => {
22-
const mockWs: MockWs = {
23-
close: () => undefined,
24-
onclose: null,
25-
onerror: null,
26-
onmessage: null,
27-
onopen: null,
28-
readyState: WebSocketReadyState.OPEN,
29-
send: () => undefined,
30-
}
37+
const mockWs = createMockWs()
3138

3239
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
3340

@@ -43,15 +50,7 @@ await describe('WS Adapter', async () => {
4350
})
4451

4552
await it('should convert ArrayBuffer data to string in onmessage', () => {
46-
const mockWs: MockWs = {
47-
close: () => undefined,
48-
onclose: null,
49-
onerror: null,
50-
onmessage: null,
51-
onopen: null,
52-
readyState: WebSocketReadyState.OPEN,
53-
send: () => undefined,
54-
}
53+
const mockWs = createMockWs()
5554

5655
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
5756

@@ -67,15 +66,7 @@ await describe('WS Adapter', async () => {
6766
})
6867

6968
await it('should convert Buffer[] data to string in onmessage', () => {
70-
const mockWs: MockWs = {
71-
close: () => undefined,
72-
onclose: null,
73-
onerror: null,
74-
onmessage: null,
75-
onopen: null,
76-
readyState: WebSocketReadyState.OPEN,
77-
send: () => undefined,
78-
}
69+
const mockWs = createMockWs()
7970

8071
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
8172

@@ -91,15 +82,7 @@ await describe('WS Adapter', async () => {
9182
})
9283

9384
await it('should pass through string data in onmessage', () => {
94-
const mockWs: MockWs = {
95-
close: () => undefined,
96-
onclose: null,
97-
onerror: null,
98-
onmessage: null,
99-
onopen: null,
100-
readyState: WebSocketReadyState.OPEN,
101-
send: () => undefined,
102-
}
85+
const mockWs = createMockWs()
10386

10487
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
10588

@@ -114,15 +97,8 @@ await describe('WS Adapter', async () => {
11497
})
11598

11699
await it('should delegate readyState getter to ws', () => {
117-
const mockWs: MockWs = {
118-
close: () => undefined,
119-
onclose: null,
120-
onerror: null,
121-
onmessage: null,
122-
onopen: null,
123-
readyState: WebSocketReadyState.CONNECTING,
124-
send: () => undefined,
125-
}
100+
const mockWs = createMockWs()
101+
mockWs.readyState = WebSocketReadyState.CONNECTING
126102

127103
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
128104

@@ -131,16 +107,9 @@ await describe('WS Adapter', async () => {
131107

132108
await it('should delegate send() to ws', () => {
133109
let sentData: string | undefined
134-
const mockWs: MockWs = {
135-
close: () => undefined,
136-
onclose: null,
137-
onerror: null,
138-
onmessage: null,
139-
onopen: null,
140-
readyState: WebSocketReadyState.OPEN,
141-
send: (data: string) => {
142-
sentData = data
143-
},
110+
const mockWs = createMockWs()
111+
mockWs.send = (data: string) => {
112+
sentData = data
144113
}
145114

146115
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
@@ -152,17 +121,10 @@ await describe('WS Adapter', async () => {
152121
await it('should delegate close() to ws', () => {
153122
let closeCode: number | undefined
154123
let closeReason: string | undefined
155-
const mockWs: MockWs = {
156-
close: (code?: number, reason?: string) => {
157-
closeCode = code
158-
closeReason = reason
159-
},
160-
onclose: null,
161-
onerror: null,
162-
onmessage: null,
163-
onopen: null,
164-
readyState: WebSocketReadyState.OPEN,
165-
send: () => undefined,
124+
const mockWs = createMockWs()
125+
mockWs.close = (code?: number, reason?: string) => {
126+
closeCode = code
127+
closeReason = reason
166128
}
167129

168130
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
@@ -173,15 +135,7 @@ await describe('WS Adapter', async () => {
173135
})
174136

175137
await it('should forward onerror event with error shape', () => {
176-
const mockWs: MockWs = {
177-
close: () => undefined,
178-
onclose: null,
179-
onerror: null,
180-
onmessage: null,
181-
onopen: null,
182-
readyState: WebSocketReadyState.OPEN,
183-
send: () => undefined,
184-
}
138+
const mockWs = createMockWs()
185139

186140
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
187141

@@ -202,16 +156,31 @@ await describe('WS Adapter', async () => {
202156
assert.strictEqual(receivedMessage, 'connection failed')
203157
})
204158

205-
await it('should forward onclose event with code and reason', () => {
206-
const mockWs: MockWs = {
207-
close: () => undefined,
208-
onclose: null,
209-
onerror: null,
210-
onmessage: null,
211-
onopen: null,
212-
readyState: WebSocketReadyState.CLOSED,
213-
send: () => undefined,
159+
await it('should forward onerror when event is a string', () => {
160+
const mockWs = createMockWs()
161+
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
162+
let receivedMessage = ''
163+
adapter.onerror = event => {
164+
receivedMessage = event.message
214165
}
166+
mockWs.onerror?.('connection refused')
167+
assert.strictEqual(receivedMessage, 'connection refused')
168+
})
169+
170+
await it('should forward onerror with fallback for unknown event type', () => {
171+
const mockWs = createMockWs()
172+
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
173+
let receivedMessage = ''
174+
adapter.onerror = event => {
175+
receivedMessage = event.message
176+
}
177+
mockWs.onerror?.(42 as unknown as Error)
178+
assert.strictEqual(receivedMessage, 'Unknown error')
179+
})
180+
181+
await it('should forward onclose event with code and reason', () => {
182+
const mockWs = createMockWs()
183+
mockWs.readyState = WebSocketReadyState.CLOSED
215184

216185
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
217186

@@ -229,15 +198,7 @@ await describe('WS Adapter', async () => {
229198
})
230199

231200
await it('should forward onopen event', () => {
232-
const mockWs: MockWs = {
233-
close: () => undefined,
234-
onclose: null,
235-
onerror: null,
236-
onmessage: null,
237-
onopen: null,
238-
readyState: WebSocketReadyState.OPEN,
239-
send: () => undefined,
240-
}
201+
const mockWs = createMockWs()
241202

242203
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
243204

@@ -252,15 +213,7 @@ await describe('WS Adapter', async () => {
252213
})
253214

254215
await it('should have getter and setter for onmessage', () => {
255-
const mockWs: MockWs = {
256-
close: () => undefined,
257-
onclose: null,
258-
onerror: null,
259-
onmessage: null,
260-
onopen: null,
261-
readyState: WebSocketReadyState.OPEN,
262-
send: () => undefined,
263-
}
216+
const mockWs = createMockWs()
264217

265218
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
266219

@@ -277,15 +230,7 @@ await describe('WS Adapter', async () => {
277230
})
278231

279232
await it('should have getter and setter for onerror', () => {
280-
const mockWs: MockWs = {
281-
close: () => undefined,
282-
onclose: null,
283-
onerror: null,
284-
onmessage: null,
285-
onopen: null,
286-
readyState: WebSocketReadyState.OPEN,
287-
send: () => undefined,
288-
}
233+
const mockWs = createMockWs()
289234

290235
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
291236

@@ -302,15 +247,7 @@ await describe('WS Adapter', async () => {
302247
})
303248

304249
await it('should have getter and setter for onclose', () => {
305-
const mockWs: MockWs = {
306-
close: () => undefined,
307-
onclose: null,
308-
onerror: null,
309-
onmessage: null,
310-
onopen: null,
311-
readyState: WebSocketReadyState.OPEN,
312-
send: () => undefined,
313-
}
250+
const mockWs = createMockWs()
314251

315252
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
316253

@@ -327,15 +264,7 @@ await describe('WS Adapter', async () => {
327264
})
328265

329266
await it('should have getter and setter for onopen', () => {
330-
const mockWs: MockWs = {
331-
close: () => undefined,
332-
onclose: null,
333-
onerror: null,
334-
onmessage: null,
335-
onopen: null,
336-
readyState: WebSocketReadyState.OPEN,
337-
send: () => undefined,
338-
}
267+
const mockWs = createMockWs()
339268

340269
const adapter = createWsAdapter(mockWs as unknown as WebSocket)
341270

ui/common/src/client/WebSocketClient.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ export class WebSocketClient {
109109
const uuid = randomUUID()
110110
const message = JSON.stringify([uuid, procedureName, payload])
111111
const effectiveTimeoutMs = timeoutMs ?? this.timeoutMs
112+
if (!Number.isFinite(effectiveTimeoutMs) || effectiveTimeoutMs <= 0) {
113+
reject(new Error(`Invalid timeout: ${String(effectiveTimeoutMs)}ms (must be > 0)`))
114+
return
115+
}
112116
const timeoutId = setTimeout(() => {
113117
this.responseHandlers.delete(uuid)
114118
reject(

0 commit comments

Comments
 (0)