Skip to content

Commit e3fe7a3

Browse files
Copilotvhvb1989
andcommitted
Fix VS Code extension commands failing with undefined fsPath in virtual file systems (#6601)
* Initial plan * Add defensive checks for undefined fsPath in VS Code extension commands This fixes the issue where provision and other commands would fail with "The 'path' argument must be of type string. Received undefined" when used with virtual file systems or certain VS Code contexts. Changes: - Added validation for selectedFile.fsPath before calling getWorkingFolder - Provides clear error messages that include URI scheme and selectedItem type - Suppresses automatic issue reporting since this is a user error - Applied fix to all affected commands: provision, deploy, up, down, restore, monitor, packageCli, and pipelineConfig - Added unit tests for the new validation logic Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> * Simplify provision.test.ts based on code review feedback Removed unused stub functions that weren't actually being used. Simplified tests to focus on the core validation logic. Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> * Refactor validation logic into shared utility function Based on code review feedback: - Extracted duplicated validation logic to validateFileSystemUri() in cmdUtil.ts - Updated all 8 command files to use the shared function - Simplified test assertions - Improved code maintainability by reducing duplication Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> * Fix validation to use strict equality check for undefined Changed condition from `!selectedFile.fsPath` to `selectedFile.fsPath === undefined` to avoid incorrectly rejecting empty string paths which are valid for root directories. Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> * Fix l10n.t() to use single string literal instead of concatenation The first argument to vscode.l10n.t() should be a single string literal for proper localization tooling compatibility. Removed string concatenation and used a single multi-line string literal instead. Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vhvb1989 <24213737+vhvb1989@users.noreply.github.com>
1 parent cf8e455 commit e3fe7a3

File tree

10 files changed

+128
-8
lines changed

10 files changed

+128
-8
lines changed

ext/vscode/src/commands/cmdUtil.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,39 @@ import * as vscode from 'vscode';
99
import { createAzureDevCli } from '../utils/azureDevCli';
1010
import { execAsync } from '../utils/execAsync';
1111
import { fileExists } from '../utils/fileUtils';
12+
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1213

1314
const AzureYamlGlobPattern: vscode.GlobPattern = '**/[aA][zZ][uU][rR][eE].{[yY][aA][mM][lL],[yY][mM][lL]}';
1415

16+
/**
17+
* Validates that a URI has a valid fsPath for file system operations.
18+
* Virtual file systems or certain VS Code contexts may not provide a valid fsPath.
19+
* @param context The action context
20+
* @param selectedFile The URI to validate
21+
* @param selectedItem The original selected item (for error message context)
22+
* @param commandName The name of the command being executed (for error message)
23+
* @throws Error if the URI doesn't have a valid fsPath
24+
*/
25+
export function validateFileSystemUri(
26+
context: IActionContext,
27+
selectedFile: vscode.Uri | undefined,
28+
selectedItem: vscode.Uri | TreeViewModel | undefined,
29+
commandName: string
30+
): void {
31+
if (selectedFile && selectedFile.fsPath === undefined) {
32+
context.errorHandling.suppressReportIssue = true;
33+
const itemType = isTreeViewModel(selectedItem) ? 'TreeViewModel' :
34+
isAzureDevCliModel(selectedItem) ? 'AzureDevCliModel' :
35+
selectedItem ? 'vscode.Uri' : 'undefined';
36+
throw new Error(vscode.l10n.t(
37+
"Unable to determine working folder for {0} command. The selected file has an unsupported URI scheme '{1}' (selectedItem type: {2}). Azure Developer CLI commands are not supported in virtual file systems. Please open a local folder or clone the repository locally.",
38+
commandName,
39+
selectedFile.scheme,
40+
itemType
41+
));
42+
}
43+
}
44+
1545
// If the command was invoked with a specific file context, use the file context as the working directory for running Azure developer CLI commands.
1646
// Otherwise search the workspace for "azure.yaml" or "azure.yml" files. If only one is found, use it (i.e. its folder). If more than one is found, ask the user which one to use.
1747
// If at this point we still do not have a working directory, prompt the user to select one.

ext/vscode/src/commands/deploy.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { executeAsTask } from '../utils/executeAsTask';
1010
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1111
import { AzureDevCliModel } from '../views/workspace/AzureDevCliModel';
1212
import { AzureDevCliService } from '../views/workspace/AzureDevCliService';
13-
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
13+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
1414

1515
export async function deploy(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
1616
let selectedModel: AzureDevCliModel | undefined;
@@ -26,6 +26,10 @@ export async function deploy(context: IActionContext, selectedItem?: vscode.Uri
2626
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2727
selectedFile = selectedItem!;
2828
}
29+
30+
// Validate that selectedFile is valid for file system operations
31+
validateFileSystemUri(context, selectedFile, selectedItem, 'deploy');
32+
2933
const workingFolder = await getWorkingFolder(context, selectedFile);
3034

3135
const azureCli = await createAzureDevCli(context);

ext/vscode/src/commands/down.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
99
import { executeAsTask } from '../utils/executeAsTask';
1010
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1111
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
12-
import { getAzDevTerminalTitle, getWorkingFolder, } from './cmdUtil';
12+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri, } from './cmdUtil';
1313

1414
/**
1515
* A tuple representing the arguments that must be passed to the `down` command when executed via {@link vscode.commands.executeCommand}
@@ -28,6 +28,10 @@ export async function down(context: IActionContext, selectedItem?: vscode.Uri |
2828
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2929
selectedFile = selectedItem!;
3030
}
31+
32+
// Validate that selectedFile is valid for file system operations
33+
validateFileSystemUri(context, selectedFile, selectedItem, 'down');
34+
3135
const workingFolder = await getWorkingFolder(context, selectedFile);
3236

3337
const confirmPrompt = vscode.l10n.t("Are you sure you want to delete all this application's Azure resources? You can soft-delete certain resources like Azure KeyVaults to preserve their data, or permanently delete and purge them.");

ext/vscode/src/commands/monitor.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
88
import { execAsync } from '../utils/execAsync';
99
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1010
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
11-
import { getWorkingFolder } from './cmdUtil';
11+
import { getWorkingFolder, validateFileSystemUri } from './cmdUtil';
1212

1313
const MonitorChoices: IAzureQuickPickItem<string>[] = [
1414
{
@@ -36,6 +36,10 @@ export async function monitor(context: IActionContext, selectedItem?: vscode.Uri
3636
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
3737
selectedFile = selectedItem!;
3838
}
39+
40+
// Validate that selectedFile is valid for file system operations
41+
validateFileSystemUri(context, selectedFile, selectedItem, 'monitor');
42+
3943
const workingFolder = await getWorkingFolder(context, selectedFile);
4044

4145
const monitorChoices = await context.ui.showQuickPick(MonitorChoices, {

ext/vscode/src/commands/packageCli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { executeAsTask } from '../utils/executeAsTask';
1010
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1111
import { AzureDevCliModel } from '../views/workspace/AzureDevCliModel';
1212
import { AzureDevCliService } from '../views/workspace/AzureDevCliService';
13-
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
13+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
1414

1515
// `package` is a reserved identifier so `packageCli` had to be used instead
1616
export async function packageCli(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
@@ -27,6 +27,10 @@ export async function packageCli(context: IActionContext, selectedItem?: vscode.
2727
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2828
selectedFile = selectedItem!;
2929
}
30+
31+
// Validate that selectedFile is valid for file system operations
32+
validateFileSystemUri(context, selectedFile, selectedItem, 'package');
33+
3034
const workingFolder = await getWorkingFolder(context, selectedFile);
3135

3236
const azureCli = await createAzureDevCli(context);

ext/vscode/src/commands/pipeline.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { IActionContext } from '@microsoft/vscode-azext-utils';
55
import { composeArgs, withArg } from '@microsoft/vscode-processutils';
66
import * as vscode from 'vscode';
7-
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
7+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
88
import { TelemetryId } from '../telemetry/telemetryId';
99
import { createAzureDevCli } from '../utils/azureDevCli';
1010
import { executeAsTask } from '../utils/executeAsTask';
@@ -28,6 +28,10 @@ export async function pipelineConfig(context: IActionContext, selectedItem?: vsc
2828
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2929
selectedFile = selectedItem!;
3030
}
31+
32+
// Validate that selectedFile is valid for file system operations
33+
validateFileSystemUri(context, selectedFile, selectedItem, 'pipeline config');
34+
3135
const workingFolder = await getWorkingFolder(context, selectedFile);
3236

3337
const azureCli = await createAzureDevCli(context);

ext/vscode/src/commands/provision.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
99
import { executeAsTask } from '../utils/executeAsTask';
1010
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1111
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
12-
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
12+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
1313

1414
export async function provision(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
1515
let selectedFile: vscode.Uri | undefined;
@@ -21,6 +21,10 @@ export async function provision(context: IActionContext, selectedItem?: vscode.U
2121
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2222
selectedFile = selectedItem!;
2323
}
24+
25+
// Validate that selectedFile is valid for file system operations
26+
validateFileSystemUri(context, selectedFile, selectedItem, 'provision');
27+
2428
const workingFolder = await getWorkingFolder(context, selectedFile);
2529

2630
const azureCli = await createAzureDevCli(context);

ext/vscode/src/commands/restore.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { executeAsTask } from '../utils/executeAsTask';
1010
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1111
import { AzureDevCliModel } from '../views/workspace/AzureDevCliModel';
1212
import { AzureDevCliService } from '../views/workspace/AzureDevCliService';
13-
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
13+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
1414

1515
export async function restore(context: IActionContext, selectedItem?: vscode.Uri | TreeViewModel): Promise<void> {
1616
let selectedModel: AzureDevCliModel | undefined;
@@ -26,6 +26,10 @@ export async function restore(context: IActionContext, selectedItem?: vscode.Uri
2626
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2727
selectedFile = selectedItem!;
2828
}
29+
30+
// Validate that selectedFile is valid for file system operations
31+
validateFileSystemUri(context, selectedFile, selectedItem, 'restore');
32+
2933
const workingFolder = await getWorkingFolder(context, selectedFile);
3034

3135
const azureCli = await createAzureDevCli(context);

ext/vscode/src/commands/up.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { createAzureDevCli } from '../utils/azureDevCli';
99
import { executeAsTask } from '../utils/executeAsTask';
1010
import { isAzureDevCliModel, isTreeViewModel, TreeViewModel } from '../utils/isTreeViewModel';
1111
import { AzureDevCliApplication } from '../views/workspace/AzureDevCliApplication';
12-
import { getAzDevTerminalTitle, getWorkingFolder } from './cmdUtil';
12+
import { getAzDevTerminalTitle, getWorkingFolder, validateFileSystemUri } from './cmdUtil';
1313

1414
/**
1515
* A tuple representing the arguments that must be passed to the `up` command when executed via {@link vscode.commands.executeCommand}
@@ -28,6 +28,10 @@ export async function up(context: IActionContext, selectedItem?: vscode.Uri | Tr
2828
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
2929
selectedFile = selectedItem!;
3030
}
31+
32+
// Validate that selectedFile is valid for file system operations
33+
validateFileSystemUri(context, selectedFile, selectedItem, 'up');
34+
3135
const workingFolder = await getWorkingFolder(context, selectedFile);
3236

3337
const azureCli = await createAzureDevCli(context);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { expect } from 'chai';
5+
import * as vscode from 'vscode';
6+
import * as sinon from 'sinon';
7+
import { provision } from '../../../commands/provision';
8+
import { IActionContext } from '@microsoft/vscode-azext-utils';
9+
10+
suite('provision command', () => {
11+
let sandbox: sinon.SinonSandbox;
12+
let mockContext: IActionContext;
13+
14+
setup(() => {
15+
sandbox = sinon.createSandbox();
16+
17+
mockContext = {
18+
errorHandling: {
19+
suppressReportIssue: false
20+
},
21+
telemetry: {
22+
properties: {}
23+
}
24+
} as unknown as IActionContext;
25+
});
26+
27+
teardown(() => {
28+
sandbox.restore();
29+
});
30+
31+
test('throws error when selectedFile has undefined fsPath (virtual file system)', async () => {
32+
// Mock the URI to ensure fsPath is undefined - simulates virtual file system
33+
const mockUri = {
34+
scheme: 'virtual',
35+
fsPath: undefined,
36+
authority: '',
37+
path: '',
38+
query: '',
39+
fragment: '',
40+
with: () => mockUri,
41+
toString: () => 'virtual:/test'
42+
} as unknown as vscode.Uri;
43+
44+
try {
45+
await provision(mockContext, mockUri);
46+
expect.fail('Should have thrown an error');
47+
} catch (error) {
48+
expect(error).to.be.instanceOf(Error);
49+
const errMessage = (error as Error).message;
50+
expect(errMessage).to.include('Unable to determine working folder');
51+
expect(errMessage).to.include('virtual');
52+
expect(errMessage).to.include('vscode.Uri');
53+
expect(errMessage).to.include('virtual file systems');
54+
}
55+
56+
expect(mockContext.errorHandling.suppressReportIssue).to.equal(true, 'Should suppress automatic issue reporting for user errors');
57+
});
58+
});

0 commit comments

Comments
 (0)