Skip to content

Commit 5766e4b

Browse files
authored
test: add notification and reset state dev options (#6105)
- Add a generic dev menu item to reset the state of a feature. Simply extend with a function handler to add an item to the menu. The initial item resets in-ide notification state. ![image](https://github.com/user-attachments/assets/042bdad2-2bac-4694-a7ff-801a857bddd3) ![image](https://github.com/user-attachments/assets/43fa90aa-af58-4b6f-af0d-ca38fb01f5b7) - Add a way to test notifications locally without modifying code. Using a dev menu item you can add a notification to global state and receive it in your IDE for visualization. ![image](https://github.com/user-attachments/assets/2a0f267c-e792-4535-a1a4-170f8f6f32ca) ![image](https://github.com/user-attachments/assets/dd08402c-c4db-422c-9b60-20cd4f9340a2) - Refactor notifications controller to not require a RuleEngine when polling is requested. --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent ecaf77f commit 5766e4b

File tree

12 files changed

+290
-85
lines changed

12 files changed

+290
-85
lines changed

packages/amazonq/src/extensionNode.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { Auth, AuthUtils, getTelemetryMetadataForConn, isAnySsoConnection } from
1818
import api from './api'
1919
import { activate as activateCWChat } from './app/chat/activation'
2020
import { beta } from 'aws-core-vscode/dev'
21-
import { activate as activateNotifications } from 'aws-core-vscode/notifications'
21+
import { activate as activateNotifications, NotificationsController } from 'aws-core-vscode/notifications'
2222
import { AuthState, AuthUtil } from 'aws-core-vscode/codewhisperer'
2323
import { telemetry, AuthUserState } from 'aws-core-vscode/telemetry'
2424

@@ -67,15 +67,14 @@ async function activateAmazonQNode(context: vscode.ExtensionContext) {
6767

6868
// TODO: Should probably emit for web as well.
6969
// Will the web metric look the same?
70-
const authState = await getAuthState()
7170
telemetry.auth_userState.emit({
7271
passive: true,
7372
result: 'Succeeded',
7473
source: AuthUtils.ExtensionUse.instance.sourceForTelemetry(),
75-
...authState,
74+
...(await getAuthState()),
7675
})
7776

78-
void activateNotifications(context, authState, getAuthState)
77+
void activateNotifications(context, getAuthState)
7978
}
8079

8180
async function getAuthState(): Promise<Omit<AuthUserState, 'source'>> {
@@ -122,13 +121,16 @@ async function setupDevMode(context: vscode.ExtensionContext) {
122121

123122
const devOptions: DevOptions = {
124123
context,
125-
auth: Auth.instance,
124+
auth: () => Auth.instance,
125+
notificationsController: () => NotificationsController.instance,
126126
menuOptions: [
127127
'editStorage',
128+
'resetState',
128129
'showEnvVars',
129130
'deleteSsoConnections',
130131
'expireSsoConnections',
131132
'editAuthConnections',
133+
'notificationsSend',
132134
'forceIdeCrash',
133135
],
134136
}

packages/core/src/dev/activation.ts

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import { getEnvironmentSpecificMemento } from '../shared/utilities/mementos'
2121
import { setContext } from '../shared'
2222
import { telemetry } from '../shared/telemetry'
2323
import { getSessionId } from '../shared/telemetry/util'
24+
import { NotificationsController } from '../notifications/controller'
25+
import { DevNotificationsState } from '../notifications/types'
26+
import { QuickPickItem } from 'vscode'
2427

2528
interface MenuOption {
2629
readonly label: string
@@ -34,21 +37,25 @@ export type DevFunction =
3437
| 'openTerminal'
3538
| 'deleteDevEnv'
3639
| 'editStorage'
40+
| 'resetState'
3741
| 'showEnvVars'
3842
| 'deleteSsoConnections'
3943
| 'expireSsoConnections'
4044
| 'editAuthConnections'
45+
| 'notificationsSend'
4146
| 'forceIdeCrash'
4247

4348
export type DevOptions = {
4449
context: vscode.ExtensionContext
45-
auth: Auth
50+
auth: () => Auth
51+
notificationsController: () => NotificationsController
4652
menuOptions?: DevFunction[]
4753
}
4854

4955
let targetContext: vscode.ExtensionContext
5056
let globalState: vscode.Memento
5157
let targetAuth: Auth
58+
let targetNotificationsController: NotificationsController
5259

5360
/**
5461
* Defines AWS Toolkit developer tools.
@@ -83,6 +90,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
8390
detail: 'Shows all globalState values, or edit a globalState/secret item',
8491
executor: openStorageFromInput,
8592
},
93+
resetState: {
94+
label: 'Reset feature state',
95+
detail: 'Quick reset the state of extension components or features',
96+
executor: resetState,
97+
},
8698
showEnvVars: {
8799
label: 'Show Environment Variables',
88100
description: 'AWS Toolkit',
@@ -104,6 +116,11 @@ const menuOptions: () => Record<DevFunction, MenuOption> = () => {
104116
detail: 'Opens editor to all Auth Connections the extension is using.',
105117
executor: editSsoConnections,
106118
},
119+
notificationsSend: {
120+
label: 'Notifications: Send Notifications',
121+
detail: 'Send JSON notifications for testing.',
122+
executor: editNotifications,
123+
},
107124
forceIdeCrash: {
108125
label: 'Crash: Force IDE ExtHost Crash',
109126
detail: `Will SIGKILL ExtHost, { pid: ${process.pid}, sessionId: '${getSessionId().slice(0, 8)}-...' }, but the IDE itself will not crash.`,
@@ -156,14 +173,19 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
156173
vscode.workspace.registerTextDocumentContentProvider('aws-dev2', new DevDocumentProvider()),
157174
// "AWS (Developer): Open Developer Menu"
158175
vscode.commands.registerCommand('aws.dev.openMenu', async () => {
159-
await vscode.commands.executeCommand('_aws.dev.invokeMenu', { context: ctx, auth: Auth.instance })
176+
await vscode.commands.executeCommand('_aws.dev.invokeMenu', {
177+
context: ctx,
178+
auth: () => Auth.instance,
179+
notificationsController: () => NotificationsController.instance,
180+
})
160181
}),
161182
// Internal command to open dev menu for a specific context and options
162183
vscode.commands.registerCommand('_aws.dev.invokeMenu', (opts: DevOptions) => {
163184
targetContext = opts.context
164185
// eslint-disable-next-line aws-toolkits/no-banned-usages
165186
globalState = targetContext.globalState
166-
targetAuth = opts.auth
187+
targetAuth = opts.auth()
188+
targetNotificationsController = opts.notificationsController()
167189
const options = menuOptions()
168190
void openMenu(
169191
entries(options)
@@ -302,7 +324,7 @@ class ObjectEditor {
302324
vscode.workspace.registerFileSystemProvider(ObjectEditor.scheme, this.fs)
303325
}
304326

305-
public async openStorage(type: 'globalsView' | 'globals' | 'secrets' | 'auth', key: string): Promise<void> {
327+
public async openStorage(type: 'globalsView' | 'globals' | 'secrets' | 'auth', key: string) {
306328
switch (type) {
307329
case 'globalsView':
308330
return showState('globalstate')
@@ -316,17 +338,19 @@ class ObjectEditor {
316338
}
317339
}
318340

319-
private async openState(storage: vscode.Memento | vscode.SecretStorage, key: string): Promise<void> {
341+
private async openState(storage: vscode.Memento | vscode.SecretStorage, key: string) {
320342
const uri = this.uriFromKey(key, storage)
321343
const tab = this.tabs.get(this.fs.uriToKey(uri))
322344

323345
if (tab) {
324346
tab.virtualFile.refresh()
325347
await vscode.window.showTextDocument(tab.editor.document)
348+
return tab.virtualFile
326349
} else {
327350
const newTab = await this.createTab(storage, key)
328351
const newKey = this.fs.uriToKey(newTab.editor.document.uri)
329352
this.tabs.set(newKey, newTab)
353+
return newTab.virtualFile
330354
}
331355
}
332356

@@ -417,6 +441,62 @@ async function openStorageFromInput() {
417441
}
418442
}
419443

444+
type ResettableFeature = {
445+
name: string
446+
executor: () => Promise<void> | void
447+
} & QuickPickItem
448+
449+
/**
450+
* Extend this array with features that may need state resets often for
451+
* testing purposes. It will appear as an entry in the "Reset feature state" menu.
452+
*/
453+
const resettableFeatures: readonly ResettableFeature[] = [
454+
{
455+
name: 'notifications',
456+
label: 'Notifications',
457+
detail: 'Resets memory/global state for the notifications panel (includes dismissed, onReceive).',
458+
executor: resetNotificationsState,
459+
},
460+
] as const
461+
462+
// TODO this is *somewhat* similar to `openStorageFromInput`. If we need another
463+
// one of these prompters, can we make it generic?
464+
async function resetState() {
465+
const wizard = new (class extends Wizard<{ target: string; key: string }> {
466+
constructor() {
467+
super()
468+
469+
this.form.target.bindPrompter(() =>
470+
createQuickPick(
471+
resettableFeatures.map((f) => {
472+
return {
473+
data: f.name,
474+
label: f.label,
475+
detail: f.detail,
476+
}
477+
}),
478+
{
479+
title: 'Select a feature/component to reset',
480+
}
481+
)
482+
)
483+
484+
this.form.key.bindPrompter(({ target }) => {
485+
if (target && resettableFeatures.some((f) => f.name === target)) {
486+
return new SkipPrompter('')
487+
}
488+
throw new Error('invalid feature target')
489+
})
490+
}
491+
})()
492+
493+
const response = await wizard.run()
494+
495+
if (response) {
496+
return resettableFeatures.find((f) => f.name === response.target)?.executor()
497+
}
498+
}
499+
420500
async function editSsoConnections() {
421501
void openStorageCommand.execute('auth', 'auth.profiles')
422502
}
@@ -460,3 +540,41 @@ export const openStorageCommand = Commands.from(ObjectEditor).declareOpenStorage
460540
export async function updateDevMode() {
461541
await setContext('aws.isDevMode', DevSettings.instance.isDevMode())
462542
}
543+
544+
async function resetNotificationsState() {
545+
await targetNotificationsController.reset()
546+
}
547+
548+
async function editNotifications() {
549+
const storageKey = 'aws.notifications.dev'
550+
const current = globalState.get(storageKey) ?? {}
551+
const isValid = (item: any) => {
552+
if (typeof item !== 'object' || !Array.isArray(item.startUp) || !Array.isArray(item.emergency)) {
553+
return false
554+
}
555+
return true
556+
}
557+
if (!isValid(current)) {
558+
// Set a default state if the developer does not have it or it's malformed.
559+
await globalState.update(storageKey, { startUp: [], emergency: [] } as DevNotificationsState)
560+
}
561+
562+
// Monitor for when the global state is updated.
563+
// A notification will be sent based on the contents.
564+
const virtualFile = await openStorageCommand.execute('globals', storageKey)
565+
virtualFile?.onDidChange(async () => {
566+
const val = globalState.get(storageKey) as DevNotificationsState
567+
if (!isValid(val)) {
568+
void vscode.window.showErrorMessage(
569+
'Dev mode: invalid notification object provided. State data must take the form: { "startUp": ToolkitNotification[], "emergency": ToolkitNotification[] }'
570+
)
571+
return
572+
}
573+
574+
// This relies on the controller being built with DevFetcher, as opposed to
575+
// the default RemoteFetcher. DevFetcher will check for notifications in the
576+
// global state, which was just modified.
577+
await targetNotificationsController.pollForStartUp()
578+
await targetNotificationsController.pollForEmergencies()
579+
})
580+
}

packages/core/src/extensionNode.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,15 +238,14 @@ export async function activate(context: vscode.ExtensionContext) {
238238

239239
// TODO: Should probably emit for web as well.
240240
// Will the web metric look the same?
241-
const authState = await getAuthState()
242241
telemetry.auth_userState.emit({
243242
passive: true,
244243
result: 'Succeeded',
245244
source: ExtensionUse.instance.sourceForTelemetry(),
246-
...authState,
245+
...(await getAuthState()),
247246
})
248247

249-
void activateNotifications(context, authState, getAuthState)
248+
void activateNotifications(context, getAuthState)
250249
} catch (error) {
251250
const stacktrace = (error as Error).stack?.split('\n')
252251
// truncate if the stacktrace is unusually long

packages/core/src/notifications/activation.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import { NotificationsController } from './controller'
7+
import { DevSettings } from '../shared/settings'
8+
import { DevFetcher, NotificationsController, RemoteFetcher } from './controller'
89
import { NotificationsNode } from './panelNode'
9-
import { RuleEngine, getRuleContext } from './rules'
10+
import { getRuleContext } from './rules'
1011
import globals from '../shared/extensionGlobals'
1112
import { AuthState } from './types'
1213
import { getLogger } from '../shared/logger/logger'
@@ -24,25 +25,26 @@ const emergencyPollTime = oneMinute * 10
2425
* @param initialState initial auth state
2526
* @param authStateFn fn to get current auth state
2627
*/
27-
export async function activate(
28-
context: vscode.ExtensionContext,
29-
initialState: AuthState,
30-
authStateFn: () => Promise<AuthState>
31-
) {
28+
export async function activate(context: vscode.ExtensionContext, authStateFn: () => Promise<AuthState>) {
3229
try {
3330
const panelNode = NotificationsNode.instance
3431
panelNode.registerView(context)
3532

36-
const controller = new NotificationsController(panelNode)
37-
const engine = new RuleEngine(await getRuleContext(context, initialState))
38-
39-
await controller.pollForStartUp(engine)
40-
await controller.pollForEmergencies(engine)
41-
42-
globals.clock.setInterval(async () => {
43-
const ruleContext = await getRuleContext(context, await authStateFn())
44-
await controller.pollForEmergencies(new RuleEngine(ruleContext))
45-
}, emergencyPollTime)
33+
const controller = new NotificationsController(
34+
panelNode,
35+
async () => await getRuleContext(context, await authStateFn()),
36+
DevSettings.instance.isDevMode() ? new DevFetcher() : new RemoteFetcher()
37+
)
38+
39+
await controller.pollForStartUp()
40+
await controller.pollForEmergencies()
41+
42+
globals.clock.setInterval(
43+
async () => {
44+
await controller.pollForEmergencies()
45+
},
46+
DevSettings.instance.get('notificationsPollInterval', emergencyPollTime)
47+
)
4648

4749
logger.debug('Activated in-IDE notifications polling module')
4850
} catch (err) {

0 commit comments

Comments
 (0)