Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/instructions/vs-code-designer.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ Each test runs in its own fresh VS Code session to avoid workspace-switch conten
| 4.5 | designerViewExtended.test.ts | Parallel branches + run-after (ADO #10109401) |
| 4.6 | keyboardNavigation.test.ts | Ctrl+Up/Down navigation (ADO #10273324) |
| 4.7 | dataMapper.test.ts, demo, smoke, standalone | Data Mapper + generic tests |
| 4.9 | azuriteAutostartFailure.test.ts, azuriteAutostartFailureAssert.test.ts | Azurite auto-start debug regression |

### Shared Helper Modules

Expand Down Expand Up @@ -179,17 +180,20 @@ Each test runs in its own fresh VS Code session to avoid workspace-switch conten

12. **Always run tests automatically after creating or modifying them**: After writing or editing any test file, immediately: lint (`npx biome check --write`), build (`npx tsup`), and run (`node src/test/ui/run-e2e.js`) — don't wait for the user to ask. Report pass/fail results with any failure details.

13. **Debug regression tests must use the real workspace launch flow**: Create the workspace through the Create Workspace webview, reopen the generated `.code-workspace` in a fresh `run-e2e.js` phase, wait for `workflow-designtime/`, and validate terminal/output/log evidence. Do not replace the suite path with one-off scripts, hand-made workspaces, or the wrong launch configuration.

### Running Tests

```bash
cd apps/vs-code-designer
npx tsup --config tsup.e2e.test.config.ts # Compile

# Run modes:
$env:E2E_MODE = "full" # All phases (4.1-4.7)
$env:E2E_MODE = "full" # All phases (4.1-4.9)
$env:E2E_MODE = "createonly" # Phase 4.1 only
$env:E2E_MODE = "designeronly" # Phase 4.2 only
$env:E2E_MODE = "newtestsonly" # Phases 4.3-4.6 only
$env:E2E_MODE = "azuriteonly" # Phase 4.9 Azurite auto-start debug regression
node src/test/ui/run-e2e.js
```

Expand Down
6 changes: 5 additions & 1 deletion apps/vs-code-designer/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ Each test runs in its own fresh VS Code session to avoid workspace-switch conten
| 4.5 | designerViewExtended.test.ts | Parallel branches + run-after (ADO #10109401) |
| 4.6 | keyboardNavigation.test.ts | Ctrl+Up/Down navigation (ADO #10273324) |
| 4.7 | dataMapper.test.ts, demo, smoke, standalone | Data Mapper + generic tests |
| 4.9 | azuriteAutostartFailure.test.ts, azuriteAutostartFailureAssert.test.ts | Azurite auto-start debug regression |

### Shared Helper Modules

Expand Down Expand Up @@ -179,17 +180,20 @@ Each test runs in its own fresh VS Code session to avoid workspace-switch conten

12. **Always run tests automatically after creating or modifying them**: After writing or editing any test file, immediately: lint (`npx biome check --write`), build (`npx tsup`), and run (`node src/test/ui/run-e2e.js`) — don't wait for the user to ask. Report pass/fail results with any failure details.

13. **Debug regression tests must use the real workspace launch flow**: Create the workspace through the Create Workspace webview, reopen the generated `.code-workspace` in a fresh `run-e2e.js` phase, wait for `workflow-designtime/`, and validate terminal/output/log evidence. Do not replace the suite path with one-off scripts, hand-made workspaces, or the wrong launch configuration.

### Running Tests

```bash
cd apps/vs-code-designer
npx tsup --config tsup.e2e.test.config.ts # Compile

# Run modes:
$env:E2E_MODE = "full" # All phases (4.1-4.7)
$env:E2E_MODE = "full" # All phases (4.1-4.9)
$env:E2E_MODE = "createonly" # Phase 4.1 only
$env:E2E_MODE = "designeronly" # Phase 4.2 only
$env:E2E_MODE = "newtestsonly" # Phases 4.3-4.6 only
$env:E2E_MODE = "azuriteonly" # Phase 4.9 Azurite auto-start debug regression
node src/test/ui/run-e2e.js
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { azuriteExtensionId, extensionCommand } from '../../../constants';
import { executeOnAzurite } from '../executeOnAzuriteExt';

const vscodeMocks = vi.hoisted(() => ({
executeCommand: vi.fn(),
getExtension: vi.fn(),
}));

vi.mock('vscode', () => ({
commands: {
executeCommand: vscodeMocks.executeCommand,
},
extensions: {
getExtension: vscodeMocks.getExtension,
},
}));

describe('executeOnAzurite', () => {
const context = {
telemetry: {
properties: {},
measurements: {},
},
ui: {
showWarningMessage: vi.fn(),
},
} as any;

beforeEach(() => {
vi.clearAllMocks();
context.telemetry.properties = {};
});

it('throws a startup error when the Azurite extension is unavailable', async () => {
vscodeMocks.getExtension.mockReturnValue(undefined);

await expect(executeOnAzurite(context, extensionCommand.azureAzuriteStart)).rejects.toThrow(
'Azurite extension is not installed or is unavailable in the current VS Code extension host.'
);

expect(vscodeMocks.getExtension).toHaveBeenCalledWith(azuriteExtensionId);
expect(vscodeMocks.executeCommand).not.toHaveBeenCalled();
expect(context.ui.showWarningMessage).not.toHaveBeenCalled();
expect(context.telemetry.properties.azuriteExtensionAvailable).toBe('false');
});

it('activates the Azurite extension before issuing the start command', async () => {
const activate = vi.fn(async () => undefined);
vscodeMocks.getExtension.mockReturnValue({
isActive: false,
activate,
});

await executeOnAzurite(context, extensionCommand.azureAzuriteStart);

expect(activate).toHaveBeenCalledTimes(1);
expect(vscodeMocks.executeCommand).toHaveBeenCalledWith(extensionCommand.azureAzuriteStart, {});
expect(context.telemetry.properties.azuriteExtensionAvailable).toBe('true');
expect(context.telemetry.properties.azuriteExtensionActive).toBe('true');
expect(context.telemetry.properties.azuriteStartCommandIssued).toBe('true');
});

it('throws a startup error when the Azurite extension fails activation', async () => {
vscodeMocks.getExtension.mockReturnValue({
isActive: false,
activate: vi.fn(async () => {
throw new Error('activation failed');
}),
});

await expect(executeOnAzurite(context, extensionCommand.azureAzuriteStart)).rejects.toThrow(
'Azurite extension could not be activated.'
);

expect(vscodeMocks.executeCommand).not.toHaveBeenCalled();
expect(context.telemetry.properties.azuriteExtensionAvailable).toBe('true');
expect(context.telemetry.properties.azuriteExtensionActive).toBe('false');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,35 @@ import * as vscode from 'vscode';
export async function executeOnAzurite(context: IActionContext, command: string, ...args: any[]): Promise<void> {
const azuriteExtension = extensions.getExtension(azuriteExtensionId);

if (azuriteExtension?.isActive) {
vscode.commands.executeCommand(command, {
...args,
});
} else {
const message: string = localize('deactivatedAzuriteExt', 'Azurite extension is deactivated, make sure to activate it');
await context.ui.showWarningMessage(message);
if (!azuriteExtension) {
context.telemetry.properties.azuriteExtensionAvailable = 'false';
throw new Error(
localize(
'missingAzuriteExt',
'Azurite extension is not installed or is unavailable in the current VS Code extension host. Make sure the Azurite extension is installed and enabled, then try debugging again.'
)
);
}

context.telemetry.properties.azuriteExtensionAvailable = 'true';
if (!azuriteExtension.isActive) {
context.telemetry.properties.azuriteExtensionActive = 'false';
try {
await azuriteExtension.activate();
} catch (error) {
throw new Error(
localize(
'activateAzuriteExtFailed',
'Azurite extension could not be activated. Make sure the Azurite extension is installed and enabled, then try debugging again. {0}',
error instanceof Error ? error.message : String(error)
)
);
}
}

context.telemetry.properties.azuriteExtensionActive = 'true';
context.telemetry.properties.azuriteStartCommandIssued = 'true';
await vscode.commands.executeCommand(command, {
...args,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import { preDebugValidate } from '../../debug/validatePreDebug';
import { verifyLocalConnectionKeys } from '../../utils/appSettings/connectionKeys';
import { activateAzurite } from '../../utils/azurite/activateAzurite';
import { getProjFiles } from '../../utils/dotnet/dotnet';
import { tryBuildCustomCodeFunctionsProject } from '../buildCustomCodeFunctionsProject';
import { pickFuncProcessInternal } from '../pickFuncProcess';

const capturedMessages: string[] = [];
const telemetryContexts: any[] = [];
const azuriteTimeoutMessage =
'Azurite did not become ready within "5" seconds. Make sure the Azurite extension is installed and running, then try debugging again.';

vi.mock('@microsoft/vscode-azext-utils', () => {
return {
callWithTelemetryAndErrorHandling: vi.fn(async (_callbackId: string, callback: (context: any) => Promise<unknown>) => {
const context = {
telemetry: {
properties: {},
measurements: {},
},
errorHandling: {},
ui: {
showWarningMessage: vi.fn(async (message: string) => {
capturedMessages.push(message);
return undefined;
}),
},
};
telemetryContexts.push(context);
try {
return await callback(context);
} catch (error) {
if (!context.errorHandling.suppressDisplay) {
capturedMessages.push(error instanceof Error ? error.message : String(error));
}
if (context.errorHandling.rethrow) {
throw error;
}
return undefined;
}
}),
UserCancelledError: class UserCancelledError extends Error {},
};
});

vi.mock('../../debug/validatePreDebug', () => ({
preDebugValidate: vi.fn(async () => {
capturedMessages.push(
'Failed to verify "AzureWebJobsStorage" connection specified in "local.settings.json". Is the local emulator installed and running?'
);
return false;
}),
}));

vi.mock('../../utils/azurite/activateAzurite', () => ({
activateAzurite: vi.fn(),
}));

vi.mock('../../utils/appSettings/connectionKeys', () => ({
verifyLocalConnectionKeys: vi.fn(),
}));

vi.mock('../../utils/dotnet/dotnet', () => ({
getProjFiles: vi.fn(),
}));

vi.mock('../buildCustomCodeFunctionsProject', () => ({
tryBuildCustomCodeFunctionsProject: vi.fn(),
}));

describe('pickFuncProcess Azurite startup', () => {
const projectPath = 'D:\\workspace\\LogicApp';
const workspaceFolder = { uri: vscode.Uri.file(projectPath), name: 'LogicApp', index: 0 };
const debugConfig = { type: 'workflow', request: 'attach', name: 'Attach to Logic App' };
const context = {
telemetry: {
properties: {},
measurements: {},
},
} as any;

beforeEach(() => {
vi.clearAllMocks();
capturedMessages.length = 0;
telemetryContexts.length = 0;
vi.mocked(activateAzurite).mockRejectedValue(new Error(azuriteTimeoutMessage));
vi.mocked(verifyLocalConnectionKeys).mockResolvedValue(undefined);
vi.mocked(getProjFiles).mockResolvedValue([]);
vi.mocked(tryBuildCustomCodeFunctionsProject).mockResolvedValue(undefined);
});

it('stops debug startup after Azurite auto-start fails without showing AzureWebJobsStorage warning', async () => {
await expect(pickFuncProcessInternal(context, debugConfig, workspaceFolder, projectPath)).rejects.toThrow(azuriteTimeoutMessage);

expect(capturedMessages).not.toContain(azuriteTimeoutMessage);
expect(activateAzurite).toHaveBeenCalledWith(telemetryContexts[0], projectPath);
expect(capturedMessages).not.toContain(
'Failed to verify "AzureWebJobsStorage" connection specified in "local.settings.json". Is the local emulator installed and running?'
);
expect(verifyLocalConnectionKeys).not.toHaveBeenCalled();
expect(preDebugValidate).not.toHaveBeenCalled();
expect(tryBuildCustomCodeFunctionsProject).not.toHaveBeenCalled();
});
});
8 changes: 7 additions & 1 deletion apps/vs-code-designer/src/app/commands/pickFuncProcess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,14 @@ export async function pickFuncProcessInternal(
projectPath: string
): Promise<string | undefined> {
await callWithTelemetryAndErrorHandling(autoStartAzuriteSetting, async (actionContext: IActionContext) => {
actionContext.errorHandling.rethrow = true;
await runWithDurationTelemetry(actionContext, autoStartAzuriteSetting, async () => {
await activateAzurite(context, projectPath);
try {
await activateAzurite(actionContext, projectPath);
} catch (error) {
actionContext.errorHandling.suppressDisplay = true;
throw error instanceof Error ? error : new Error(String(error));
}
});
});

Expand Down
Loading
Loading