@inspecto-dev/cli 0.3.3 → 0.3.5
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 +10594 -4044
- package/CHANGELOG.md +28 -0
- package/dist/bin.js +36 -2
- package/dist/{chunk-LJOKPCPD.js → chunk-7ABJRH3F.js} +1701 -182
- package/dist/index.d.ts +69 -4
- package/dist/index.js +7 -1
- package/package.json +3 -3
- package/src/bin.ts +49 -1
- package/src/commands/dev-config.ts +109 -0
- package/src/commands/doctor.ts +189 -9
- package/src/commands/init.ts +10 -3
- package/src/commands/integration-automation.ts +2 -0
- package/src/commands/integration-host-ide.ts +18 -15
- package/src/commands/integration-install.ts +100 -5
- package/src/commands/onboard.ts +80 -15
- package/src/detect/build-tool.ts +212 -15
- package/src/detect/framework.ts +3 -0
- package/src/detect/package-manager.ts +1 -1
- package/src/index.ts +1 -0
- package/src/inject/gitignore.ts +13 -2
- package/src/instructions.ts +33 -7
- package/src/onboarding/apply.ts +255 -28
- package/src/onboarding/nextjs-guidance.ts +257 -0
- package/src/onboarding/nuxt-guidance.ts +129 -0
- package/src/onboarding/planner.ts +337 -10
- package/src/onboarding/session.ts +127 -31
- package/src/onboarding/target-resolution.ts +79 -3
- package/src/onboarding/umi-guidance.ts +139 -0
- package/src/types.ts +58 -3
- package/tests/apply.test.ts +553 -0
- package/tests/build-tool.test.ts +199 -0
- package/tests/dev-config.test.ts +73 -0
- package/tests/doctor.test.ts +130 -0
- package/tests/init.test.ts +17 -0
- package/tests/install-wrapper.test.ts +56 -0
- package/tests/instructions.test.ts +10 -6
- package/tests/integration-host-ide.test.ts +20 -0
- package/tests/integration-install.test.ts +193 -0
- package/tests/nextjs-guidance.test.ts +128 -0
- package/tests/nuxt-guidance.test.ts +67 -0
- package/tests/onboard.test.ts +511 -0
- package/tests/plan.test.ts +283 -21
- package/tests/runner-script.test.ts +120 -1
- package/tests/session-resolve.test.ts +116 -0
- package/tests/session.test.ts +120 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
const writeFileMock = vi.fn()
|
|
4
|
+
const writeJSONMock = vi.fn()
|
|
5
|
+
const readJSONMock = vi.fn()
|
|
4
6
|
const existsMock = vi.fn()
|
|
5
7
|
const chmodMock = vi.fn()
|
|
6
8
|
const logMock = {
|
|
@@ -14,9 +16,13 @@ const logMock = {
|
|
|
14
16
|
blank: vi.fn(),
|
|
15
17
|
}
|
|
16
18
|
const runIntegrationAutomationMock = vi.fn()
|
|
19
|
+
const resolveIntegrationHostIdeMock = vi.fn()
|
|
20
|
+
const resolveIntegrationDispatchModeMock = vi.fn()
|
|
17
21
|
|
|
18
22
|
vi.mock('../src/utils/fs.js', () => ({
|
|
19
23
|
writeFile: writeFileMock,
|
|
24
|
+
writeJSON: writeJSONMock,
|
|
25
|
+
readJSON: readJSONMock,
|
|
20
26
|
exists: existsMock,
|
|
21
27
|
}))
|
|
22
28
|
|
|
@@ -38,11 +44,21 @@ vi.mock('../src/commands/integration-automation.js', () => ({
|
|
|
38
44
|
runIntegrationAutomation: runIntegrationAutomationMock,
|
|
39
45
|
}))
|
|
40
46
|
|
|
47
|
+
vi.mock('../src/commands/integration-host-ide.js', () => ({
|
|
48
|
+
resolveIntegrationHostIde: resolveIntegrationHostIdeMock,
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
vi.mock('../src/commands/integration-dispatch-mode.js', () => ({
|
|
52
|
+
resolveIntegrationDispatchMode: resolveIntegrationDispatchModeMock,
|
|
53
|
+
}))
|
|
54
|
+
|
|
41
55
|
describe('integration install', () => {
|
|
42
56
|
beforeEach(() => {
|
|
43
57
|
vi.resetModules()
|
|
44
58
|
vi.clearAllMocks()
|
|
59
|
+
vi.spyOn(process, 'cwd').mockReturnValue('/repo')
|
|
45
60
|
existsMock.mockResolvedValue(false)
|
|
61
|
+
readJSONMock.mockResolvedValue(null)
|
|
46
62
|
vi.stubGlobal(
|
|
47
63
|
'fetch',
|
|
48
64
|
vi.fn().mockResolvedValue({
|
|
@@ -56,6 +72,17 @@ describe('integration install', () => {
|
|
|
56
72
|
status: 'launched',
|
|
57
73
|
message: 'Onboarding launched in Cursor for Codex.',
|
|
58
74
|
})
|
|
75
|
+
resolveIntegrationHostIdeMock.mockResolvedValue({
|
|
76
|
+
ide: 'cursor',
|
|
77
|
+
source: 'explicit',
|
|
78
|
+
confidence: 'high',
|
|
79
|
+
candidates: ['cursor'],
|
|
80
|
+
})
|
|
81
|
+
resolveIntegrationDispatchModeMock.mockResolvedValue({
|
|
82
|
+
mode: 'extension',
|
|
83
|
+
ready: true,
|
|
84
|
+
reason: 'cursor_codex_extension',
|
|
85
|
+
})
|
|
59
86
|
})
|
|
60
87
|
|
|
61
88
|
it('installs the Claude Code skill into the user skill directory', async () => {
|
|
@@ -145,6 +172,92 @@ describe('integration install', () => {
|
|
|
145
172
|
)
|
|
146
173
|
})
|
|
147
174
|
|
|
175
|
+
it('installs the Trae skill with a launcher script', async () => {
|
|
176
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
177
|
+
|
|
178
|
+
await installIntegration('trae')
|
|
179
|
+
|
|
180
|
+
expect(writeFileMock).toHaveBeenCalledWith(
|
|
181
|
+
'.trae/skills/inspecto-onboarding/SKILL.md',
|
|
182
|
+
'# mock asset',
|
|
183
|
+
)
|
|
184
|
+
expect(writeFileMock).toHaveBeenCalledWith(
|
|
185
|
+
'.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
|
|
186
|
+
'# mock asset',
|
|
187
|
+
)
|
|
188
|
+
expect(chmodMock).toHaveBeenCalledWith(
|
|
189
|
+
'.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
|
|
190
|
+
0o755,
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('persists an explicit project host ide into .inspecto/settings.local.json before onboarding launch', async () => {
|
|
195
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
196
|
+
|
|
197
|
+
await installIntegration('gemini', { ide: 'trae-cn' })
|
|
198
|
+
|
|
199
|
+
expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
|
|
200
|
+
ide: 'trae-cn',
|
|
201
|
+
'provider.default': 'gemini.extension',
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it('updates the project host ide and provider.default to the selected assistant without dropping other keys', async () => {
|
|
206
|
+
readJSONMock.mockResolvedValue({
|
|
207
|
+
ide: 'trae',
|
|
208
|
+
'provider.default': 'coco.cli',
|
|
209
|
+
'prompt.autoSend': true,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
213
|
+
|
|
214
|
+
await installIntegration('gemini', { ide: 'trae-cn' })
|
|
215
|
+
|
|
216
|
+
expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
|
|
217
|
+
ide: 'trae-cn',
|
|
218
|
+
'provider.default': 'gemini.extension',
|
|
219
|
+
'prompt.autoSend': true,
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('persists the selected assistant as provider.default so onboarding does not fall back to another detected tool', async () => {
|
|
224
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
225
|
+
|
|
226
|
+
await installIntegration('codex', { ide: 'cursor' })
|
|
227
|
+
|
|
228
|
+
expect(writeJSONMock).toHaveBeenCalledWith('/repo/.inspecto/settings.local.json', {
|
|
229
|
+
ide: 'cursor',
|
|
230
|
+
'provider.default': 'codex.extension',
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('does not persist host ide settings for user-level installs', async () => {
|
|
235
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
236
|
+
|
|
237
|
+
await installIntegration('codex', { scope: 'user', ide: 'cursor' })
|
|
238
|
+
|
|
239
|
+
expect(writeJSONMock).not.toHaveBeenCalled()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('installs the Coco skill with a launcher script', async () => {
|
|
243
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
244
|
+
|
|
245
|
+
await installIntegration('coco')
|
|
246
|
+
|
|
247
|
+
expect(writeFileMock).toHaveBeenCalledWith(
|
|
248
|
+
'.trae/skills/inspecto-onboarding/SKILL.md',
|
|
249
|
+
'# mock asset',
|
|
250
|
+
)
|
|
251
|
+
expect(writeFileMock).toHaveBeenCalledWith(
|
|
252
|
+
'.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
|
|
253
|
+
'# mock asset',
|
|
254
|
+
)
|
|
255
|
+
expect(chmodMock).toHaveBeenCalledWith(
|
|
256
|
+
'.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
|
|
257
|
+
0o755,
|
|
258
|
+
)
|
|
259
|
+
})
|
|
260
|
+
|
|
148
261
|
it('surfaces a blocked automation outcome as a warning with next-step guidance', async () => {
|
|
149
262
|
runIntegrationAutomationMock.mockResolvedValue({
|
|
150
263
|
status: 'blocked',
|
|
@@ -166,6 +279,40 @@ describe('integration install', () => {
|
|
|
166
279
|
)
|
|
167
280
|
})
|
|
168
281
|
|
|
282
|
+
it('blocks automatic onboarding when no explicit host ide, config, or env signal is available', async () => {
|
|
283
|
+
runIntegrationAutomationMock.mockResolvedValue({
|
|
284
|
+
status: 'blocked',
|
|
285
|
+
message:
|
|
286
|
+
'Automatic setup stopped: Inspecto could not determine which IDE should receive onboarding.',
|
|
287
|
+
nextStep:
|
|
288
|
+
'Re-run with --host-ide <vscode|cursor|trae|trae-cn> or run the command from the target IDE terminal to continue automatic setup.',
|
|
289
|
+
})
|
|
290
|
+
resolveIntegrationHostIdeMock.mockResolvedValue({
|
|
291
|
+
ide: null,
|
|
292
|
+
source: 'none',
|
|
293
|
+
confidence: 'low',
|
|
294
|
+
candidates: [],
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
const { installIntegration } = await import('../src/commands/integration-install.js')
|
|
298
|
+
|
|
299
|
+
await installIntegration('coco')
|
|
300
|
+
|
|
301
|
+
expect(runIntegrationAutomationMock).toHaveBeenCalledWith(
|
|
302
|
+
'coco',
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
ignoreProjectArtifacts: true,
|
|
305
|
+
}),
|
|
306
|
+
'/repo',
|
|
307
|
+
)
|
|
308
|
+
expect(logMock.warn).toHaveBeenCalledWith(
|
|
309
|
+
'Automatic setup stopped: Inspecto could not determine which IDE should receive onboarding.',
|
|
310
|
+
)
|
|
311
|
+
expect(logMock.hint).toHaveBeenCalledWith(
|
|
312
|
+
'Re-run with --host-ide <vscode|cursor|trae|trae-cn> or run the command from the target IDE terminal to continue automatic setup.',
|
|
313
|
+
)
|
|
314
|
+
})
|
|
315
|
+
|
|
169
316
|
it('surfaces a partial automation outcome as a warning with follow-up guidance', async () => {
|
|
170
317
|
runIntegrationAutomationMock.mockResolvedValue({
|
|
171
318
|
status: 'partial',
|
|
@@ -207,6 +354,7 @@ describe('integration install', () => {
|
|
|
207
354
|
await installIntegration('codex', { preview: true, ide: 'cursor' })
|
|
208
355
|
|
|
209
356
|
expect(writeFileMock).not.toHaveBeenCalled()
|
|
357
|
+
expect(writeJSONMock).not.toHaveBeenCalled()
|
|
210
358
|
expect(chmodMock).not.toHaveBeenCalled()
|
|
211
359
|
expect(fetchMock).not.toHaveBeenCalled()
|
|
212
360
|
expect(logMock.info).toHaveBeenCalledWith('Step 1/6: Previewing Codex integration assets')
|
|
@@ -398,6 +546,20 @@ describe('integration install', () => {
|
|
|
398
546
|
})
|
|
399
547
|
})
|
|
400
548
|
|
|
549
|
+
it('describes coco using the trae skill install target', async () => {
|
|
550
|
+
const { describeIntegration } = await import('../src/commands/integration-install.js')
|
|
551
|
+
|
|
552
|
+
expect(describeIntegration('coco')).toMatchObject({
|
|
553
|
+
assistant: 'coco',
|
|
554
|
+
targets: [
|
|
555
|
+
'.trae/skills/inspecto-onboarding/SKILL.md',
|
|
556
|
+
'.trae/skills/inspecto-onboarding/scripts/run-inspecto.sh',
|
|
557
|
+
],
|
|
558
|
+
preferredInstall:
|
|
559
|
+
'npx @inspecto-dev/cli integrations install coco --host-ide <vscode|cursor|trae|trae-cn>',
|
|
560
|
+
})
|
|
561
|
+
})
|
|
562
|
+
|
|
401
563
|
it('prints integration paths without using post-install hints', async () => {
|
|
402
564
|
const { printIntegrationPath } = await import('../src/commands/integration-install.js')
|
|
403
565
|
|
|
@@ -534,6 +696,37 @@ describe('integration install', () => {
|
|
|
534
696
|
})
|
|
535
697
|
})
|
|
536
698
|
|
|
699
|
+
it('wires the dev command group to link, status, and unlink handlers', async () => {
|
|
700
|
+
const linkCommand = vi.fn().mockResolvedValue(undefined)
|
|
701
|
+
const statusCommand = vi.fn().mockResolvedValue(undefined)
|
|
702
|
+
const unlinkCommand = vi.fn().mockResolvedValue(undefined)
|
|
703
|
+
|
|
704
|
+
vi.doMock('../src/commands/init.js', () => ({ init: vi.fn() }))
|
|
705
|
+
vi.doMock('../src/commands/doctor.js', () => ({ doctor: vi.fn() }))
|
|
706
|
+
vi.doMock('../src/commands/teardown.js', () => ({ teardown: vi.fn() }))
|
|
707
|
+
vi.doMock('../src/commands/detect.js', () => ({ detect: vi.fn() }))
|
|
708
|
+
vi.doMock('../src/commands/plan.js', () => ({ plan: vi.fn() }))
|
|
709
|
+
vi.doMock('../src/commands/apply.js', () => ({ apply: vi.fn() }))
|
|
710
|
+
vi.doMock('../src/commands/dev-config.js', () => ({
|
|
711
|
+
devLink: linkCommand,
|
|
712
|
+
devStatus: statusCommand,
|
|
713
|
+
devUnlink: unlinkCommand,
|
|
714
|
+
}))
|
|
715
|
+
|
|
716
|
+
const { runCli } = await import('../src/bin.js')
|
|
717
|
+
|
|
718
|
+
await runCli(['node', 'inspecto', 'dev', 'link', '--cli-bin', '/tmp/inspecto/bin.js'])
|
|
719
|
+
await runCli(['node', 'inspecto', 'dev', 'status', '--json'])
|
|
720
|
+
await runCli(['node', 'inspecto', 'dev', 'unlink'])
|
|
721
|
+
|
|
722
|
+
expect(linkCommand).toHaveBeenCalledWith({
|
|
723
|
+
cliBin: '/tmp/inspecto/bin.js',
|
|
724
|
+
json: false,
|
|
725
|
+
})
|
|
726
|
+
expect(statusCommand).toHaveBeenCalledWith(true)
|
|
727
|
+
expect(unlinkCommand).toHaveBeenCalledWith(false)
|
|
728
|
+
})
|
|
729
|
+
|
|
537
730
|
it('sets a non-zero exit code when integrations doctor reports a blocked result', async () => {
|
|
538
731
|
const doctorCommand = vi.fn().mockResolvedValue({ status: 'blocked' })
|
|
539
732
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { createNextJsGuidance } from '../src/onboarding/nextjs-guidance.js'
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = []
|
|
8
|
+
|
|
9
|
+
async function createTempProject(files: Record<string, string>) {
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'inspecto-nextjs-'))
|
|
11
|
+
tempDirs.push(root)
|
|
12
|
+
|
|
13
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
14
|
+
const filePath = path.join(root, relativePath)
|
|
15
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
16
|
+
await fs.writeFile(filePath, contents)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return root
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('createNextJsGuidance', () => {
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('creates a high-confidence patch plan for object-export next.config.mjs', async () => {
|
|
28
|
+
const root = await createTempProject({
|
|
29
|
+
'next.config.mjs': `export default {\n reactStrictMode: true,\n}\n`,
|
|
30
|
+
'app/layout.tsx': `export default function RootLayout({ children }) { return <html><body>{children}</body></html> }\n`,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const guidance = createNextJsGuidance(root)
|
|
34
|
+
|
|
35
|
+
expect(guidance.metaFramework).toBe('Next.js')
|
|
36
|
+
expect(guidance.routerMode).toBe('app')
|
|
37
|
+
expect(guidance.patches).toEqual(
|
|
38
|
+
expect.arrayContaining([
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
path: 'next.config.mjs',
|
|
41
|
+
confidence: 'high',
|
|
42
|
+
reason: 'next_config_object_export',
|
|
43
|
+
}),
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
path: 'app/layout.tsx',
|
|
46
|
+
status: 'manual_patch_required',
|
|
47
|
+
reason: 'next_app_router_mount',
|
|
48
|
+
}),
|
|
49
|
+
]),
|
|
50
|
+
)
|
|
51
|
+
expect(guidance.assistantPrompt).toContain('Next.js')
|
|
52
|
+
expect(guidance.patches[0]?.snippet).toContain('if (dev) {')
|
|
53
|
+
expect(guidance.patches[0]?.snippet).not.toContain('!isServer')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('treats exported nextConfig objects as high-confidence patches', async () => {
|
|
57
|
+
const root = await createTempProject({
|
|
58
|
+
'next.config.ts': `import type { NextConfig } from 'next'\nconst nextConfig: NextConfig = {\n reactStrictMode: true,\n}\n\nexport default nextConfig\n`,
|
|
59
|
+
'src/app/layout.tsx': `export default function RootLayout({ children }) { return <html><body>{children}</body></html> }\n`,
|
|
60
|
+
'package.json': `{\n "scripts": {\n "dev": "next dev"\n }\n}\n`,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const guidance = createNextJsGuidance(root)
|
|
64
|
+
|
|
65
|
+
expect(guidance.patches).toEqual(
|
|
66
|
+
expect.arrayContaining([
|
|
67
|
+
expect.objectContaining({
|
|
68
|
+
path: 'next.config.ts',
|
|
69
|
+
status: 'planned',
|
|
70
|
+
confidence: 'high',
|
|
71
|
+
reason: 'next_config_object_export',
|
|
72
|
+
}),
|
|
73
|
+
]),
|
|
74
|
+
)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('degrades to a medium-confidence manual patch plan for wrapped next config', async () => {
|
|
78
|
+
const root = await createTempProject({
|
|
79
|
+
'next.config.js': `const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: true })\nmodule.exports = withBundleAnalyzer({ reactStrictMode: true })\n`,
|
|
80
|
+
'pages/_app.tsx': `export default function App({ Component, pageProps }) { return <Component {...pageProps} /> }\n`,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const guidance = createNextJsGuidance(root)
|
|
84
|
+
|
|
85
|
+
expect(guidance.routerMode).toBe('pages')
|
|
86
|
+
expect(guidance.patches[0]).toMatchObject({
|
|
87
|
+
path: 'next.config.js',
|
|
88
|
+
confidence: 'medium',
|
|
89
|
+
status: 'manual_patch_required',
|
|
90
|
+
reason: 'next_config_wrapped_export',
|
|
91
|
+
})
|
|
92
|
+
expect(guidance.patches).toEqual(
|
|
93
|
+
expect.arrayContaining([
|
|
94
|
+
expect.objectContaining({
|
|
95
|
+
path: 'pages/_app.tsx',
|
|
96
|
+
status: 'manual_patch_required',
|
|
97
|
+
reason: 'next_pages_router_mount',
|
|
98
|
+
}),
|
|
99
|
+
]),
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('adds a webpack dev-server follow-up when package.json still runs next dev without --webpack', async () => {
|
|
104
|
+
const root = await createTempProject({
|
|
105
|
+
'next.config.ts': `import type { NextConfig } from 'next'\nconst nextConfig: NextConfig = {\n reactStrictMode: true,\n}\n\nexport default nextConfig\n`,
|
|
106
|
+
'app/layout.tsx': `export default function RootLayout({ children }) { return <html><body>{children}</body></html> }\n`,
|
|
107
|
+
'package.json': `{\n "scripts": {\n "dev": "next dev"\n }\n}\n`,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const guidance = createNextJsGuidance(root)
|
|
111
|
+
|
|
112
|
+
expect(guidance.pendingSteps).toEqual(
|
|
113
|
+
expect.arrayContaining([
|
|
114
|
+
'Update the Next.js dev script to use webpack mode for Inspecto validation.',
|
|
115
|
+
]),
|
|
116
|
+
)
|
|
117
|
+
expect(guidance.patches).toEqual(
|
|
118
|
+
expect.arrayContaining([
|
|
119
|
+
expect.objectContaining({
|
|
120
|
+
path: 'package.json',
|
|
121
|
+
status: 'manual_patch_required',
|
|
122
|
+
reason: 'next_dev_script_requires_webpack',
|
|
123
|
+
}),
|
|
124
|
+
]),
|
|
125
|
+
)
|
|
126
|
+
expect(guidance.assistantPrompt).toContain('Use the generated patches directly')
|
|
127
|
+
})
|
|
128
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { createNuxtGuidance } from '../src/onboarding/nuxt-guidance.js'
|
|
6
|
+
|
|
7
|
+
const tempDirs: string[] = []
|
|
8
|
+
|
|
9
|
+
async function createTempProject(files: Record<string, string>) {
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'inspecto-nuxt-'))
|
|
11
|
+
tempDirs.push(root)
|
|
12
|
+
|
|
13
|
+
for (const [relativePath, contents] of Object.entries(files)) {
|
|
14
|
+
const filePath = path.join(root, relativePath)
|
|
15
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
16
|
+
await fs.writeFile(filePath, contents)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return root
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('createNuxtGuidance', () => {
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await Promise.all(tempDirs.splice(0).map(dir => fs.rm(dir, { recursive: true, force: true })))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('creates a high-confidence patch plan for object-export nuxt.config.ts', async () => {
|
|
28
|
+
const root = await createTempProject({
|
|
29
|
+
'nuxt.config.ts': `export default defineNuxtConfig({\n devtools: { enabled: true },\n})\n`,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const guidance = createNuxtGuidance(root)
|
|
33
|
+
|
|
34
|
+
expect(guidance.metaFramework).toBe('Nuxt')
|
|
35
|
+
expect(guidance.framework).toBe('vue')
|
|
36
|
+
expect(guidance.patches).toEqual(
|
|
37
|
+
expect.arrayContaining([
|
|
38
|
+
expect.objectContaining({
|
|
39
|
+
path: 'nuxt.config.ts',
|
|
40
|
+
confidence: 'high',
|
|
41
|
+
reason: 'nuxt_config_object_export',
|
|
42
|
+
status: 'planned',
|
|
43
|
+
}),
|
|
44
|
+
expect.objectContaining({
|
|
45
|
+
path: 'plugins/inspecto.client.ts',
|
|
46
|
+
reason: 'nuxt_client_plugin_mount',
|
|
47
|
+
status: 'manual_patch_required',
|
|
48
|
+
}),
|
|
49
|
+
]),
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('degrades to a medium-confidence manual patch plan for wrapped nuxt config', async () => {
|
|
54
|
+
const root = await createTempProject({
|
|
55
|
+
'nuxt.config.js': `export default withFoo(defineNuxtConfig({ ssr: true }))\n`,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const guidance = createNuxtGuidance(root)
|
|
59
|
+
|
|
60
|
+
expect(guidance.patches[0]).toMatchObject({
|
|
61
|
+
path: 'nuxt.config.js',
|
|
62
|
+
confidence: 'medium',
|
|
63
|
+
status: 'manual_patch_required',
|
|
64
|
+
reason: 'nuxt_config_wrapped_export',
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|