@inspecto-dev/cli 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/.turbo/turbo-test.log +2653 -92
- package/CHANGELOG.md +12 -0
- package/README.md +126 -6
- package/dist/bin.js +59 -361
- package/dist/{chunk-PSYZB5GI.js → chunk-LJOKPCPD.js} +1389 -91
- package/dist/index.d.ts +74 -1
- package/dist/index.js +3 -1
- package/package.json +2 -2
- package/src/bin.ts +85 -6
- package/src/commands/integration-automation.ts +484 -0
- package/src/commands/integration-dispatch-mode.ts +92 -0
- package/src/commands/integration-doctor.ts +117 -0
- package/src/commands/integration-host-ide.ts +156 -0
- package/src/commands/integration-install.ts +262 -101
- package/src/commands/onboard.ts +19 -0
- package/src/index.ts +1 -0
- package/src/inject/extension.ts +144 -24
- package/src/integrations/capabilities.ts +131 -0
- package/src/utils/process.ts +3 -0
- package/tests/extension-installer.test.ts +205 -0
- package/tests/install-wrapper.test.ts +45 -4
- package/tests/integration-automation.test.ts +435 -0
- package/tests/integration-dispatch-mode.test.ts +203 -0
- package/tests/integration-doctor.test.ts +165 -0
- package/tests/integration-host-ide.test.ts +172 -0
- package/tests/integration-install.test.ts +282 -20
- package/tests/onboard.test.ts +118 -0
- package/tests/shared-capabilities.test.ts +45 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const installExtensionMock = vi.fn()
|
|
4
|
+
const openIdeWorkspaceMock = vi.fn()
|
|
5
|
+
const openUriMock = vi.fn()
|
|
6
|
+
const resolveHostIdeBinaryMock = vi.fn()
|
|
7
|
+
const resolveHostIdeMock = vi.fn()
|
|
8
|
+
const setTimeoutMock = vi.fn<(callback: () => void, delay: number) => number>()
|
|
9
|
+
const resolveDispatchModeMock = vi.fn()
|
|
10
|
+
const existsMock = vi.fn()
|
|
11
|
+
const logMock = {
|
|
12
|
+
header: vi.fn(),
|
|
13
|
+
info: vi.fn(),
|
|
14
|
+
success: vi.fn(),
|
|
15
|
+
warn: vi.fn(),
|
|
16
|
+
hint: vi.fn(),
|
|
17
|
+
ready: vi.fn(),
|
|
18
|
+
error: vi.fn(),
|
|
19
|
+
blank: vi.fn(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
vi.mock('../src/inject/extension.js', () => ({
|
|
23
|
+
installExtension: installExtensionMock,
|
|
24
|
+
openIdeWorkspace: openIdeWorkspaceMock,
|
|
25
|
+
openUri: openUriMock,
|
|
26
|
+
resolveHostIdeBinary: resolveHostIdeBinaryMock,
|
|
27
|
+
}))
|
|
28
|
+
|
|
29
|
+
vi.mock('../src/commands/integration-host-ide.js', () => ({
|
|
30
|
+
resolveIntegrationHostIde: resolveHostIdeMock,
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
vi.mock('../src/commands/integration-dispatch-mode.js', () => ({
|
|
34
|
+
resolveIntegrationDispatchMode: resolveDispatchModeMock,
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
vi.mock('../src/utils/logger.js', () => ({
|
|
38
|
+
log: logMock,
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
vi.mock('../src/utils/fs.js', () => ({
|
|
42
|
+
exists: existsMock,
|
|
43
|
+
}))
|
|
44
|
+
|
|
45
|
+
describe('runIntegrationAutomation', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.resetAllMocks()
|
|
48
|
+
installExtensionMock.mockResolvedValue(null)
|
|
49
|
+
openIdeWorkspaceMock.mockResolvedValue(true)
|
|
50
|
+
openUriMock.mockResolvedValue(true)
|
|
51
|
+
resolveHostIdeBinaryMock.mockResolvedValue('cursor')
|
|
52
|
+
existsMock.mockResolvedValue(true)
|
|
53
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
54
|
+
mode: 'extension',
|
|
55
|
+
ready: true,
|
|
56
|
+
reason: 'default',
|
|
57
|
+
})
|
|
58
|
+
setTimeoutMock.mockImplementation((callback, _delay) => {
|
|
59
|
+
callback()
|
|
60
|
+
return 0
|
|
61
|
+
})
|
|
62
|
+
vi.stubGlobal('setTimeout', setTimeoutMock as typeof setTimeout)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('installs the resolved IDE extension and launches onboarding when confidence is high', async () => {
|
|
66
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
67
|
+
ide: 'cursor',
|
|
68
|
+
confidence: 'high',
|
|
69
|
+
source: 'explicit',
|
|
70
|
+
candidates: ['cursor'],
|
|
71
|
+
})
|
|
72
|
+
installExtensionMock.mockResolvedValue({
|
|
73
|
+
type: 'extension_installed',
|
|
74
|
+
id: 'inspecto.inspecto',
|
|
75
|
+
description: 'installed_via_cli',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
79
|
+
|
|
80
|
+
await runIntegrationAutomation('codex', { ide: 'cursor' }, '/repo')
|
|
81
|
+
|
|
82
|
+
expect(resolveHostIdeMock).toHaveBeenCalledWith({
|
|
83
|
+
explicitIde: 'cursor',
|
|
84
|
+
cwd: '/repo',
|
|
85
|
+
})
|
|
86
|
+
expect(installExtensionMock).toHaveBeenCalledWith(false, 'cursor', true, undefined)
|
|
87
|
+
expect(openIdeWorkspaceMock).toHaveBeenCalledWith('cursor', '/repo')
|
|
88
|
+
expect(logMock.success).toHaveBeenCalledWith('Step 2/6: Resolved host IDE')
|
|
89
|
+
expect(logMock.success).toHaveBeenCalledWith(
|
|
90
|
+
'Step 3/6: Installed the Inspecto extension in Cursor',
|
|
91
|
+
)
|
|
92
|
+
expect(logMock.success).toHaveBeenCalledWith('Step 4/6: Resolved Codex runtime')
|
|
93
|
+
expect(logMock.success).toHaveBeenCalledWith('Step 5/6: Opened workspace in Cursor')
|
|
94
|
+
expect(logMock.success).toHaveBeenCalledWith('Step 6/6: Launched onboarding in Cursor')
|
|
95
|
+
expect(openUriMock).toHaveBeenCalledWith(
|
|
96
|
+
'cursor://inspecto.inspecto/send?target=codex&prompt=Set+up+Inspecto+in+this+project&autoSend=true&workspace=%2Frepo&overrides=%7B%22type%22%3A%22extension%22%7D',
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('passes a local Inspecto VSIX path through to extension installation when provided', async () => {
|
|
101
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
102
|
+
ide: 'trae-cn',
|
|
103
|
+
confidence: 'high',
|
|
104
|
+
source: 'explicit',
|
|
105
|
+
candidates: ['trae-cn'],
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
109
|
+
|
|
110
|
+
await runIntegrationAutomation(
|
|
111
|
+
'gemini',
|
|
112
|
+
{ ide: 'trae-cn', inspectoVsix: '/tmp/inspecto.vsix' },
|
|
113
|
+
'/repo',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
expect(installExtensionMock).toHaveBeenCalledWith(false, 'trae-cn', true, '/tmp/inspecto.vsix')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('waits briefly after a fresh extension install before launching the onboarding URI', async () => {
|
|
120
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
121
|
+
ide: 'cursor',
|
|
122
|
+
confidence: 'high',
|
|
123
|
+
source: 'explicit',
|
|
124
|
+
candidates: ['cursor'],
|
|
125
|
+
})
|
|
126
|
+
installExtensionMock.mockResolvedValue({
|
|
127
|
+
type: 'extension_installed',
|
|
128
|
+
id: 'inspecto.inspecto',
|
|
129
|
+
description: 'installed_via_cli',
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
133
|
+
|
|
134
|
+
await runIntegrationAutomation('codex', { ide: 'cursor' }, '/repo')
|
|
135
|
+
|
|
136
|
+
expect(setTimeoutMock).toHaveBeenCalledWith(expect.any(Function), 1500)
|
|
137
|
+
expect(setTimeoutMock).toHaveBeenCalledWith(expect.any(Function), 1000)
|
|
138
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
139
|
+
'Waiting briefly for the IDE extension to finish activating...',
|
|
140
|
+
)
|
|
141
|
+
expect(openUriMock).toHaveBeenCalledTimes(1)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('uses codex CLI mode in Cursor when the Codex extension is unavailable but the CLI exists', async () => {
|
|
145
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
146
|
+
ide: 'cursor',
|
|
147
|
+
confidence: 'high',
|
|
148
|
+
source: 'explicit',
|
|
149
|
+
candidates: ['cursor'],
|
|
150
|
+
})
|
|
151
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
152
|
+
mode: 'cli',
|
|
153
|
+
ready: true,
|
|
154
|
+
reason: 'codex_cli',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
158
|
+
|
|
159
|
+
await runIntegrationAutomation('codex', { ide: 'cursor' }, '/repo')
|
|
160
|
+
|
|
161
|
+
expect(openUriMock).toHaveBeenCalledWith(
|
|
162
|
+
'cursor://inspecto.inspecto/send?target=codex&prompt=Set+up+Inspecto+in+this+project&autoSend=true&workspace=%2Frepo&overrides=%7B%22type%22%3A%22cli%22%7D',
|
|
163
|
+
)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('stops and explains what to install when codex cannot run in Cursor', async () => {
|
|
167
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
168
|
+
ide: 'cursor',
|
|
169
|
+
confidence: 'high',
|
|
170
|
+
source: 'explicit',
|
|
171
|
+
candidates: ['cursor'],
|
|
172
|
+
})
|
|
173
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
174
|
+
mode: null,
|
|
175
|
+
ready: false,
|
|
176
|
+
reason: 'missing_codex_runtime',
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
180
|
+
|
|
181
|
+
await runIntegrationAutomation('codex', { ide: 'cursor' }, '/repo')
|
|
182
|
+
|
|
183
|
+
expect(openUriMock).not.toHaveBeenCalled()
|
|
184
|
+
expect(logMock.warn).toHaveBeenCalledWith(
|
|
185
|
+
'Step 4/6: Could not resolve a runnable Codex runtime',
|
|
186
|
+
)
|
|
187
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
188
|
+
'Install the Codex plugin in Cursor or install the `codex` CLI, then rerun the command.',
|
|
189
|
+
)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('uses Claude CLI mode in Cursor when the Claude extension is unavailable but the CLI exists', async () => {
|
|
193
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
194
|
+
ide: 'cursor',
|
|
195
|
+
confidence: 'high',
|
|
196
|
+
source: 'explicit',
|
|
197
|
+
candidates: ['cursor'],
|
|
198
|
+
})
|
|
199
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
200
|
+
mode: 'cli',
|
|
201
|
+
ready: true,
|
|
202
|
+
reason: 'claude-code_cli',
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
206
|
+
|
|
207
|
+
await runIntegrationAutomation('claude-code', { ide: 'cursor' }, '/repo')
|
|
208
|
+
|
|
209
|
+
expect(openUriMock).toHaveBeenCalledWith(
|
|
210
|
+
'cursor://inspecto.inspecto/send?target=claude-code&prompt=Set+up+Inspecto+in+this+project&autoSend=false&workspace=%2Frepo&overrides=%7B%22type%22%3A%22cli%22%7D',
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('stops and explains what to install when gemini cannot run in VS Code', async () => {
|
|
215
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
216
|
+
ide: 'vscode',
|
|
217
|
+
confidence: 'high',
|
|
218
|
+
source: 'explicit',
|
|
219
|
+
candidates: ['vscode'],
|
|
220
|
+
})
|
|
221
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
222
|
+
mode: null,
|
|
223
|
+
ready: false,
|
|
224
|
+
reason: 'missing_gemini_runtime',
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
228
|
+
|
|
229
|
+
await runIntegrationAutomation('gemini', { ide: 'vscode' }, '/repo')
|
|
230
|
+
|
|
231
|
+
expect(openUriMock).not.toHaveBeenCalled()
|
|
232
|
+
expect(logMock.warn).toHaveBeenCalledWith(
|
|
233
|
+
'Step 4/6: Could not resolve a runnable Gemini runtime',
|
|
234
|
+
)
|
|
235
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
236
|
+
'Install the Gemini plugin in VS Code or install the `gemini` CLI, then rerun the command.',
|
|
237
|
+
)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('uses Gemini CLI mode in Trae CN when the Gemini extension is unavailable but the CLI exists', async () => {
|
|
241
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
242
|
+
ide: 'trae-cn',
|
|
243
|
+
confidence: 'high',
|
|
244
|
+
source: 'explicit',
|
|
245
|
+
candidates: ['trae-cn'],
|
|
246
|
+
})
|
|
247
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
248
|
+
mode: 'cli',
|
|
249
|
+
ready: true,
|
|
250
|
+
reason: 'gemini_cli',
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
254
|
+
|
|
255
|
+
await runIntegrationAutomation('gemini', { ide: 'trae-cn' }, '/repo')
|
|
256
|
+
|
|
257
|
+
expect(openIdeWorkspaceMock).toHaveBeenCalledWith('trae-cn', '/repo')
|
|
258
|
+
expect(logMock.success).toHaveBeenCalledWith('Step 5/6: Opened workspace in Trae CN')
|
|
259
|
+
expect(openUriMock).toHaveBeenCalledWith(
|
|
260
|
+
'trae-cn://inspecto.inspecto/send?target=gemini&prompt=Set+up+Inspecto+in+this+project&autoSend=false&workspace=%2Frepo&overrides=%7B%22type%22%3A%22cli%22%7D',
|
|
261
|
+
)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('skips extension install and URI launch when host ide confidence is low', async () => {
|
|
265
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
266
|
+
ide: null,
|
|
267
|
+
confidence: 'low',
|
|
268
|
+
source: 'ambiguous',
|
|
269
|
+
candidates: ['cursor', 'vscode'],
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
273
|
+
|
|
274
|
+
await runIntegrationAutomation('codex', {}, '/repo')
|
|
275
|
+
|
|
276
|
+
expect(installExtensionMock).not.toHaveBeenCalled()
|
|
277
|
+
expect(openUriMock).not.toHaveBeenCalled()
|
|
278
|
+
expect(logMock.warn).toHaveBeenCalledWith(
|
|
279
|
+
'Step 2/6: Could not confidently resolve the host IDE',
|
|
280
|
+
)
|
|
281
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
282
|
+
'Re-run with --host-ide <vscode|cursor|trae|trae-cn> or run the command from the target IDE terminal to continue automatic setup.',
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('prints a dry preview without installing extensions or opening the IDE when preview mode is enabled', async () => {
|
|
287
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
288
|
+
ide: 'cursor',
|
|
289
|
+
confidence: 'high',
|
|
290
|
+
source: 'explicit',
|
|
291
|
+
candidates: ['cursor'],
|
|
292
|
+
})
|
|
293
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
294
|
+
mode: 'cli',
|
|
295
|
+
ready: true,
|
|
296
|
+
reason: 'codex_cli',
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
300
|
+
|
|
301
|
+
await expect(
|
|
302
|
+
runIntegrationAutomation('codex', { ide: 'cursor', preview: true }, '/repo'),
|
|
303
|
+
).resolves.toMatchObject({
|
|
304
|
+
status: 'preview',
|
|
305
|
+
message:
|
|
306
|
+
'Preview complete. Inspecto did not write files or open IDE windows. Review the resolved setup below, then rerun without --preview to apply it.',
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
expect(installExtensionMock).not.toHaveBeenCalled()
|
|
310
|
+
expect(openIdeWorkspaceMock).not.toHaveBeenCalled()
|
|
311
|
+
expect(openUriMock).not.toHaveBeenCalled()
|
|
312
|
+
expect(logMock.info).toHaveBeenCalledWith('Step 2/6: Previewed host IDE resolution')
|
|
313
|
+
expect(logMock.info).toHaveBeenCalledWith('Step 3/6: Previewed Inspecto extension installation')
|
|
314
|
+
expect(logMock.info).toHaveBeenCalledWith('Step 4/6: Previewed Codex runtime')
|
|
315
|
+
expect(logMock.info).toHaveBeenCalledWith('Step 5/6: Previewed workspace routing in Cursor')
|
|
316
|
+
expect(logMock.info).toHaveBeenCalledWith('Step 6/6: Previewed onboarding launch')
|
|
317
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
318
|
+
'cursor://inspecto.inspecto/send?target=codex&prompt=Set+up+Inspecto+in+this+project&autoSend=true&workspace=%2Frepo&overrides=%7B%22type%22%3A%22cli%22%7D',
|
|
319
|
+
)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('reports a blocked preflight when the host IDE binary is unavailable in preview mode', async () => {
|
|
323
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
324
|
+
ide: 'trae-cn',
|
|
325
|
+
confidence: 'high',
|
|
326
|
+
source: 'explicit',
|
|
327
|
+
candidates: ['trae-cn'],
|
|
328
|
+
})
|
|
329
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
330
|
+
mode: 'cli',
|
|
331
|
+
ready: true,
|
|
332
|
+
reason: 'gemini_cli',
|
|
333
|
+
})
|
|
334
|
+
resolveHostIdeBinaryMock.mockResolvedValue(null)
|
|
335
|
+
|
|
336
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
337
|
+
|
|
338
|
+
await expect(
|
|
339
|
+
runIntegrationAutomation('gemini', { ide: 'trae-cn', preview: true }, '/repo'),
|
|
340
|
+
).resolves.toMatchObject({
|
|
341
|
+
status: 'preview_blocked',
|
|
342
|
+
message:
|
|
343
|
+
'Preview blocked. Inspecto did not write files or open IDE windows because setup cannot continue until the blocking issue below is resolved.',
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
expect(logMock.warn).toHaveBeenCalledWith(
|
|
347
|
+
'Step 3/6: Could not verify Inspecto extension installation in Trae CN',
|
|
348
|
+
)
|
|
349
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
350
|
+
'No Trae CN CLI binary was found. Automatic extension install and workspace opening may not work.',
|
|
351
|
+
)
|
|
352
|
+
expect(openUriMock).not.toHaveBeenCalled()
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('suppresses step logs and returns structured details in silent preview mode', async () => {
|
|
356
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
357
|
+
ide: 'cursor',
|
|
358
|
+
confidence: 'high',
|
|
359
|
+
source: 'explicit',
|
|
360
|
+
candidates: ['cursor'],
|
|
361
|
+
})
|
|
362
|
+
resolveDispatchModeMock.mockResolvedValue({
|
|
363
|
+
mode: 'cli',
|
|
364
|
+
ready: true,
|
|
365
|
+
reason: 'codex_cli',
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
369
|
+
|
|
370
|
+
await expect(
|
|
371
|
+
runIntegrationAutomation('codex', { ide: 'cursor', preview: true, silent: true }, '/repo'),
|
|
372
|
+
).resolves.toMatchObject({
|
|
373
|
+
status: 'preview',
|
|
374
|
+
details: {
|
|
375
|
+
hostIde: {
|
|
376
|
+
id: 'cursor',
|
|
377
|
+
label: 'Cursor',
|
|
378
|
+
source: 'from --host-ide',
|
|
379
|
+
confidence: 'high',
|
|
380
|
+
candidates: ['cursor'],
|
|
381
|
+
},
|
|
382
|
+
inspectoExtension: {
|
|
383
|
+
source: 'marketplace',
|
|
384
|
+
reference: 'inspecto.inspecto',
|
|
385
|
+
binaryAvailable: true,
|
|
386
|
+
status: 'preview_ready',
|
|
387
|
+
},
|
|
388
|
+
runtime: {
|
|
389
|
+
assistant: 'Codex',
|
|
390
|
+
ready: true,
|
|
391
|
+
mode: 'cli',
|
|
392
|
+
},
|
|
393
|
+
workspace: {
|
|
394
|
+
path: '/repo',
|
|
395
|
+
attempted: true,
|
|
396
|
+
},
|
|
397
|
+
onboarding: {
|
|
398
|
+
uri: 'cursor://inspecto.inspecto/send?target=codex&prompt=Set+up+Inspecto+in+this+project&autoSend=true&workspace=%2Frepo&overrides=%7B%22type%22%3A%22cli%22%7D',
|
|
399
|
+
autoSend: true,
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
expect(logMock.info).not.toHaveBeenCalled()
|
|
405
|
+
expect(logMock.warn).not.toHaveBeenCalled()
|
|
406
|
+
expect(logMock.hint).not.toHaveBeenCalled()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('returns a workspace-specific message when onboarding opens but the target workspace could not be opened first', async () => {
|
|
410
|
+
resolveHostIdeMock.mockResolvedValue({
|
|
411
|
+
ide: 'cursor',
|
|
412
|
+
confidence: 'high',
|
|
413
|
+
source: 'explicit',
|
|
414
|
+
candidates: ['cursor'],
|
|
415
|
+
})
|
|
416
|
+
installExtensionMock.mockResolvedValue({
|
|
417
|
+
type: 'extension_installed',
|
|
418
|
+
id: 'inspecto.inspecto',
|
|
419
|
+
description: 'installed_via_cli',
|
|
420
|
+
})
|
|
421
|
+
openIdeWorkspaceMock.mockResolvedValue(false)
|
|
422
|
+
|
|
423
|
+
const { runIntegrationAutomation } = await import('../src/commands/integration-automation.js')
|
|
424
|
+
|
|
425
|
+
await expect(
|
|
426
|
+
runIntegrationAutomation('codex', { ide: 'cursor' }, '/repo'),
|
|
427
|
+
).resolves.toMatchObject({
|
|
428
|
+
status: 'partial',
|
|
429
|
+
message:
|
|
430
|
+
'Onboarding opened in Cursor for Codex, but Inspecto could not open the target workspace first.',
|
|
431
|
+
nextStep:
|
|
432
|
+
'If the wrong IDE window received onboarding, open /repo in Cursor and rerun the command from that project.',
|
|
433
|
+
})
|
|
434
|
+
})
|
|
435
|
+
})
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const existsMock = vi.fn()
|
|
4
|
+
const readJSONMock = vi.fn()
|
|
5
|
+
const whichMock = vi.fn()
|
|
6
|
+
|
|
7
|
+
vi.mock('../src/utils/fs.js', () => ({
|
|
8
|
+
exists: existsMock,
|
|
9
|
+
readJSON: readJSONMock,
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('../src/utils/exec.js', () => ({
|
|
13
|
+
which: whichMock,
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
describe('resolveIntegrationDispatchMode', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks()
|
|
19
|
+
existsMock.mockResolvedValue(false)
|
|
20
|
+
readJSONMock.mockResolvedValue(null)
|
|
21
|
+
whichMock.mockResolvedValue(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('prefers the Cursor Codex extension when it is installed', async () => {
|
|
25
|
+
existsMock.mockImplementation(async filePath => {
|
|
26
|
+
return (
|
|
27
|
+
filePath === '/Users/tester/.cursor/extensions' ||
|
|
28
|
+
filePath === '/Users/tester/.cursor/extensions/.obsolete'
|
|
29
|
+
)
|
|
30
|
+
})
|
|
31
|
+
readJSONMock.mockResolvedValue({})
|
|
32
|
+
|
|
33
|
+
vi.doMock('node:fs/promises', () => ({
|
|
34
|
+
readdir: vi.fn().mockResolvedValue(['openai.chatgpt-1.2.3']),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
const { resolveIntegrationDispatchMode } =
|
|
38
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
resolveIntegrationDispatchMode({
|
|
42
|
+
assistant: 'codex',
|
|
43
|
+
hostIde: 'cursor',
|
|
44
|
+
homeDir: '/Users/tester',
|
|
45
|
+
}),
|
|
46
|
+
).resolves.toMatchObject({
|
|
47
|
+
mode: 'extension',
|
|
48
|
+
ready: true,
|
|
49
|
+
reason: 'cursor_codex_extension',
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('falls back to codex CLI when the Cursor extension is not installed but the CLI is available', async () => {
|
|
54
|
+
whichMock.mockImplementation(async bin => bin === 'codex')
|
|
55
|
+
|
|
56
|
+
const { resolveIntegrationDispatchMode } =
|
|
57
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
58
|
+
|
|
59
|
+
await expect(
|
|
60
|
+
resolveIntegrationDispatchMode({
|
|
61
|
+
assistant: 'codex',
|
|
62
|
+
hostIde: 'cursor',
|
|
63
|
+
homeDir: '/Users/tester',
|
|
64
|
+
}),
|
|
65
|
+
).resolves.toMatchObject({
|
|
66
|
+
mode: 'cli',
|
|
67
|
+
ready: true,
|
|
68
|
+
reason: 'codex_cli',
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('returns guidance when neither the Cursor Codex extension nor the codex CLI is available', async () => {
|
|
73
|
+
const { resolveIntegrationDispatchMode } =
|
|
74
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
75
|
+
|
|
76
|
+
await expect(
|
|
77
|
+
resolveIntegrationDispatchMode({
|
|
78
|
+
assistant: 'codex',
|
|
79
|
+
hostIde: 'cursor',
|
|
80
|
+
homeDir: '/Users/tester',
|
|
81
|
+
}),
|
|
82
|
+
).resolves.toMatchObject({
|
|
83
|
+
mode: null,
|
|
84
|
+
ready: false,
|
|
85
|
+
reason: 'missing_codex_runtime',
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('prefers the Claude Code extension in Cursor when available', async () => {
|
|
90
|
+
existsMock.mockImplementation(async filePath => {
|
|
91
|
+
return (
|
|
92
|
+
filePath === '/Users/tester/.cursor/extensions' ||
|
|
93
|
+
filePath === '/Users/tester/.cursor/extensions/.obsolete'
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
readJSONMock.mockResolvedValue({})
|
|
97
|
+
|
|
98
|
+
vi.doMock('node:fs/promises', () => ({
|
|
99
|
+
readdir: vi.fn().mockResolvedValue(['anthropic.claude-code-0.9.0']),
|
|
100
|
+
}))
|
|
101
|
+
|
|
102
|
+
const { resolveIntegrationDispatchMode } =
|
|
103
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
104
|
+
|
|
105
|
+
await expect(
|
|
106
|
+
resolveIntegrationDispatchMode({
|
|
107
|
+
assistant: 'claude-code',
|
|
108
|
+
hostIde: 'cursor',
|
|
109
|
+
homeDir: '/Users/tester',
|
|
110
|
+
}),
|
|
111
|
+
).resolves.toMatchObject({
|
|
112
|
+
mode: 'extension',
|
|
113
|
+
ready: true,
|
|
114
|
+
reason: 'cursor_claude-code_extension',
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('falls back to claude CLI in Cursor when the extension is unavailable', async () => {
|
|
119
|
+
whichMock.mockImplementation(async bin => bin === 'claude')
|
|
120
|
+
|
|
121
|
+
const { resolveIntegrationDispatchMode } =
|
|
122
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
resolveIntegrationDispatchMode({
|
|
126
|
+
assistant: 'claude-code',
|
|
127
|
+
hostIde: 'cursor',
|
|
128
|
+
homeDir: '/Users/tester',
|
|
129
|
+
}),
|
|
130
|
+
).resolves.toMatchObject({
|
|
131
|
+
mode: 'cli',
|
|
132
|
+
ready: true,
|
|
133
|
+
reason: 'claude-code_cli',
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('prefers the Gemini extension in VS Code when available', async () => {
|
|
138
|
+
existsMock.mockImplementation(async filePath => {
|
|
139
|
+
return (
|
|
140
|
+
filePath === '/Users/tester/.vscode/extensions' ||
|
|
141
|
+
filePath === '/Users/tester/.vscode/extensions/.obsolete'
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
readJSONMock.mockResolvedValue({})
|
|
145
|
+
|
|
146
|
+
vi.doMock('node:fs/promises', () => ({
|
|
147
|
+
readdir: vi.fn().mockResolvedValue(['google.geminicodeassist-2.1.0']),
|
|
148
|
+
}))
|
|
149
|
+
|
|
150
|
+
const { resolveIntegrationDispatchMode } =
|
|
151
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
152
|
+
|
|
153
|
+
await expect(
|
|
154
|
+
resolveIntegrationDispatchMode({
|
|
155
|
+
assistant: 'gemini',
|
|
156
|
+
hostIde: 'vscode',
|
|
157
|
+
homeDir: '/Users/tester',
|
|
158
|
+
}),
|
|
159
|
+
).resolves.toMatchObject({
|
|
160
|
+
mode: 'extension',
|
|
161
|
+
ready: true,
|
|
162
|
+
reason: 'vscode_gemini_extension',
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('falls back to gemini CLI when the VS Code extension is unavailable', async () => {
|
|
167
|
+
whichMock.mockImplementation(async bin => bin === 'gemini')
|
|
168
|
+
|
|
169
|
+
const { resolveIntegrationDispatchMode } =
|
|
170
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
171
|
+
|
|
172
|
+
await expect(
|
|
173
|
+
resolveIntegrationDispatchMode({
|
|
174
|
+
assistant: 'gemini',
|
|
175
|
+
hostIde: 'vscode',
|
|
176
|
+
homeDir: '/Users/tester',
|
|
177
|
+
}),
|
|
178
|
+
).resolves.toMatchObject({
|
|
179
|
+
mode: 'cli',
|
|
180
|
+
ready: true,
|
|
181
|
+
reason: 'gemini_cli',
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('falls back to gemini CLI in Trae CN when the Gemini extension is unavailable', async () => {
|
|
186
|
+
whichMock.mockImplementation(async bin => bin === 'gemini')
|
|
187
|
+
|
|
188
|
+
const { resolveIntegrationDispatchMode } =
|
|
189
|
+
await import('../src/commands/integration-dispatch-mode.js')
|
|
190
|
+
|
|
191
|
+
await expect(
|
|
192
|
+
resolveIntegrationDispatchMode({
|
|
193
|
+
assistant: 'gemini',
|
|
194
|
+
hostIde: 'trae-cn',
|
|
195
|
+
homeDir: '/Users/tester',
|
|
196
|
+
}),
|
|
197
|
+
).resolves.toMatchObject({
|
|
198
|
+
mode: 'cli',
|
|
199
|
+
ready: true,
|
|
200
|
+
reason: 'gemini_cli',
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
})
|