From 205875d3fbc46dc341e912f37bd7ba0f686519b2 Mon Sep 17 00:00:00 2001 From: Wagner Silveira Date: Mon, 8 Jun 2026 15:22:42 +1200 Subject: [PATCH 1/2] fix(vscode): respect manual LSP path settings when auto-validation is disabled Three issues fixed: 1. languageServerDLLPath and languageServerNupkgPath settings were declared in package.json but never consumed by getSDKPaths(). Now when autoRuntimeDependenciesValidationAndInstallation is OFF, these settings are read directly instead of constructing paths from autoRuntimeDependenciesPath. 2. Regression from 0d9fafd74: a tryGetLogicAppCustomCodeFunctionsProjects gate was added that silently prevented the LSP server from starting unless a C# custom code project existed alongside the Logic App. The original design (298310ab4) had no such gate. Removed. 3. installBinaries() unconditionally overwrote dotnetBinaryPath, nodeJsBinaryPath, and funcCoreToolsBinaryPath with system defaults on every activation when auto-validation was OFF, clobbering user-configured paths. Now only sets defaults if the settings are not already configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../__test__/languageServer.test.ts | 116 +++++++++++++++--- .../src/app/languageServer/languageServer.ts | 30 ++++- .../src/app/utils/__test__/binaries.test.ts | 17 ++- .../src/app/utils/binaries.ts | 12 +- apps/vs-code-designer/src/constants.ts | 2 + 5 files changed, 147 insertions(+), 30 deletions(-) diff --git a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts index e22e95f406e..7cdbb51e01d 100644 --- a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts +++ b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts @@ -12,7 +12,6 @@ const mocks = vi.hoisted(() => ({ readFile: vi.fn(), readdir: vi.fn(), showWarningMessage: vi.fn(), - tryGetLogicAppCustomCodeFunctionsProjects: vi.fn(), tryGetLogicAppProjectRoot: vi.fn(), getDotNetCommand: vi.fn(), })); @@ -52,10 +51,6 @@ vi.mock('../../utils/verifyIsProject', () => ({ tryGetLogicAppProjectRoot: mocks.tryGetLogicAppProjectRoot, })); -vi.mock('../../utils/customCodeUtils', () => ({ - tryGetLogicAppCustomCodeFunctionsProjects: mocks.tryGetLogicAppCustomCodeFunctionsProjects, -})); - vi.mock('../../utils/dotnet/dotnet', () => ({ getDotNetCommand: mocks.getDotNetCommand, })); @@ -93,14 +88,21 @@ describe('LogicAppsLanguageServer', () => { beforeEach(() => { vi.clearAllMocks(); mocks.createFileSystemWatcher.mockReturnValue({ onDidChange: vi.fn() }); - mocks.getGlobalSetting.mockReturnValue(dependenciesPath); + mocks.getGlobalSetting.mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return true; + } + if (key === 'autoRuntimeDependenciesPath') { + return dependenciesPath; + } + return undefined; + }); mocks.getWorkspaceFolderPath.mockResolvedValue('D:\\workspace'); mocks.languageClient.mockImplementation(() => ({ start: vi.fn().mockResolvedValue(undefined) })); mocks.pathExists.mockResolvedValue(false); mocks.readFile.mockResolvedValue('{}'); mocks.readdir.mockResolvedValue([]); mocks.tryGetLogicAppProjectRoot.mockResolvedValue(projectPath); - mocks.tryGetLogicAppCustomCodeFunctionsProjects.mockResolvedValue(['D:\\workspace\\custom-code-project']); mocks.getDotNetCommand.mockReturnValue('D:\\dependencies\\DotNetSDK\\dotnet.exe'); mocks.getAzureConnectorDetailsForLocalProject.mockResolvedValue({ accessToken: 'Bearer token', @@ -120,19 +122,6 @@ describe('LogicAppsLanguageServer', () => { expect(mocks.showWarningMessage).not.toHaveBeenCalled(); }); - it('does not start or prompt for Azure connector metadata when no linked custom code project is found', async () => { - mocks.tryGetLogicAppCustomCodeFunctionsProjects.mockResolvedValue([]); - - await new LogicAppsLanguageServer({} as any).start(); - - expect(mocks.tryGetLogicAppCustomCodeFunctionsProjects).toHaveBeenCalledWith(projectPath); - expect(mocks.pathExists).not.toHaveBeenCalled(); - expect(mocks.readdir).not.toHaveBeenCalled(); - expect(mocks.getAzureConnectorDetailsForLocalProject).not.toHaveBeenCalled(); - expect(mocks.languageClient).not.toHaveBeenCalled(); - expect(mocks.showWarningMessage).not.toHaveBeenCalled(); - }); - it('does not scan a missing SDK directory', async () => { mocks.pathExists.mockImplementation(async (filePath: string) => filePath === lspServerPath); @@ -195,4 +184,91 @@ describe('LogicAppsLanguageServer', () => { ); expect(languageClient.start).toHaveBeenCalledOnce(); }); + + describe('manual mode (autoRuntimeDependenciesValidationAndInstallation = false)', () => { + const manualDllPath = 'C:\\custom\\LSPServer\\SdkLspServer.dll'; + const manualNupkgPath = 'C:\\custom\\LanguageServerLogicApps\\Microsoft.Azure.Workflows.Sdk.1.0.0-preview.1.nupkg'; + + beforeEach(() => { + mocks.getGlobalSetting.mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return false; + } + if (key === 'languageServerDLLPath') { + return manualDllPath; + } + if (key === 'languageServerNupkgPath') { + return manualNupkgPath; + } + return undefined; + }); + }); + + it('uses explicit path settings when auto-validation is off', async () => { + const languageClient = { start: vi.fn().mockResolvedValue(undefined) }; + mocks.languageClient.mockReturnValue(languageClient); + mocks.pathExists.mockResolvedValue(true); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.languageClient).toHaveBeenCalledWith( + 'logicAppsLanguageServer', + 'Logic Apps language server', + { + run: { + command: 'D:\\dependencies\\DotNetSDK\\dotnet.exe', + args: [manualDllPath, '--sdk', manualNupkgPath], + }, + debug: { + command: 'D:\\dependencies\\DotNetSDK\\dotnet.exe', + args: [manualDllPath, '--sdk', manualNupkgPath], + }, + }, + expect.objectContaining({ + initializationOptions: expect.objectContaining({ + apiConfig: expect.objectContaining({ + bearerToken: 'Bearer token', + }), + }), + }) + ); + expect(languageClient.start).toHaveBeenCalledOnce(); + }); + + it('warns and returns undefined lspServerPath when DLL path does not exist', async () => { + mocks.pathExists.mockImplementation(async (filePath: string) => filePath !== manualDllPath); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.showWarningMessage).toHaveBeenCalledWith(expect.stringContaining('Language server DLL not found at configured path')); + expect(mocks.languageClient).not.toHaveBeenCalled(); + }); + + it('warns and returns undefined sdkNupkgPath when nupkg path does not exist', async () => { + mocks.pathExists.mockImplementation(async (filePath: string) => filePath !== manualNupkgPath); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.showWarningMessage).toHaveBeenCalledWith( + expect.stringContaining('Language server SDK nupkg not found at configured path') + ); + expect(mocks.languageClient).not.toHaveBeenCalled(); + }); + + it('returns undefined for both paths when settings are empty', async () => { + mocks.getGlobalSetting.mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return false; + } + return undefined; + }); + + await new LogicAppsLanguageServer({} as any).start(); + + expect(mocks.showWarningMessage).toHaveBeenCalledWith( + 'Install or repair Logic Apps language server dependencies before starting C# workflow authoring.' + ); + expect(mocks.languageClient).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/vs-code-designer/src/app/languageServer/languageServer.ts b/apps/vs-code-designer/src/app/languageServer/languageServer.ts index fe6d6cbba8b..ef767e94e7f 100644 --- a/apps/vs-code-designer/src/app/languageServer/languageServer.ts +++ b/apps/vs-code-designer/src/app/languageServer/languageServer.ts @@ -2,7 +2,10 @@ import type { IActionContext } from '@microsoft/vscode-azext-utils'; import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils'; import { autoRuntimeDependenciesPathSettingKey, + autoRuntimeDependenciesValidationAndInstallationSetting, connectionsFileName, + languageServerDLLPathSettingKey, + languageServerNupkgPathSettingKey, lspDirectory, onStartLanguageServerProtocol, workflowAppApiVersion, @@ -20,7 +23,6 @@ import type { AzureConnectorDetails } from '@microsoft/vscode-extension-logic-ap import { getAzureConnectorDetailsForLocalProject } from '../utils/codeless/common'; import * as vscode from 'vscode'; import { filterCompletionResult } from './completionFilter'; -import { tryGetLogicAppCustomCodeFunctionsProjects } from '../utils/customCodeUtils'; import { getDotNetCommand } from '../utils/dotnet/dotnet'; export default class LogicAppsLanguageServer { @@ -46,11 +48,6 @@ export default class LogicAppsLanguageServer { return; } - const customCodeProjectPaths = await tryGetLogicAppCustomCodeFunctionsProjects(this.projectPath); - if (!customCodeProjectPaths || customCodeProjectPaths.length === 0) { - return; - } - const { lspServerPath, sdkNupkgPath } = await this.getSDKPaths(); this.lspServerPath = lspServerPath; @@ -178,6 +175,27 @@ export default class LogicAppsLanguageServer { } private async getSDKPaths() { + const autoValidate = getGlobalSetting(autoRuntimeDependenciesValidationAndInstallationSetting); + + if (!autoValidate) { + // Manual mode: read explicit path settings + const lspServerPath = getGlobalSetting(languageServerDLLPathSettingKey) || undefined; + const sdkNupkgPath = getGlobalSetting(languageServerNupkgPathSettingKey) || undefined; + + if (lspServerPath && !(await fse.pathExists(lspServerPath))) { + window.showWarningMessage(`Language server DLL not found at configured path: ${lspServerPath}`); + return { lspServerPath: undefined, sdkNupkgPath }; + } + + if (sdkNupkgPath && !(await fse.pathExists(sdkNupkgPath))) { + window.showWarningMessage(`Language server SDK nupkg not found at configured path: ${sdkNupkgPath}`); + return { lspServerPath, sdkNupkgPath: undefined }; + } + + return { lspServerPath, sdkNupkgPath }; + } + + // Auto mode: construct paths from dependenciesPath const dependenciesPath = getGlobalSetting(autoRuntimeDependenciesPathSettingKey); if (!dependenciesPath) { return { lspServerPath: undefined, sdkNupkgPath: undefined }; diff --git a/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts b/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts index 1b2a3246a3e..efafff3029c 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts @@ -575,13 +575,28 @@ describe('binaries', () => { expect(updateGlobalSetting).toHaveBeenCalledWith('funcCoreToolsBinaryPath', 'func'); }); - it('should set default paths in devContainer workspace', async () => { + it('should not overwrite existing paths in devContainer workspace', async () => { (getGlobalSetting as Mock).mockReturnValue(true); const devContainerModule = await import('../devContainerUtils'); vi.mocked(devContainerModule.isDevContainerWorkspace).mockResolvedValue(true); await installBinaries(context); + expect(updateGlobalSetting).not.toHaveBeenCalled(); + }); + + it('should set default paths in devContainer workspace when paths are not set', async () => { + (getGlobalSetting as Mock).mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return true; + } + return undefined; + }); + const devContainerModule = await import('../devContainerUtils'); + vi.mocked(devContainerModule.isDevContainerWorkspace).mockResolvedValue(true); + + await installBinaries(context); + expect(updateGlobalSetting).toHaveBeenCalledWith('dotnetBinaryPath', 'dotnet'); expect(updateGlobalSetting).toHaveBeenCalledWith('nodeJsBinaryPath', 'node'); expect(updateGlobalSetting).toHaveBeenCalledWith('funcCoreToolsBinaryPath', 'func'); diff --git a/apps/vs-code-designer/src/app/utils/binaries.ts b/apps/vs-code-designer/src/app/utils/binaries.ts index 2776be288f4..af036e2777e 100644 --- a/apps/vs-code-designer/src/app/utils/binaries.ts +++ b/apps/vs-code-designer/src/app/utils/binaries.ts @@ -476,9 +476,15 @@ export async function installBinaries(context: IActionContext) { await onboardBinaries(context); context.telemetry.properties.autoRuntimeDependenciesValidationAndInstallationSetting = 'true'; } else { - await updateGlobalSetting(dotNetBinaryPathSettingKey, DependencyDefaultPath.dotnet); - await updateGlobalSetting(nodeJsBinaryPathSettingKey, DependencyDefaultPath.node); - await updateGlobalSetting(funcCoreToolsBinaryPathSettingKey, DependencyDefaultPath.funcCoreTools); + if (!getGlobalSetting(dotNetBinaryPathSettingKey)) { + await updateGlobalSetting(dotNetBinaryPathSettingKey, DependencyDefaultPath.dotnet); + } + if (!getGlobalSetting(nodeJsBinaryPathSettingKey)) { + await updateGlobalSetting(nodeJsBinaryPathSettingKey, DependencyDefaultPath.node); + } + if (!getGlobalSetting(funcCoreToolsBinaryPathSettingKey)) { + await updateGlobalSetting(funcCoreToolsBinaryPathSettingKey, DependencyDefaultPath.funcCoreTools); + } context.telemetry.properties.autoRuntimeDependenciesValidationAndInstallationSetting = 'false'; } } diff --git a/apps/vs-code-designer/src/constants.ts b/apps/vs-code-designer/src/constants.ts index b2bdf08880f..d94cd6163b4 100644 --- a/apps/vs-code-designer/src/constants.ts +++ b/apps/vs-code-designer/src/constants.ts @@ -279,6 +279,8 @@ export const dotNetBinaryPathSettingKey = 'dotnetBinaryPath'; export const nodeJsBinaryPathSettingKey = 'nodeJsBinaryPath'; export const funcCoreToolsBinaryPathSettingKey = 'funcCoreToolsBinaryPath'; export const dependencyTimeoutSettingKey = 'dependencyTimeout'; +export const languageServerDLLPathSettingKey = 'languageServerDLLPath'; +export const languageServerNupkgPathSettingKey = 'languageServerNupkgPath'; export const unitTestExplorer = 'unitTestExplorer'; export const verifyConnectionKeysSetting = 'verifyConnectionKeys'; export const useSmbDeployment = 'useSmbDeploymentForHybrid'; From 7e39a20423488771b75a5d0fcf4a7490806e8e8b Mon Sep 17 00:00:00 2001 From: Wagner Silveira Date: Mon, 8 Jun 2026 15:31:52 +1200 Subject: [PATCH 2/2] fix: address review feedback - remove duplicate warnings, use realistic test values - Remove path-not-found warnings from getSDKPaths() to avoid duplicate popups (start() already shows appropriate warnings when paths are undefined) - Use realistic path strings instead of boolean in binaries test mock Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../__test__/languageServer.test.ts | 10 ++++++---- .../src/app/languageServer/languageServer.ts | 2 -- .../src/app/utils/__test__/binaries.test.ts | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts index 7cdbb51e01d..15e33157e57 100644 --- a/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts +++ b/apps/vs-code-designer/src/app/languageServer/__test__/languageServer.test.ts @@ -235,22 +235,24 @@ describe('LogicAppsLanguageServer', () => { expect(languageClient.start).toHaveBeenCalledOnce(); }); - it('warns and returns undefined lspServerPath when DLL path does not exist', async () => { + it('warns and does not start when DLL path does not exist', async () => { mocks.pathExists.mockImplementation(async (filePath: string) => filePath !== manualDllPath); await new LogicAppsLanguageServer({} as any).start(); - expect(mocks.showWarningMessage).toHaveBeenCalledWith(expect.stringContaining('Language server DLL not found at configured path')); + expect(mocks.showWarningMessage).toHaveBeenCalledWith( + expect.stringContaining('Install or repair Logic Apps language server dependencies') + ); expect(mocks.languageClient).not.toHaveBeenCalled(); }); - it('warns and returns undefined sdkNupkgPath when nupkg path does not exist', async () => { + it('warns and does not start when nupkg path does not exist', async () => { mocks.pathExists.mockImplementation(async (filePath: string) => filePath !== manualNupkgPath); await new LogicAppsLanguageServer({} as any).start(); expect(mocks.showWarningMessage).toHaveBeenCalledWith( - expect.stringContaining('Language server SDK nupkg not found at configured path') + expect.stringContaining('Install or repair Logic Apps language server SDK dependencies') ); expect(mocks.languageClient).not.toHaveBeenCalled(); }); diff --git a/apps/vs-code-designer/src/app/languageServer/languageServer.ts b/apps/vs-code-designer/src/app/languageServer/languageServer.ts index ef767e94e7f..7f5e0e02535 100644 --- a/apps/vs-code-designer/src/app/languageServer/languageServer.ts +++ b/apps/vs-code-designer/src/app/languageServer/languageServer.ts @@ -183,12 +183,10 @@ export default class LogicAppsLanguageServer { const sdkNupkgPath = getGlobalSetting(languageServerNupkgPathSettingKey) || undefined; if (lspServerPath && !(await fse.pathExists(lspServerPath))) { - window.showWarningMessage(`Language server DLL not found at configured path: ${lspServerPath}`); return { lspServerPath: undefined, sdkNupkgPath }; } if (sdkNupkgPath && !(await fse.pathExists(sdkNupkgPath))) { - window.showWarningMessage(`Language server SDK nupkg not found at configured path: ${sdkNupkgPath}`); return { lspServerPath, sdkNupkgPath: undefined }; } diff --git a/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts b/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts index efafff3029c..8f73387ae7c 100644 --- a/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts +++ b/apps/vs-code-designer/src/app/utils/__test__/binaries.test.ts @@ -576,7 +576,21 @@ describe('binaries', () => { }); it('should not overwrite existing paths in devContainer workspace', async () => { - (getGlobalSetting as Mock).mockReturnValue(true); + (getGlobalSetting as Mock).mockImplementation((key: string) => { + if (key === 'autoRuntimeDependenciesValidationAndInstallation') { + return true; + } + if (key === 'dotnetBinaryPath') { + return '/custom/path/dotnet'; + } + if (key === 'nodeJsBinaryPath') { + return '/custom/path/node'; + } + if (key === 'funcCoreToolsBinaryPath') { + return '/custom/path/func'; + } + return undefined; + }); const devContainerModule = await import('../devContainerUtils'); vi.mocked(devContainerModule.isDevContainerWorkspace).mockResolvedValue(true);