diff --git a/packages/core/src/node/__tests__/open-in-editor.test.ts b/packages/core/src/node/__tests__/open-in-editor.test.ts index cc6cda53..fc98405d 100644 --- a/packages/core/src/node/__tests__/open-in-editor.test.ts +++ b/packages/core/src/node/__tests__/open-in-editor.test.ts @@ -1,6 +1,7 @@ import type { DevToolsNodeContext } from '@vitejs/devtools-kit' import { resolve } from 'node:path' -import { describe, expect, it, vi } from 'vitest' +import { launchEditor } from 'devframe/utils/launch-editor' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { openInEditor } from '../rpc/public/open-in-editor' // Mock launch-editor so tests don't actually open files @@ -20,6 +21,10 @@ describe('openInEditor – path traversal protection', () => { return handler as (path: string) => Promise } + beforeEach(() => { + vi.mocked(launchEditor).mockClear() + }) + it('allows opening a file inside the project root', async () => { const handler = await getHandler() await expect(handler('src/main.ts')).resolves.not.toThrow() @@ -30,6 +35,18 @@ describe('openInEditor – path traversal protection', () => { await expect(handler('src/utils/helper.ts')).resolves.not.toThrow() }) + it('resolves relative paths against cwd (Vite project root), not workspaceRoot', async () => { + const handler = await getHandler() + await handler('src/main.ts') + expect(launchEditor).toHaveBeenCalledWith(resolve(cwd, 'src/main.ts')) + }) + + it('allows jumping to sibling packages within the workspace root', async () => { + const handler = await getHandler() + await expect(handler('../sibling-pkg/src/foo.ts')).resolves.not.toThrow() + expect(launchEditor).toHaveBeenCalledWith(resolve(workspaceRoot, 'sibling-pkg/src/foo.ts')) + }) + it('rejects path traversal with ../', async () => { const handler = await getHandler() await expect(handler('../../etc/passwd')).rejects.toThrow( @@ -46,7 +63,7 @@ describe('openInEditor – path traversal protection', () => { it('rejects traversal disguised within a subpath', async () => { const handler = await getHandler() - await expect(handler('src/../../secret/file.txt')).rejects.toThrow( + await expect(handler('src/../../../secret/file.txt')).rejects.toThrow( 'Path is outside the workspace root', ) }) diff --git a/packages/core/src/node/rpc/public/open-in-editor.ts b/packages/core/src/node/rpc/public/open-in-editor.ts index d728f785..9ff73876 100644 --- a/packages/core/src/node/rpc/public/open-in-editor.ts +++ b/packages/core/src/node/rpc/public/open-in-editor.ts @@ -10,7 +10,7 @@ export const openInEditor = defineRpcFunction({ setup: (context) => { return { handler: async (path: string) => { - const resolved = resolve(context.workspaceRoot, path) + const resolved = resolve(context.cwd, path) const rel = relative(context.workspaceRoot, resolved) // Prevent escaping the workspace root diff --git a/packages/core/src/node/rpc/public/open-in-finder.ts b/packages/core/src/node/rpc/public/open-in-finder.ts index 7ad4ce91..27651029 100644 --- a/packages/core/src/node/rpc/public/open-in-finder.ts +++ b/packages/core/src/node/rpc/public/open-in-finder.ts @@ -10,7 +10,7 @@ export const openInFinder = defineRpcFunction({ setup: (context) => { return { handler: async (path: string) => { - const resolved = resolve(context.workspaceRoot, path) + const resolved = resolve(context.cwd, path) const rel = relative(context.workspaceRoot, resolved) // Ensure the path stays within workspace root