Growth chat session for new users (#295229)

* Revert Growth agent isReadOnly infrastructure

Remove the isReadOnly extension point property and Growth-specific
infrastructure from the chat sessions API that was added in #294255.
This was scaffolding for a growth session approach via the extension
chatSessions API, which is being replaced with a core-side implementation.

Removed:
- isReadOnly on IChatSessionsExtensionPoint and its schema
- isReadOnly filtering in session target/delegation pickers
- contribution?.isReadOnly parameter from getAgentCanContinueIn
- Growth from getAgentSessionProvider (prevents cache persistence)
- Growth-specific and isReadOnly tests

Kept:
- AgentSessionProviders.Growth enum value (used by new implementation)
- ChatSessionStatus.NeedsInput in proposed API + ext host types
- Growth cases in name/icon/description switch statements

* growth notification for new chat users

* hygiene skill

* Update src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupGrowthSession.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Address PR review feedback

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Josh Spicer
2026-02-13 12:30:22 -08:00
committed by GitHub
parent f378a5c85c
commit 08de9ad064
13 changed files with 393 additions and 29 deletions

25
.github/skills/hygiene/SKILL.md vendored Normal file
View File

@@ -0,0 +1,25 @@
# Hygiene Checks
VS Code runs a hygiene check as a git pre-commit hook. Commits will be rejected if hygiene fails.
## What it checks
The hygiene linter scans all staged `.ts` files for issues including (but not limited to):
- **Unicode characters**: Non-ASCII characters (em-dashes, curly quotes, emoji, etc.) are rejected. Use ASCII equivalents in comments and code.
- **Double-quoted strings**: Only use `"double quotes"` for externalized (localized) strings. Use `'single quotes'` everywhere else.
- **Copyright headers**: All files must include the Microsoft copyright header.
## How it runs
The git pre-commit hook (via husky) runs `npm run precommit`, which executes:
```bash
node --experimental-strip-types build/hygiene.ts
```
This scans only **staged files** (from `git diff --cached`). To run it manually:
```bash
npm run precommit
```

View File

@@ -9,7 +9,6 @@ import { URI } from '../../../../../base/common/uri.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { observableValue } from '../../../../../base/common/observable.js';
import { IChatSessionTiming } from '../../common/chatService/chatService.js';
import { IChatSessionsExtensionPoint } from '../../common/chatSessionsService.js';
import { foreground, listActiveSelectionForeground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
import { getChatSessionType } from '../../common/model/chatUri.js';
@@ -37,7 +36,6 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes
case AgentSessionProviders.Cloud:
case AgentSessionProviders.Claude:
case AgentSessionProviders.Codex:
case AgentSessionProviders.Growth:
return type;
default:
return undefined;
@@ -97,11 +95,7 @@ export function isFirstPartyAgentSessionProvider(provider: AgentSessionProviders
}
}
export function getAgentCanContinueIn(provider: AgentSessionProviders, contribution?: IChatSessionsExtensionPoint): boolean {
// Read-only sessions (e.g., Growth) are passive/informational and cannot be delegation targets
if (contribution?.isReadOnly) {
return false;
}
export function getAgentCanContinueIn(provider: AgentSessionProviders): boolean {
switch (provider) {
case AgentSessionProviders.Local:
case AgentSessionProviders.Background:
@@ -127,7 +121,7 @@ export function getAgentSessionProviderDescription(provider: AgentSessionProvide
case AgentSessionProviders.Codex:
return localize('chat.session.providerDescription.codex', "Opens a new Codex session in the editor. Codex sessions can be managed from the chat sessions view.");
case AgentSessionProviders.Growth:
return localize('chat.session.providerDescription.growth', "Educational messages to help you learn Copilot features.");
return localize('chat.session.providerDescription.growth', "Learn about Copilot features.");
}
}

View File

@@ -1101,6 +1101,15 @@ configurationRegistry.registerConfiguration({
mode: 'auto'
}
},
[ChatConfiguration.GrowthNotificationEnabled]: {
type: 'boolean',
description: nls.localize('chat.growthNotification', "Controls whether to show a growth notification in the agent sessions view to encourage new users to try Copilot."),
default: false,
tags: ['experimental'],
experiment: {
mode: 'auto'
}
},
[ChatConfiguration.RestoreLastPanelSession]: {
type: 'boolean',
description: nls.localize('chat.restoreLastPanelSession', "Controls whether the last session is restored in panel after restart."),

View File

@@ -204,11 +204,6 @@ const extensionPoint = ExtensionsRegistry.registerExtensionPoint<IChatSessionsEx
type: 'boolean',
default: false
},
isReadOnly: {
description: localize('chatSessionsExtPoint.isReadOnly', 'Whether this session type is for read-only agents that do not support interactive chat. This flag is incompatible with \'canDelegate\'.'),
type: 'boolean',
default: false
},
customAgentTarget: {
description: localize('chatSessionsExtPoint.customAgentTarget', 'When set, the chat session will show a filtered mode picker that prefers custom agents whose target property matches this value. Custom agents without a target property are still shown in all session types. This enables the use of standard agent/mode with contributed sessions.'),
type: 'string'
@@ -657,7 +652,7 @@ export class ChatSessionsService extends Disposable implements IChatSessionsServ
private _enableContribution(contribution: IChatSessionsExtensionPoint, ext: IRelaxedExtensionDescription): void {
const disposableStore = new DisposableStore();
this._contributionDisposables.set(contribution.type, disposableStore);
if (contribution.isReadOnly || contribution.canDelegate) {
if (contribution.canDelegate) {
disposableStore.add(this._registerAgent(contribution, ext));
disposableStore.add(this._registerCommands(contribution));
}

View File

@@ -41,12 +41,14 @@ import { IPreferencesService } from '../../../../services/preferences/common/pre
import { IExtension, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { IChatModeService } from '../../common/chatModes.js';
import { IChatSessionsService } from '../../common/chatSessionsService.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
import { CHAT_CATEGORY, CHAT_SETUP_ACTION_ID, CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../actions/chatActions.js';
import { ChatViewContainerId, IChatWidgetService } from '../chat.js';
import { chatViewsWelcomeRegistry } from '../viewsWelcome/chatViewsWelcome.js';
import { ChatSetupAnonymous } from './chatSetup.js';
import { ChatSetupController } from './chatSetupController.js';
import { GrowthSessionController, registerGrowthSession } from './chatSetupGrowthSession.js';
import { AICodeActionsHelper, AINewSymbolNamesProvider, ChatCodeActionsProvider, SetupAgent } from './chatSetupProviders.js';
import { ChatSetup } from './chatSetupRunner.js';
@@ -71,6 +73,8 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService,
@IExtensionService private readonly extensionService: IExtensionService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
@IConfigurationService private readonly configurationService: IConfigurationService,
) {
super();
@@ -83,6 +87,7 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
const controller = new Lazy(() => this._register(this.instantiationService.createInstance(ChatSetupController, context, requests)));
this.registerSetupAgents(context, controller);
this.registerGrowthSession(chatEntitlementService);
this.registerActions(context, requests, controller);
this.registerUrlLinkHandler();
this.checkExtensionInstallation(context);
@@ -171,6 +176,37 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
this._register(Event.runAndSubscribe(context.onDidChange, () => updateRegistration()));
}
private registerGrowthSession(chatEntitlementService: ChatEntitlementService): void {
const growthSessionDisposables = markAsSingleton(new MutableDisposable());
const updateGrowthSession = () => {
const experimentEnabled = this.configurationService.getValue<boolean>(ChatConfiguration.GrowthNotificationEnabled) === true;
// Show for users who don't have the Copilot extension installed yet.
// Additional conditions (e.g., anonymous, entitlement) can be layered here.
const shouldShow = experimentEnabled && !chatEntitlementService.sentiment.installed;
if (shouldShow && !growthSessionDisposables.value) {
const disposables = new DisposableStore();
const controller = disposables.add(this.instantiationService.createInstance(GrowthSessionController));
if (!controller.isDismissed) {
disposables.add(registerGrowthSession(this.chatSessionsService, controller));
// Fully unregister when dismissed to prevent cached session from
// appearing during filtered model updates from other providers.
disposables.add(controller.onDidDismiss(() => {
growthSessionDisposables.clear();
}));
growthSessionDisposables.value = disposables;
} else {
disposables.dispose();
}
} else if (!shouldShow) {
growthSessionDisposables.clear();
}
};
this._register(chatEntitlementService.onDidChangeSentiment(() => updateGrowthSession()));
updateGrowthSession();
}
private registerActions(context: ChatEntitlementContext, requests: ChatEntitlementRequests, controller: Lazy<ChatSetupController>): void {
//#region Global Chat Setup Actions

View File

@@ -0,0 +1,173 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Codicon } from '../../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
import { localize, localize2 } from '../../../../../nls.js';
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { ILifecycleService, LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js';
import { ChatSessionStatus, IChatSessionItem, IChatSessionItemController, IChatSessionsService } from '../../common/chatSessionsService.js';
import { AgentSessionProviders } from '../agentSessions/agentSessions.js';
import { IAgentSession } from '../agentSessions/agentSessionsModel.js';
import { ISessionOpenerParticipant, ISessionOpenOptions, sessionOpenerRegistry } from '../agentSessions/agentSessionsOpener.js';
import { IChatWidgetService } from '../chat.js';
import { CHAT_OPEN_ACTION_ID, IChatViewOpenOptions } from '../actions/chatActions.js';
/**
* Core-side growth session controller that shows a single "attention needed"
* session item in the agent sessions view for anonymous/new users.
*
* When the user clicks the session, we open the chat panel (which triggers the
* anonymous setup flow). When the user opens chat at all, the badge is cleared.
*
* The session is shown at most once, tracked via a storage flag.
*/
export class GrowthSessionController extends Disposable implements IChatSessionItemController {
static readonly STORAGE_KEY = 'chat.growthSession.dismissed';
private static readonly SESSION_URI = URI.from({ scheme: AgentSessionProviders.Growth, path: '/growth-welcome' });
private readonly _onDidChangeChatSessionItems = this._register(new Emitter<void>());
readonly onDidChangeChatSessionItems: Event<void> = this._onDidChangeChatSessionItems.event;
private readonly _onDidDismiss = this._register(new Emitter<void>());
readonly onDidDismiss: Event<void> = this._onDidDismiss.event;
private readonly _created = Date.now();
private _dismissed: boolean;
get isDismissed(): boolean { return this._dismissed; }
constructor(
@IStorageService private readonly storageService: IStorageService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@ILogService private readonly logService: ILogService,
) {
super();
this._dismissed = this.storageService.getBoolean(GrowthSessionController.STORAGE_KEY, StorageScope.APPLICATION, false);
// Dismiss the growth session when the user opens chat.
// Wait until the workbench is fully restored so we skip widgets
// that were restored from a previous session at startup.
this.lifecycleService.when(LifecyclePhase.Restored).then(() => {
if (this._store.isDisposed || this._dismissed) {
return;
}
this._register(this.chatWidgetService.onDidAddWidget(() => {
this.dismiss();
}));
});
}
get items(): readonly IChatSessionItem[] {
if (this._dismissed) {
return [];
}
return [{
resource: GrowthSessionController.SESSION_URI,
label: localize('growthSession.label', "Try Copilot"),
description: localize('growthSession.description', "GitHub Copilot is available. Try it for free."),
status: ChatSessionStatus.NeedsInput,
iconPath: Codicon.lightbulb,
timing: {
created: this._created,
lastRequestStarted: undefined,
lastRequestEnded: undefined,
},
}];
}
async refresh(): Promise<void> {
// Nothing to refresh -- this is a static, local-only session item
}
private dismiss(): void {
if (this._dismissed) {
return;
}
this.logService.trace('[GrowthSession] Dismissing growth session');
this._dismissed = true;
this.storageService.store(GrowthSessionController.STORAGE_KEY, true, StorageScope.APPLICATION, StorageTarget.USER);
// Fire change event first so that listeners (like the model) see empty items
this._onDidChangeChatSessionItems.fire();
// Then fire dismiss event which triggers unregistration of the controller.
this._onDidDismiss.fire();
}
}
/**
* Handles clicks on the growth session item in the agent sessions view.
* Opens a new local chat session with a pre-seeded welcome message.
* The user can then send messages that go through the normal agent.
*/
export class GrowthSessionOpenerParticipant implements ISessionOpenerParticipant {
async handleOpenSession(accessor: ServicesAccessor, session: IAgentSession, _openOptions?: ISessionOpenOptions): Promise<boolean> {
if (session.providerType !== AgentSessionProviders.Growth) {
return false;
}
const commandService = accessor.get(ICommandService);
const opts: IChatViewOpenOptions = {
query: '',
isPartialQuery: true,
previousRequests: [{
request: localize('growthSession.previousRequest', "Tell me about GitHub Copilot!"),
// allow-any-unicode-next-line
response: localize('growthSession.previousResponse', "Welcome to GitHub Copilot, your AI coding assistant! Here are some things you can try:\n\n- 🐛 *\"Help me debug this error\"* — paste an error message and get a fix\n- 🧪 *\"Write tests for my function\"* — select code and ask for unit tests\n- 💡 *\"Explain this code\"* — highlight something unfamiliar and ask what it does\n- 🚀 *\"Scaffold a REST API\"* — describe what you want and let Agent mode build it\n- 🎨 *\"Refactor this to be more readable\"* — select messy code and clean it up\n\nType anything below to get started!"),
}],
};
await commandService.executeCommand(CHAT_OPEN_ACTION_ID, opts);
return true;
}
}
/**
* Registers the growth session controller and opener participant.
* Returns a disposable that cleans up all registrations.
*/
export function registerGrowthSession(chatSessionsService: IChatSessionsService, growthController: GrowthSessionController): IDisposable {
const disposables = new DisposableStore();
// Register as session item controller so it appears in the sessions view
disposables.add(chatSessionsService.registerChatSessionItemController(AgentSessionProviders.Growth, growthController));
// Register opener participant so clicking the growth session opens chat
disposables.add(sessionOpenerRegistry.registerParticipant(new GrowthSessionOpenerParticipant()));
return disposables;
}
// #region Developer Actions
registerAction2(class ResetGrowthSessionAction extends Action2 {
constructor() {
super({
id: 'workbench.action.chat.resetGrowthSession',
title: localize2('resetGrowthSession', "Reset Growth Session Notification"),
category: localize2('developer', "Developer"),
f1: true,
});
}
run(accessor: ServicesAccessor): void {
const storageService = accessor.get(IStorageService);
storageService.remove(GrowthSessionController.STORAGE_KEY, StorageScope.APPLICATION);
}
});
// #endregion

View File

@@ -132,7 +132,7 @@ export class ChatSuggestNextWidget extends Disposable {
return false;
}
const provider = getAgentSessionProvider(c.type);
return provider !== undefined && getAgentCanContinueIn(provider, c);
return provider !== undefined && getAgentCanContinueIn(provider);
});
if (showContinueOn && availableContributions.length > 0) {

View File

@@ -54,8 +54,7 @@ export class DelegationSessionPickerActionItem extends SessionTypePickerActionIt
return true; // Always show active session type
}
const contribution = this.chatSessionsService.getChatSessionContribution(type);
return getAgentCanContinueIn(type, contribution);
return getAgentCanContinueIn(type);
}
protected override _getSessionCategory(sessionTypeItem: ISessionTypeItem) {

View File

@@ -163,10 +163,6 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
const contributions = this.chatSessionsService.getAllChatSessionContributions();
for (const contribution of contributions) {
if (contribution.isReadOnly) {
continue; // Read-only sessions are not interactive and should not appear in session target picker
}
const agentSessionType = getAgentSessionProvider(contribution.type);
if (!agentSessionType) {
continue;

View File

@@ -85,7 +85,6 @@ export interface IChatSessionsExtensionPoint {
readonly capabilities?: IChatAgentAttachmentCapabilities;
readonly commands?: IChatSessionCommandContribution[];
readonly canDelegate?: boolean;
readonly isReadOnly?: boolean;
/**
* When set, the chat session will show a filtered mode picker with custom agents
* that have a matching `target` property. This enables contributed chat sessions

View File

@@ -49,6 +49,7 @@ export enum ChatConfiguration {
ExitAfterDelegation = 'chat.exitAfterDelegation',
AgentsControlClickBehavior = 'chat.agentsControl.clickBehavior',
ExplainChangesEnabled = 'chat.editing.explainChanges.enabled',
GrowthNotificationEnabled = 'chat.growthNotification.enabled',
}
/**

View File

@@ -2100,13 +2100,8 @@ suite('AgentSessions', () => {
suite('AgentSessionsViewModel - getAgentCanContinueIn', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('should return false when contribution.isReadOnly is true', () => {
const result = getAgentCanContinueIn(AgentSessionProviders.Cloud, { type: 'test', name: 'test', displayName: 'Test', description: 'test', isReadOnly: true });
assert.strictEqual(result, false);
});
test('should return true for Cloud when contribution is not read-only', () => {
const result = getAgentCanContinueIn(AgentSessionProviders.Cloud, { type: 'test', name: 'test', displayName: 'Test', description: 'test', isReadOnly: false });
test('should return true for Cloud provider', () => {
const result = getAgentCanContinueIn(AgentSessionProviders.Cloud);
assert.strictEqual(result, true);
});

View File

@@ -0,0 +1,142 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { Emitter } from '../../../../../../base/common/event.js';
import { DisposableStore } from '../../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../../base/common/uri.js';
import { Codicon } from '../../../../../../base/common/codicons.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js';
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ILifecycleService } from '../../../../../services/lifecycle/common/lifecycle.js';
import { TestLifecycleService, workbenchInstantiationService } from '../../../../../test/browser/workbenchTestServices.js';
import { ChatSessionStatus } from '../../../common/chatSessionsService.js';
import { AgentSessionProviders } from '../../../browser/agentSessions/agentSessions.js';
import { IAgentSession } from '../../../browser/agentSessions/agentSessionsModel.js';
import { GrowthSessionController, GrowthSessionOpenerParticipant } from '../../../browser/chatSetup/chatSetupGrowthSession.js';
import { IChatWidget, IChatWidgetService } from '../../../browser/chat.js';
import { MockChatWidgetService } from '../widget/mockChatWidget.js';
import { ServicesAccessor } from '../../../../../../editor/browser/editorExtensions.js';
class TestMockChatWidgetService extends MockChatWidgetService {
private readonly _onDidAddWidget = new Emitter<IChatWidget>();
override readonly onDidAddWidget = this._onDidAddWidget.event;
fireDidAddWidget(): void {
this._onDidAddWidget.fire(undefined!);
}
dispose(): void {
this._onDidAddWidget.dispose();
}
}
suite('GrowthSessionController', () => {
const disposables = new DisposableStore();
let instantiationService: TestInstantiationService;
let mockWidgetService: TestMockChatWidgetService;
setup(() => {
instantiationService = disposables.add(workbenchInstantiationService(undefined, disposables));
mockWidgetService = new TestMockChatWidgetService();
disposables.add({ dispose: () => mockWidgetService.dispose() });
const mockLifecycleService = disposables.add(new TestLifecycleService());
instantiationService.stub(IChatWidgetService, mockWidgetService);
instantiationService.stub(ILifecycleService, mockLifecycleService);
});
teardown(() => {
disposables.clear();
});
ensureNoDisposablesAreLeakedInTestSuite();
test('should return a single NeedsInput session item', () => {
const controller = disposables.add(instantiationService.createInstance(GrowthSessionController));
const items = controller.items;
assert.strictEqual(items.length, 1);
assert.strictEqual(items[0].status, ChatSessionStatus.NeedsInput);
assert.strictEqual(items[0].label, 'Try Copilot');
assert.ok(items[0].resource.scheme === AgentSessionProviders.Growth);
});
test('should return empty items after dismiss', async () => {
const controller = disposables.add(instantiationService.createInstance(GrowthSessionController));
assert.strictEqual(controller.items.length, 1);
// Allow the lifecycle.when() promise to resolve and register the listener
await new Promise<void>(r => setTimeout(r, 0));
// Fire widget add — should dismiss
mockWidgetService.fireDidAddWidget();
assert.strictEqual(controller.items.length, 0);
});
test('should fire onDidChangeChatSessionItems on dismiss', async () => {
const controller = disposables.add(instantiationService.createInstance(GrowthSessionController));
let fired = false;
disposables.add(controller.onDidChangeChatSessionItems(() => {
fired = true;
}));
await new Promise<void>(r => setTimeout(r, 0));
mockWidgetService.fireDidAddWidget();
assert.strictEqual(fired, true);
});
test('should not fire onDidChangeChatSessionItems twice', async () => {
const controller = disposables.add(instantiationService.createInstance(GrowthSessionController));
let fireCount = 0;
disposables.add(controller.onDidChangeChatSessionItems(() => {
fireCount++;
}));
await new Promise<void>(r => setTimeout(r, 0));
mockWidgetService.fireDidAddWidget();
mockWidgetService.fireDidAddWidget();
assert.strictEqual(fireCount, 1);
});
test('refresh is a no-op', async () => {
const controller = disposables.add(instantiationService.createInstance(GrowthSessionController));
await controller.refresh();
assert.strictEqual(controller.items.length, 1);
});
});
suite('GrowthSessionOpenerParticipant', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('should return false for non-Growth sessions', async () => {
const participant = new GrowthSessionOpenerParticipant();
const session: IAgentSession = {
providerType: AgentSessionProviders.Local,
providerLabel: 'Local',
resource: URI.parse('local://session-1'),
status: ChatSessionStatus.Completed,
label: 'Test Session',
icon: Codicon.vm,
timing: { created: Date.now(), lastRequestStarted: undefined, lastRequestEnded: undefined },
isArchived: () => false,
setArchived: () => { },
isRead: () => true,
setRead: () => { },
};
// The participant checks providerType before touching the accessor,
// so a stub accessor is sufficient for this test path.
const stubAccessor = { get: () => undefined } as unknown as ServicesAccessor;
const result = await participant.handleOpenSession(stubAccessor, session);
assert.strictEqual(result, false);
});
});