Skip to content

Commit 1737295

Browse files
committed
test(ipc): cover utils:get-workload-available-tools validation
Add focused vitest coverage for the utils IPC handlers: rejection of non-object, prototype-key, out-of-range, and non-http payloads; happy paths for a full workload and an empty object; plus smoke tests for the other three handlers in the module.
1 parent 5040ccd commit 1737295

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
3+
const ctx = vi.hoisted(() => {
4+
const handlers = new Map<string, (...args: unknown[]) => Promise<unknown>>()
5+
return {
6+
handlers,
7+
getHeaders: vi.fn(() => ({ 'x-test': '1' })),
8+
getInstanceId: vi.fn().mockResolvedValue('instance-abc'),
9+
isOfficialReleaseBuild: vi.fn(() => false),
10+
getWorkloadAvailableTools: vi.fn(),
11+
}
12+
})
13+
14+
vi.mock('electron', () => ({
15+
ipcMain: {
16+
handle: (
17+
channel: string,
18+
handler: (...args: unknown[]) => Promise<unknown>
19+
) => {
20+
ctx.handlers.set(channel, handler)
21+
},
22+
},
23+
}))
24+
25+
vi.mock('../../headers', () => ({
26+
getHeaders: ctx.getHeaders,
27+
}))
28+
29+
vi.mock('../../util', () => ({
30+
getInstanceId: ctx.getInstanceId,
31+
isOfficialReleaseBuild: ctx.isOfficialReleaseBuild,
32+
}))
33+
34+
vi.mock('../../utils/mcp-tools', () => ({
35+
getWorkloadAvailableTools: ctx.getWorkloadAvailableTools,
36+
}))
37+
38+
import { register } from '../utils'
39+
40+
describe('utils IPC handlers', () => {
41+
beforeEach(() => {
42+
vi.clearAllMocks()
43+
ctx.handlers.clear()
44+
register()
45+
})
46+
47+
describe('utils:get-workload-available-tools', () => {
48+
const invoke = async (payload: unknown) => {
49+
const handler = ctx.handlers.get('utils:get-workload-available-tools')!
50+
return handler(null, payload)
51+
}
52+
53+
it.each([
54+
['null', null],
55+
['undefined', undefined],
56+
['string', 'not-an-object'],
57+
['number', 42],
58+
['array', [{ name: 'x' }]],
59+
])(
60+
'rejects non-object payload (%s) with TypeError',
61+
async (_label, bad) => {
62+
await expect(invoke(bad)).rejects.toBeInstanceOf(TypeError)
63+
expect(ctx.getWorkloadAvailableTools).not.toHaveBeenCalled()
64+
}
65+
)
66+
67+
it('rejects invalid transport_type (prototype key)', async () => {
68+
await expect(
69+
invoke({ name: 'a', transport_type: '__proto__' })
70+
).rejects.toBeInstanceOf(TypeError)
71+
await expect(
72+
invoke({ name: 'a', transport_type: 'constructor' })
73+
).rejects.toBeInstanceOf(TypeError)
74+
expect(ctx.getWorkloadAvailableTools).not.toHaveBeenCalled()
75+
})
76+
77+
it('rejects invalid proxy_mode', async () => {
78+
await expect(
79+
invoke({ name: 'a', proxy_mode: 'stdio' })
80+
).rejects.toBeInstanceOf(TypeError)
81+
})
82+
83+
it('rejects non-integer or out-of-range port', async () => {
84+
await expect(
85+
invoke({ name: 'a', port: Number.NaN })
86+
).rejects.toBeInstanceOf(TypeError)
87+
await expect(
88+
invoke({ name: 'a', port: Number.POSITIVE_INFINITY })
89+
).rejects.toBeInstanceOf(TypeError)
90+
await expect(invoke({ name: 'a', port: 3.14 })).rejects.toBeInstanceOf(
91+
TypeError
92+
)
93+
await expect(invoke({ name: 'a', port: -1 })).rejects.toBeInstanceOf(
94+
TypeError
95+
)
96+
await expect(invoke({ name: 'a', port: 70000 })).rejects.toBeInstanceOf(
97+
TypeError
98+
)
99+
})
100+
101+
it('rejects non-http(s) url', async () => {
102+
await expect(
103+
invoke({ name: 'a', url: 'file:///etc/passwd' })
104+
).rejects.toBeInstanceOf(TypeError)
105+
await expect(
106+
invoke({ name: 'a', url: 'javascript:alert(1)' })
107+
).rejects.toBeInstanceOf(TypeError)
108+
await expect(
109+
invoke({ name: 'a', url: 'not a url' })
110+
).rejects.toBeInstanceOf(TypeError)
111+
})
112+
113+
it('rejects non-string name / non-boolean remote', async () => {
114+
await expect(invoke({ name: 123 })).rejects.toBeInstanceOf(TypeError)
115+
await expect(
116+
invoke({ name: 'a', remote: 'true' })
117+
).rejects.toBeInstanceOf(TypeError)
118+
})
119+
120+
it('forwards a valid full workload to getWorkloadAvailableTools', async () => {
121+
ctx.getWorkloadAvailableTools.mockResolvedValue({ tool: {} })
122+
const workload = {
123+
name: 'weather',
124+
url: 'http://localhost:3000/mcp',
125+
transport_type: 'streamable-http',
126+
proxy_mode: 'streamable-http',
127+
port: 3000,
128+
remote: false,
129+
}
130+
131+
const result = await invoke(workload)
132+
133+
expect(ctx.getWorkloadAvailableTools).toHaveBeenCalledWith(workload)
134+
expect(result).toEqual({ tool: {} })
135+
})
136+
137+
it('forwards a valid partial workload (empty object) — consumer returns null when name is missing', async () => {
138+
ctx.getWorkloadAvailableTools.mockResolvedValue(null)
139+
140+
const result = await invoke({})
141+
142+
expect(ctx.getWorkloadAvailableTools).toHaveBeenCalledWith({})
143+
expect(result).toBeNull()
144+
})
145+
146+
it('tolerates empty url string (createTransport falls back to localhost)', async () => {
147+
ctx.getWorkloadAvailableTools.mockResolvedValue({})
148+
await invoke({ name: 'a', url: '' })
149+
expect(ctx.getWorkloadAvailableTools).toHaveBeenCalledTimes(1)
150+
})
151+
})
152+
153+
it('telemetry-headers returns current headers', () => {
154+
const handler = ctx.handlers.get('telemetry-headers')!
155+
expect(handler(null)).toEqual({ 'x-test': '1' })
156+
})
157+
158+
it('is-official-release-build returns the flag', () => {
159+
const handler = ctx.handlers.get('is-official-release-build')!
160+
expect(handler(null)).toBe(false)
161+
})
162+
163+
it('get-instance-id returns the resolved id', async () => {
164+
const handler = ctx.handlers.get('get-instance-id')!
165+
await expect(handler(null)).resolves.toBe('instance-abc')
166+
})
167+
})

0 commit comments

Comments
 (0)