From e70fe53eb32f75e331f1310a87f7be0ee969e1b7 Mon Sep 17 00:00:00 2001 From: Ella Hathaway Date: Mon, 10 Aug 2026 10:52:56 -0700 Subject: [PATCH] Respect project server ready action overrides Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- extension/src/dcp/types.ts | 1 + extension/src/debugger/languages/dotnet.ts | 5 +- extension/src/test/dotnetDebugger.test.ts | 63 +++++++++++++++++++++- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/extension/src/dcp/types.ts b/extension/src/dcp/types.ts index d8a8a5610f1..e995c35663f 100644 --- a/extension/src/dcp/types.ts +++ b/extension/src/dcp/types.ts @@ -130,6 +130,7 @@ export interface RunSessionPayload { } export interface DebugLaunchSettings { + [key: string]: unknown; env?: { [key: string]: string }; args?: string[]; launchProfile?: string; diff --git a/extension/src/debugger/languages/dotnet.ts b/extension/src/debugger/languages/dotnet.ts index 2b372d7099d..da7bb8b5667 100644 --- a/extension/src/debugger/languages/dotnet.ts +++ b/extension/src/debugger/languages/dotnet.ts @@ -447,8 +447,9 @@ export function createProjectDebuggerExtension(dotNetServiceProducer: (debugSess debugConfiguration.checkForDevCert = baseProfile?.useSSL; // The apphost's application URL is the Aspire dashboard URL. We already get the dashboard login URL later on, - // so we should just avoid setting up serverReadyAction and manually open the browser ourselves. - if (!launchOptions.isApphost) { + // so avoid generating a serverReadyAction for the apphost and manually open the browser ourselves. + // For project resources, launch settings supply a default only when debugger settings did not provide one. + if (!launchOptions.isApphost && debugConfiguration.serverReadyAction === undefined) { debugConfiguration.serverReadyAction = determineServerReadyAction(baseProfile?.launchBrowser, baseProfile?.applicationUrl, baseProfile?.launchUrl); } diff --git a/extension/src/test/dotnetDebugger.test.ts b/extension/src/test/dotnetDebugger.test.ts index a229f1d6b4f..3bcda840bda 100644 --- a/extension/src/test/dotnetDebugger.test.ts +++ b/extension/src/test/dotnetDebugger.test.ts @@ -5,9 +5,9 @@ import * as nodePath from 'path'; import * as sinon from 'sinon'; import * as vscode from 'vscode'; import { createProjectDebuggerExtension, projectDebuggerExtension, quoteCommandLineArgument } from '../debugger/languages/dotnet'; -import { AspireResourceExtendedDebugConfiguration, ExecutableLaunchConfiguration, ProjectLaunchConfiguration } from '../dcp/types'; +import { AspireExtendedDebugConfiguration, AspireResourceExtendedDebugConfiguration, ExecutableLaunchConfiguration, ProjectLaunchConfiguration } from '../dcp/types'; import * as io from '../utils/io'; -import { ResourceDebuggerExtension } from '../debugger/debuggerExtensions'; +import { createDebugSessionConfiguration, ResourceDebuggerExtension } from '../debugger/debuggerExtensions'; import { AppHostParentOutputFilter, AspireDebugSession } from '../debugger/AspireDebugSession'; class TestDotNetService { @@ -1741,6 +1741,65 @@ suite('Dotnet Debugger Extension Tests', () => { fs.rmSync(tempDir, { recursive: true, force: true }); }); + test('preserves serverReadyAction from project debugger settings', async () => { + const fs = require('fs'); + const os = require('os'); + const path = require('path'); + + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'aspire-test-')); + const projectDir = path.join(tempDir, 'WebProject'); + const propertiesDir = path.join(projectDir, 'Properties'); + fs.mkdirSync(propertiesDir, { recursive: true }); + + const projectPath = path.join(projectDir, 'WebProject.csproj'); + fs.writeFileSync(projectPath, ''); + fs.writeFileSync(path.join(propertiesDir, 'launchSettings.json'), JSON.stringify({ + profiles: { + Development: { + commandName: 'Project', + launchBrowser: true, + applicationUrl: 'https://localhost:5001' + } + } + }, null, 2)); + + const outputPath = path.join(projectDir, 'bin', 'Debug', 'net7.0', 'WebProject.dll'); + const { extension } = createDebuggerExtension(outputPath, null, true, true); + const launchConfig: ProjectLaunchConfiguration = { + type: 'project', + project_path: projectPath, + launch_profile: 'Development' + }; + const serverReadyAction = { + action: 'openIntegratedBrowser', + pattern: 'Now listening on:\\s+\\[?(https?://[^\\]\\s]+)' + }; + const debugSessionConfig: AspireExtendedDebugConfiguration = { + type: 'aspire', + request: 'launch', + name: 'Aspire', + program: projectPath, + debuggers: { + project: { + serverReadyAction + } + } + }; + const fakeAspireDebugSession = sinon.createStubInstance(AspireDebugSession); + + const debugConfig = await createDebugSessionConfiguration( + debugSessionConfig, + launchConfig, + undefined, + [], + { debug: true, runId: '1', debugSessionId: '1', isApphost: false, debugSession: fakeAspireDebugSession }, + extension); + + assert.deepStrictEqual(debugConfig.serverReadyAction, serverReadyAction); + + fs.rmSync(tempDir, { recursive: true, force: true }); + }); + test('uses executable path for Executable command launch profiles instead of project output', async () => { // Bug #15647: Executable command profiles use the executablePath and // commandLineArgs to define how to run the class library project. The extension