Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: race condition in saving history before switching accounts #6487

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
7 changes: 4 additions & 3 deletions vscode/src/chat/chat-view/ChatController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1912,10 +1912,11 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv
})
}

public async clearAndRestartSession(chatMessages?: ChatMessage[]): Promise<void> {
public async clearAndRestartSession(chatMessages?: ChatMessage[], shouldSave = true): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: Should we separate the secession step from this method if this has been causing issues?

this.cancelSubmitOrEditOperation()
void this.saveSession()

if (shouldSave) {
void this.saveSession()
}
this.chatBuilder = new ChatBuilder(this.chatBuilder.selectedModel, undefined, chatMessages)
this.postViewTranscript()
}
Expand Down
24 changes: 22 additions & 2 deletions vscode/src/chat/chat-view/ChatsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,31 @@ export class ChatsController implements vscode.Disposable {
const hasSwitchedAccount =
this.currentAuthAccount &&
this.currentAuthAccount.endpoint !== authStatus.endpoint
if (hasLoggedOut || hasSwitchedAccount) {
if (hasLoggedOut) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (hasLoggedOut) {
if (hasLoggedOut) {
this.disposeAllChats()
return

Can we return early since the code below is doing the same thing without saving the session?

this.disposeAllChats()
}
if (hasSwitchedAccount) {
// Update account reference before disposing
this.currentAuthAccount = authStatus.authenticated
? { ...authStatus }
: undefined

// Dispose chats without saving to storage
this.activeEditor = undefined
const oldEditors = this.editors
this.editors = []
for (const editor of oldEditors) {
if (editor.webviewPanelOrView) {
disposeWebviewViewOrPanel(editor.webviewPanelOrView)
}
editor.dispose()
}
Comment on lines +94 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.activeEditor = undefined
const oldEditors = this.editors
this.editors = []
for (const editor of oldEditors) {
if (editor.webviewPanelOrView) {
disposeWebviewViewOrPanel(editor.webviewPanelOrView)
}
editor.dispose()
}
this.disposeAllChats()

This looks like it does the same thing as disposeAllChats method? On account switch, the new ChatBuilder(undefined) will be recreated, so it should be safe to saveSession? We should also look into not invoking saveSession when the chat is empty (unrelated to this PR) 👀


this.currentAuthAccount = authStatus.authenticated ? { ...authStatus } : undefined
// Clear panel without saving
if (this.panel) {
this.panel.clearAndRestartSession([], false)
}
}
})
)
)
Expand Down
207 changes: 136 additions & 71 deletions vscode/test/e2e/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { SERVER_URL, VALID_TOKEN, VALID_TOKEN_PERSON2 } from '../fixtures/mock-server'
import { expectSignInPage, sidebarSignin } from './common'
import { expectSignInPage, getChatInputs, getChatSidebarPanel, sidebarSignin } from './common'
import {
type ClientConfigSingletonRefetchIntervalOverride,
type DotcomUrlOverride,
Expand Down Expand Up @@ -133,74 +133,139 @@ test.extend<ExpectedV2Events>({
'cody.experiment.interactiveTutorial:enrolled',
'cody.signInNotification:shown',
],
})('switch account via account dropdown menu in webview', async ({ page, sidebar }) => {
test.slow()
await expect(sidebar!.getByText('Sign in to Sourcegraph')).toBeVisible()
await sidebar!.getByRole('button', { name: 'Sourcegraph logo Continue' }).click()
await sidebar!.getByText('Sourcegraph Instance URL').click()
await sidebar!.getByPlaceholder('Example: https://instance.').click()
await sidebar!.getByPlaceholder('Example: https://instance.').fill(SERVER_URL)

await sidebar!.getByText('Access Token (Optional)').click()

await sidebar!.getByPlaceholder('Access token...').click()
await sidebar!.getByPlaceholder('Access token...').fill(VALID_TOKEN)
await sidebar!.getByPlaceholder('Access token...').press('Enter')

await expect(sidebar!.getByText('Invalid access token.')).not.toBeVisible()
await expect(sidebar!.getByText('Sign in to Sourcegraph')).not.toBeVisible()
await expect(sidebar!.getByLabel('Chat message')).toBeVisible()

// Open the User Dropdown menu
await expect(sidebar!.getByRole('button', { name: 'New Chat' })).toBeVisible()
await sidebar!.getByLabel('Account Menu Button').click({ delay: 2000 })

const codeWebview = sidebar!.getByLabel('cody-webview')

// Should have logged into the default account "Person"
await expect(codeWebview.getByText('Enterprise')).toBeVisible()
await expect(codeWebview.getByText('Person', { exact: true })).toBeVisible()
await expect(codeWebview.getByText('[email protected]')).toBeVisible()

// Make sure the options are visible
await expect(sidebar!.getByRole('option', { name: 'Extension Settings' })).toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Switch Account' })).toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Sign Out' })).toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Help' })).toBeVisible()

await sidebar!.getByRole('option', { name: 'Switch Account' }).click()
await expect(sidebar!.getByText('Active')).toBeVisible()
await expect(sidebar!.getByText(SERVER_URL)).toBeVisible()

await sidebar!.getByText('Add another account').click()
await expect(sidebar!.getByText('Account Details')).toBeVisible()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).toBeVisible()
await sidebar!.getByLabel('Instance URL').click()
await sidebar!.getByLabel('Instance URL').fill(SERVER_URL)
await sidebar!.getByText('Access Token (Optional)').click()
await sidebar!.getByPlaceholder('sgp_xxx_xxx').click()
await sidebar!.getByPlaceholder('sgp_xxx_xxx').fill(VALID_TOKEN_PERSON2)
await sidebar!.getByRole('button', { name: 'Add and Switch' }).click()

// Makes sure the dropdown menu is closed.
await expect(sidebar!.getByText('Account Details')).not.toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Help' })).not.toBeVisible()

// Should have switched to the new account "Person2". It'd take a few seconds for the webview to update.
await sidebar!.getByText('Person 2', { exact: true }).hover()

// Open dropdown menu
await sidebar!.getByLabel('Account Menu Button').click({ delay: 2000 })

await expect(sidebar!.getByRole('option', { name: 'Help' })).toBeVisible()
await expect(codeWebview.getByText('Person', { exact: true })).not.toBeVisible()
await expect(codeWebview.getByText('[email protected]')).toBeVisible()

// Clicking on Cancel should move back to the Account Switcher view.
await sidebar!.getByRole('option', { name: 'Switch Account' }).click()
await sidebar!.getByText('Add another account').click()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).toBeVisible()
await sidebar!.getByRole('option', { name: 'Cancel' }).click()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).not.toBeVisible()
await expect(sidebar!.getByText(SERVER_URL)).toBeVisible()
})

test.extend<ExpectedV2Events>({
expectedV2Events: [
'cody.extension:installed',
'cody.auth.login:clicked',
'cody.auth.login:firstEver',
'cody.auth.login.token:clicked',
'cody.auth:connected',
'cody.userMenu:open',
'cody.auth:disconnected',
'cody.interactiveTutorial:attemptingStart',
'cody.experiment.interactiveTutorial:enrolled',
'cody.signInNotification:shown',
],
})('switching the account does not show chat from previous account', async ({ page, sidebar }) => {
test.slow()
await expect(sidebar!.getByText('Sign in to Sourcegraph')).toBeVisible()
await sidebar!.getByRole('button', { name: 'Sourcegraph logo Continue' }).click()
await sidebar!.getByText('Sourcegraph Instance URL').click()
await sidebar!.getByPlaceholder('Example: https://instance.').click()
await sidebar!.getByPlaceholder('Example: https://instance.').fill(SERVER_URL)

await sidebar!.getByText('Access Token (Optional)').click()

await sidebar!.getByPlaceholder('Access token...').click()
await sidebar!.getByPlaceholder('Access token...').fill(VALID_TOKEN)
await sidebar!.getByPlaceholder('Access token...').press('Enter')

await expect(sidebar!.getByText('Invalid access token.')).not.toBeVisible()
await expect(sidebar!.getByText('Sign in to Sourcegraph')).not.toBeVisible()
await expect(sidebar!.getByLabel('Chat message')).toBeVisible()

const sidebarChat = getChatSidebarPanel(page)
const chatInput = getChatInputs(sidebarChat).first()
await chatInput.fill('Hello world')
await chatInput.press('Enter')
await sidebar!.getByTestId('tab-history').click()
await sidebar!.getByRole('button', { name: 'Hello world' }).first().click()

// Open the User Dropdown menu
await expect(sidebar!.getByRole('button', { name: 'New Chat' })).toBeVisible()
await sidebar!.getByLabel('Account Menu Button').click({ delay: 2000 })
await sidebar!.getByRole('option', { name: 'Switch Account' }).click()
await expect(sidebar!.getByText('Active')).toBeVisible()
await expect(sidebar!.getByText(SERVER_URL)).toBeVisible()

await sidebar!.getByText('Add another account').click()
await expect(sidebar!.getByText('Account Details')).toBeVisible()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).toBeVisible()
await sidebar!.getByLabel('Instance URL').click()
await sidebar!.getByLabel('Instance URL').fill(SERVER_URL)
await sidebar!.getByText('Access Token (Optional)').click()
await sidebar!.getByPlaceholder('sgp_xxx_xxx').click()
await sidebar!.getByPlaceholder('sgp_xxx_xxx').fill(VALID_TOKEN_PERSON2)
await sidebar!.getByRole('button', { name: 'Add and Switch' }).click()

// Makes sure the dropdown menu is closed.
await expect(sidebar!.getByText('Account Details')).not.toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Help' })).not.toBeVisible()

await chatInput.fill('test message')
await chatInput.press('Enter')
await sidebar!.getByTestId('tab-history').click()
expect(await sidebar!.getByRole('button', { name: 'Hello world' }).count()).toBe(0)
})
.skip('switch account via account dropwdown menu in webview', async ({ page, sidebar }) => {
await expect(sidebar!.getByText('Sign in to Sourcegraph')).toBeVisible()
await sidebar!.getByRole('button', { name: 'Sourcegraph logo Continue' }).click()
await sidebar!.getByText('Sourcegraph Instance URL').click()
await sidebar!.getByPlaceholder('Example: https://instance.').click()
await sidebar!.getByPlaceholder('Example: https://instance.').fill(SERVER_URL)

await sidebar!.getByText('Access Token (Optional)').click()

await sidebar!.getByPlaceholder('Access token...').click()
await sidebar!.getByPlaceholder('Access token...').fill(VALID_TOKEN)
await sidebar!.getByPlaceholder('Access token...').press('Enter')

await expect(sidebar!.getByText('Invalid access token.')).not.toBeVisible()
await expect(sidebar!.getByText('Sign in to Sourcegraph')).not.toBeVisible()
await expect(sidebar!.getByLabel('Chat message')).toBeVisible()

// Open the User Dropdown menu
await expect(sidebar!.getByRole('button', { name: 'New Chat' })).toBeVisible()
await sidebar!.getByLabel('Account Menu Button').click({ delay: 2000 })

const codeWebview = sidebar!.getByLabel('cody-webview')

// Should have logged into the default account "Person"
await expect(codeWebview.getByText('Enterprise')).toBeVisible()
await expect(codeWebview.getByText('Person', { exact: true })).toBeVisible()
await expect(codeWebview.getByText('[email protected]')).toBeVisible()

// Make sure the options are visible
await expect(sidebar!.getByRole('option', { name: 'Extension Settings' })).toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Switch Account' })).toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Sign Out' })).toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Help' })).toBeVisible()

await sidebar!.getByRole('option', { name: 'Switch Account' }).click()
await expect(sidebar!.getByText('Active')).toBeVisible()
await expect(sidebar!.getByText(SERVER_URL)).toBeVisible()

await sidebar!.getByText('Add another account').click()
await expect(sidebar!.getByText('Account Details')).toBeVisible()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).toBeVisible()
await sidebar!.getByLabel('Instance URL').click()
await sidebar!.getByLabel('Instance URL').fill(SERVER_URL)
await sidebar!.getByText('Access Token (Optional)').click()
await sidebar!.getByPlaceholder('sgp_xxx_xxx').click()
await sidebar!.getByPlaceholder('sgp_xxx_xxx').fill(VALID_TOKEN_PERSON2)
await sidebar!.getByRole('button', { name: 'Add and Switch' }).click()

// Makes sure the dropdown menu is closed.
await expect(sidebar!.getByText('Account Details')).not.toBeVisible()
await expect(sidebar!.getByRole('option', { name: 'Help' })).not.toBeVisible()

// Should have switched to the new account "Person2". It'd take a few seconds for the webview to update.
await sidebar!.getByText('Person 2', { exact: true }).hover()

// Open dropdown menu
await sidebar!.getByLabel('Account Menu Button').click({ delay: 2000 })

await expect(sidebar!.getByRole('option', { name: 'Help' })).toBeVisible()
await expect(codeWebview.getByText('Person', { exact: true })).not.toBeVisible()
await expect(codeWebview.getByText('[email protected]')).toBeVisible()

// Clicking on Cancel should move back to the Account Switcher view.
await sidebar!.getByRole('option', { name: 'Switch Account' }).click()
await sidebar!.getByText('Add another account').click()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).toBeVisible()
await sidebar!.getByRole('option', { name: 'Cancel' }).click()
await expect(sidebar!.getByRole('button', { name: 'Add and Switch' })).not.toBeVisible()
await expect(sidebar!.getByText(SERVER_URL)).toBeVisible()
})
Loading