Skip to content
This repository was archived by the owner on Sep 30, 2024. It is now read-only.

Commit b456c3b

Browse files
authored
Cody: display error on older SG version or when cody is not enabled(#52739)
1 parent 9b948aa commit b456c3b

File tree

14 files changed

+368
-104
lines changed

14 files changed

+368
-104
lines changed

client/cody-shared/src/codebase-context/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class CodebaseContext {
4848
}
4949

5050
public getEmbeddingSearchErrors(): string {
51-
return this.embeddingResultsError
51+
return this.embeddingResultsError.trim()
5252
}
5353

5454
public async getSearchResults(

client/cody-shared/src/sourcegraph-api/graphql/client.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,28 @@ import {
1515
REPOSITORY_EMBEDDING_EXISTS_QUERY,
1616
SEARCH_TYPE_REPO_QUERY,
1717
CURRENT_USER_ID_AND_VERIFIED_EMAIL_QUERY,
18+
CURRENT_SITE_VERSION_QUERY,
19+
CURRENT_SITE_HAS_CODY_ENABLED_QUERY,
20+
CURRENT_SITE_GRAPHQL_FIELDS_QUERY,
1821
} from './queries'
1922

2023
interface APIResponse<T> {
2124
data?: T
2225
errors?: { message: string; path?: string[] }[]
2326
}
2427

28+
interface SiteVersionResponse {
29+
site: { productVersion: string } | null
30+
}
31+
32+
interface SiteGraphqlFieldsResponse {
33+
__type: { fields: { name: string }[] } | null
34+
}
35+
36+
interface SiteHasCodyEnabledResponse {
37+
site: { isCodyEnabled: boolean } | null
38+
}
39+
2540
interface CurrentUserIdResponse {
2641
currentUser: { id: string } | null
2742
}
@@ -110,6 +125,32 @@ export class SourcegraphGraphQLAPIClient {
110125
return new URL(this.config.serverEndpoint).origin === new URL(this.dotcomUrl).origin
111126
}
112127

128+
public async getSiteVersion(): Promise<string | Error> {
129+
return this.fetchSourcegraphAPI<APIResponse<SiteVersionResponse>>(CURRENT_SITE_VERSION_QUERY, {}).then(
130+
response =>
131+
extractDataOrError(response, data =>
132+
// Example values: "5.1.0" or "222587_2023-05-30_5.0-39cbcf1a50f0" for insider builds
133+
data.site?.productVersion ? data.site?.productVersion : new Error('site version not found')
134+
)
135+
)
136+
}
137+
138+
public async getSiteHasIsCodyEnabledField(): Promise<boolean | Error> {
139+
return this.fetchSourcegraphAPI<APIResponse<SiteGraphqlFieldsResponse>>(
140+
CURRENT_SITE_GRAPHQL_FIELDS_QUERY,
141+
{}
142+
).then(response =>
143+
extractDataOrError(response, data => !!data.__type?.fields?.find(field => field.name === 'isCodyEnabled'))
144+
)
145+
}
146+
147+
public async getSiteHasCodyEnabled(): Promise<boolean | Error> {
148+
return this.fetchSourcegraphAPI<APIResponse<SiteHasCodyEnabledResponse>>(
149+
CURRENT_SITE_HAS_CODY_ENABLED_QUERY,
150+
{}
151+
).then(response => extractDataOrError(response, data => data.site?.isCodyEnabled ?? false))
152+
}
153+
113154
public async getCurrentUserId(): Promise<string | Error> {
114155
return this.fetchSourcegraphAPI<APIResponse<CurrentUserIdResponse>>(CURRENT_USER_ID_QUERY, {}).then(response =>
115156
extractDataOrError(response, data =>
@@ -124,7 +165,7 @@ export class SourcegraphGraphQLAPIClient {
124165
{}
125166
).then(response =>
126167
extractDataOrError(response, data =>
127-
data.currentUser ? { ...data.currentUser } : new Error('current user not found')
168+
data.currentUser ? { ...data.currentUser } : new Error('current user not found with verified email')
128169
)
129170
)
130171
}
@@ -150,6 +191,45 @@ export class SourcegraphGraphQLAPIClient {
150191
)
151192
}
152193

194+
/**
195+
* Checks if Cody is enabled on the current Sourcegraph instance.
196+
*
197+
* @returns
198+
* enabled: Whether Cody is enabled.
199+
* version: The Sourcegraph version.
200+
*
201+
* This method first checks the Sourcegraph version using `getSiteVersion()`.
202+
* If the version is before 5.0.0, Cody is disabled.
203+
* If the version is 5.0.0 or newer, it checks for the existence of the `isCodyEnabled` field using `getSiteHasIsCodyEnabledField()`.
204+
* If the field exists, it calls `getSiteHasCodyEnabled()` to check its value.
205+
* If the field does not exist, Cody is assumed to be enabled for versions between 5.0.0 - 5.1.0.
206+
*/
207+
public async isCodyEnabled(): Promise<{ enabled: boolean; version: string }> {
208+
// Check site version.
209+
const siteVersion = await this.getSiteVersion()
210+
if (isError(siteVersion)) {
211+
return { enabled: false, version: 'unknown' }
212+
}
213+
const insiderBuild = siteVersion.length > 12 || siteVersion.includes('dev')
214+
if (insiderBuild) {
215+
return { enabled: true, version: siteVersion }
216+
}
217+
// NOTE: Cody does not work on versions older than 5.0
218+
const versionBeforeCody = siteVersion < '5.0.0'
219+
if (versionBeforeCody) {
220+
return { enabled: false, version: siteVersion }
221+
}
222+
// Beta version is betwewen 5.0.0 - 5.1.0 and does not have isCodyEnabled field
223+
const betaVersion = siteVersion >= '5.0.0' && siteVersion < '5.1.0'
224+
const hasIsCodyEnabledField = await this.getSiteHasIsCodyEnabledField()
225+
// The isCodyEnabled field does not exist before version 5.1.0
226+
if (!betaVersion && !isError(hasIsCodyEnabledField) && hasIsCodyEnabledField) {
227+
const siteHasCodyEnabled = await this.getSiteHasCodyEnabled()
228+
return { enabled: !isError(siteHasCodyEnabled) && siteHasCodyEnabled, version: siteVersion }
229+
}
230+
return { enabled: insiderBuild || betaVersion, version: siteVersion }
231+
}
232+
153233
public async logEvent(event: {
154234
event: string
155235
userCookieID: string

client/cody-shared/src/sourcegraph-api/graphql/queries.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@ query CurrentUser {
55
}
66
}`
77

8+
export const CURRENT_SITE_VERSION_QUERY = `
9+
query SiteProductVersion {
10+
site {
11+
productVersion
12+
}
13+
}`
14+
15+
export const CURRENT_SITE_HAS_CODY_ENABLED_QUERY = `
16+
query SiteHasCodyEnabled {
17+
site {
18+
isCodyEnabled
19+
}
20+
}`
21+
22+
export const CURRENT_SITE_GRAPHQL_FIELDS_QUERY = `
23+
query SiteGraphQLFields {
24+
__type(name: "Site") {
25+
fields {
26+
name
27+
}
28+
}
29+
}`
30+
831
export const CURRENT_USER_ID_AND_VERIFIED_EMAIL_QUERY = `
932
query CurrentUser {
1033
currentUser {

client/cody/src/chat/ChatViewProvider.ts

Lines changed: 15 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import { Guardrails, annotateAttribution } from '@sourcegraph/cody-shared/src/gu
1515
import { highlightTokens } from '@sourcegraph/cody-shared/src/hallucinations-detector'
1616
import { IntentDetector } from '@sourcegraph/cody-shared/src/intent-detector'
1717
import { Message } from '@sourcegraph/cody-shared/src/sourcegraph-api'
18-
import { SourcegraphGraphQLAPIClient } from '@sourcegraph/cody-shared/src/sourcegraph-api/graphql'
19-
import { isError } from '@sourcegraph/cody-shared/src/utils'
2018

2119
import { View } from '../../webviews/NavBar'
2220
import { getFullConfig, updateConfiguration } from '../configuration'
@@ -36,44 +34,11 @@ import {
3634
DOTCOM_URL,
3735
ExtensionMessage,
3836
WebviewMessage,
39-
isLocalApp,
37+
defaultAuthStatus,
4038
isLoggedIn,
4139
} from './protocol'
4240
import { getRecipe } from './recipes'
43-
import { getCodebaseContext } from './utils'
44-
45-
export async function getAuthStatus(
46-
config: Pick<ConfigurationWithAccessToken, 'serverEndpoint' | 'accessToken' | 'customHeaders'>
47-
): Promise<AuthStatus> {
48-
if (!config.accessToken) {
49-
return {
50-
showInvalidAccessTokenError: false,
51-
authenticated: false,
52-
hasVerifiedEmail: false,
53-
requiresVerifiedEmail: false,
54-
}
55-
}
56-
57-
const client = new SourcegraphGraphQLAPIClient(config)
58-
if (client.isDotCom() || isLocalApp(config.serverEndpoint)) {
59-
const data = await client.getCurrentUserIdAndVerifiedEmail()
60-
return {
61-
showInvalidAccessTokenError: isError(data),
62-
authenticated: !isError(data),
63-
hasVerifiedEmail: !isError(data) && data?.hasVerifiedEmail,
64-
// on sourcegraph.com this is always true
65-
requiresVerifiedEmail: true,
66-
}
67-
}
68-
69-
const currentUserID = await client.getCurrentUserId()
70-
return {
71-
showInvalidAccessTokenError: isError(currentUserID),
72-
authenticated: !isError(currentUserID),
73-
hasVerifiedEmail: false,
74-
requiresVerifiedEmail: false,
75-
}
76-
}
41+
import { getAuthStatus, getCodebaseContext } from './utils'
7742

7843
export type Config = Pick<
7944
ConfigurationWithAccessToken,
@@ -231,14 +196,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
231196
accessToken: message.accessToken,
232197
customHeaders: this.config.customHeaders,
233198
})
234-
// activate when user has valid login
235-
await vscode.commands.executeCommand('setContext', 'cody.activated', isLoggedIn(authStatus))
236-
if (isLoggedIn(authStatus)) {
237-
await updateConfiguration('serverEndpoint', message.serverEndpoint)
238-
await this.secretStorage.store(CODY_ACCESS_TOKEN_SECRET, message.accessToken)
239-
this.sendEvent('auth', 'login')
240-
}
241-
void this.webview?.postMessage({ type: 'login', authStatus })
199+
200+
await updateConfiguration('serverEndpoint', message.serverEndpoint)
201+
await this.secretStorage.store(CODY_ACCESS_TOKEN_SECRET, message.accessToken)
202+
await this.sendLogin(authStatus)
242203
break
243204
}
244205
case 'insert':
@@ -344,21 +305,15 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
344305
this.transcript.addErrorAsAssistantResponse(err)
345306
// Log users out on unauth error
346307
if (statusCode && statusCode >= 400 && statusCode <= 410) {
308+
const authStatus = { ...defaultAuthStatus }
347309
if (statusCode === 403) {
348-
void this.sendLogin({
349-
showInvalidAccessTokenError: false,
350-
authenticated: true,
351-
hasVerifiedEmail: false,
352-
requiresVerifiedEmail: true,
353-
})
310+
authStatus.authenticated = true
311+
authStatus.requiresVerifiedEmail = true
354312
} else {
355-
void this.sendLogin({
356-
showInvalidAccessTokenError: true,
357-
authenticated: false,
358-
hasVerifiedEmail: false,
359-
requiresVerifiedEmail: false,
360-
})
313+
authStatus.showInvalidAccessTokenError = true
361314
}
315+
debug('ChatViewProvider:onError:unauth', err, { verbose: { authStatus } })
316+
void this.sendLogin(authStatus)
362317
void this.clearAndRestartSession()
363318
}
364319
this.onCompletionEnd()
@@ -604,15 +559,10 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
604559
* Save Login state to webview
605560
*/
606561
public async sendLogin(authStatus: AuthStatus): Promise<void> {
607-
this.sendEvent('token', 'Set')
562+
// activate extension when user has valid login
608563
await vscode.commands.executeCommand('setContext', 'cody.activated', isLoggedIn(authStatus))
609-
if (isLoggedIn(authStatus)) {
610-
this.sendEvent('auth', 'login')
611-
}
612-
void this.webview?.postMessage({
613-
type: 'login',
614-
authStatus,
615-
})
564+
await this.webview?.postMessage({ type: 'login', authStatus })
565+
this.sendEvent('auth', 'login')
616566
}
617567

618568
/**

client/cody/src/chat/protocol.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,7 @@ import { View } from '../../webviews/NavBar'
99
* A message sent from the webview to the extension host.
1010
*/
1111
export type WebviewMessage =
12-
| {
13-
command: 'initialized'
14-
}
12+
| { command: 'initialized' }
1513
| { command: 'event'; event: string; value: string }
1614
| { command: 'submit'; text: string; submitType: 'user' | 'suggestion' }
1715
| { command: 'executeRecipe'; recipe: RecipeID }
@@ -57,9 +55,32 @@ export interface AuthStatus {
5755
authenticated: boolean
5856
hasVerifiedEmail: boolean
5957
requiresVerifiedEmail: boolean
58+
siteHasCodyEnabled: boolean
59+
siteVersion: string
60+
}
61+
62+
export const defaultAuthStatus = {
63+
showInvalidAccessTokenError: false,
64+
authenticated: false,
65+
hasVerifiedEmail: false,
66+
requiresVerifiedEmail: false,
67+
siteHasCodyEnabled: false,
68+
siteVersion: '',
69+
}
70+
71+
export const unauthenticatedStatus = {
72+
showInvalidAccessTokenError: true,
73+
authenticated: false,
74+
hasVerifiedEmail: false,
75+
requiresVerifiedEmail: false,
76+
siteHasCodyEnabled: false,
77+
siteVersion: '',
6078
}
6179

6280
export function isLoggedIn(authStatus: AuthStatus): boolean {
81+
if (!authStatus.siteHasCodyEnabled) {
82+
return false
83+
}
6384
return authStatus.authenticated && (authStatus.requiresVerifiedEmail ? authStatus.hasVerifiedEmail : true)
6485
}
6586

client/cody/src/chat/utils.test.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { convertGitCloneURLToCodebaseName } from './utils'
1+
import { defaultAuthStatus, unauthenticatedStatus } from './protocol'
2+
import { convertGitCloneURLToCodebaseName, newAuthStatus } from './utils'
23

34
describe('convertGitCloneURLToCodebaseName', () => {
45
test('converts GitHub SSH URL', () => {
@@ -65,3 +66,69 @@ describe('convertGitCloneURLToCodebaseName', () => {
6566
expect(convertGitCloneURLToCodebaseName('invalid')).toEqual(null)
6667
})
6768
})
69+
70+
describe('validateAuthStatus', () => {
71+
// NOTE: Site version is for frontend use and doesn't play a role in validating auth status
72+
const siteVersion = ''
73+
const isDotComOrApp = true
74+
const verifiedEmail = true
75+
const codyEnabled = true
76+
const validUser = true
77+
// DOTCOM AND APP USERS
78+
test('returns auth state for invalid user on dotcom or app instance', () => {
79+
const expected = { ...unauthenticatedStatus }
80+
expect(newAuthStatus(isDotComOrApp, !validUser, !verifiedEmail, codyEnabled, siteVersion)).toEqual(expected)
81+
})
82+
83+
test('returns auth status for valid user with varified email on dotcom or app instance', () => {
84+
const expected = {
85+
...defaultAuthStatus,
86+
authenticated: true,
87+
hasVerifiedEmail: true,
88+
showInvalidAccessTokenError: false,
89+
requiresVerifiedEmail: true,
90+
siteHasCodyEnabled: true,
91+
}
92+
expect(newAuthStatus(isDotComOrApp, validUser, verifiedEmail, codyEnabled, siteVersion)).toEqual(expected)
93+
})
94+
95+
test('returns auth status for valid user without verified email on dotcom or app instance', () => {
96+
const expected = {
97+
...defaultAuthStatus,
98+
authenticated: true,
99+
hasVerifiedEmail: false,
100+
requiresVerifiedEmail: true,
101+
siteHasCodyEnabled: true,
102+
}
103+
expect(newAuthStatus(isDotComOrApp, validUser, !verifiedEmail, codyEnabled, siteVersion)).toEqual(expected)
104+
})
105+
106+
// ENTERPRISE
107+
test('returns auth status for valid user on enterprise instance with Cody enabled', () => {
108+
const expected = {
109+
...defaultAuthStatus,
110+
authenticated: true,
111+
siteHasCodyEnabled: true,
112+
}
113+
expect(newAuthStatus(!isDotComOrApp, validUser, verifiedEmail, codyEnabled, siteVersion)).toEqual(expected)
114+
})
115+
116+
test('returns auth status for invalid user on enterprise instance with Cody enabled', () => {
117+
const expected = { ...unauthenticatedStatus }
118+
expect(newAuthStatus(!isDotComOrApp, !validUser, verifiedEmail, codyEnabled, siteVersion)).toEqual(expected)
119+
})
120+
121+
test('returns auth status for valid user on enterprise instance with Cody disabled', () => {
122+
const expected = {
123+
...defaultAuthStatus,
124+
authenticated: true,
125+
siteHasCodyEnabled: false,
126+
}
127+
expect(newAuthStatus(!isDotComOrApp, validUser, !verifiedEmail, !codyEnabled, siteVersion)).toEqual(expected)
128+
})
129+
130+
test('returns auth status for invalid user on enterprise instance with Cody disabled', () => {
131+
const expected = { ...unauthenticatedStatus }
132+
expect(newAuthStatus(!isDotComOrApp, !validUser, verifiedEmail, !codyEnabled, siteVersion)).toEqual(expected)
133+
})
134+
})

0 commit comments

Comments
 (0)