diff --git a/packages/core/src/utils/shellAstParser.test.ts b/packages/core/src/utils/shellAstParser.test.ts index 0b0e6abe9..506147e6b 100644 --- a/packages/core/src/utils/shellAstParser.test.ts +++ b/packages/core/src/utils/shellAstParser.test.ts @@ -4,12 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ +import path from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { initParser, isShellCommandReadOnlyAST, extractCommandRules, _resetParser, + _resolveWasmPathForTesting, } from './shellAstParser.js'; beforeAll(async () => { @@ -20,6 +22,44 @@ afterAll(() => { _resetParser(); }); +describe('WASM path resolution', () => { + it('resolves bundled WASM relative to the real CLI path when launched via symlink', () => { + const symlinkedCliPath = path.join('/usr', 'bin', 'qwen'); + const realCliPath = path.join( + '/opt', + 'homebrew', + 'lib', + 'node_modules', + '@qwen-code', + 'qwen-code', + 'dist', + 'cli.js', + ); + + const result = _resolveWasmPathForTesting( + 'tree-sitter.wasm', + symlinkedCliPath, + () => realCliPath, + ); + + expect(result).toBe( + path.join( + '/opt', + 'homebrew', + 'lib', + 'node_modules', + '@qwen-code', + 'qwen-code', + 'dist', + 'vendor', + 'tree-sitter', + 'tree-sitter.wasm', + ), + ); + expect(result).not.toContain(path.join('/usr', 'bin', 'vendor')); + }); +}); + // ========================================================================= // isShellCommandReadOnlyAST — mirror all tests from shellReadOnlyChecker.test.ts // ========================================================================= diff --git a/packages/core/src/utils/shellAstParser.ts b/packages/core/src/utils/shellAstParser.ts index 7b5e5d2b2..0f315b2f9 100644 --- a/packages/core/src/utils/shellAstParser.ts +++ b/packages/core/src/utils/shellAstParser.ts @@ -15,6 +15,7 @@ */ import Parser from 'web-tree-sitter'; +import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -22,8 +23,18 @@ import { fileURLToPath } from 'node:url'; // Constants // --------------------------------------------------------------------------- -const __filename_ = fileURLToPath(import.meta.url); -const __dirname_ = path.dirname(__filename_); +const __filename_ = resolveModuleFilePath(fileURLToPath(import.meta.url)); + +function resolveModuleFilePath(moduleFilePath: string): string { + try { + const resolved = fs.realpathSync(moduleFilePath); + // Guard against test environments where `fs` is mocked and realpathSync + // returns undefined rather than throwing. + return typeof resolved === 'string' ? resolved : moduleFilePath; + } catch { + return moduleFilePath; + } +} /** * Root commands considered read-only by default (no sub-command analysis needed @@ -569,10 +580,24 @@ let initPromise: Promise | null = null; * - Bundle (dist/cli.js): vendor at same level (0 levels) */ function resolveWasmPath(filename: string): string { - const inSrcUtils = __filename_.includes(path.join('src', 'utils')); - const levelsUp = !inSrcUtils ? 0 : __filename_.endsWith('.ts') ? 2 : 3; + return resolveWasmPathForModule(filename, __filename_); +} + +function resolveWasmPathForModule( + filename: string, + moduleFilePath: string, + resolvePath: (moduleFilePath: string) => string = resolveModuleFilePath, +): string { + const resolvedModuleFilePath = resolvePath(moduleFilePath); + const moduleDir = path.dirname(resolvedModuleFilePath); + const inSrcUtils = resolvedModuleFilePath.includes(path.join('src', 'utils')); + const levelsUp = !inSrcUtils + ? 0 + : resolvedModuleFilePath.endsWith('.ts') + ? 2 + : 3; return path.join( - __dirname_, + moduleDir, ...Array(levelsUp).fill('..'), 'vendor', 'tree-sitter', @@ -1084,3 +1109,15 @@ export function _resetParser(): void { bashLanguage = null; initPromise = null; } + +/** + * Internal helper exposed for tests. + * @internal + */ +export function _resolveWasmPathForTesting( + filename: string, + moduleFilePath: string, + resolvePath?: (moduleFilePath: string) => string, +): string { + return resolveWasmPathForModule(filename, moduleFilePath, resolvePath); +}