Skip to content

Commit 20d325d

Browse files
authored
Merge pull request #6047 from continuedev/pe/onboarding-card-refactor
feat: onboarding card upgrade tab
2 parents aa2eb30 + ecdb4f5 commit 20d325d

31 files changed

+1886
-842
lines changed
Lines changed: 344 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
1+
import { jest } from "@jest/globals";
2+
import { BrowserSerializedContinueConfig } from "..";
3+
4+
// Create the mock before importing anything else
5+
const mockDecodeSecretLocation = jest.fn();
6+
7+
// Mock the module
8+
jest.unstable_mockModule("@continuedev/config-yaml", () => ({
9+
SecretType: {
10+
User: "user",
11+
Organization: "organization",
12+
FreeTrial: "free_trial",
13+
},
14+
decodeSecretLocation: mockDecodeSecretLocation,
15+
}));
16+
17+
// Import after mocking
18+
const { usesFreeTrialApiKey } = await import("./usesFreeTrialApiKey");
19+
const { SecretType } = await import("@continuedev/config-yaml");
20+
21+
beforeEach(() => {
22+
mockDecodeSecretLocation.mockReset();
23+
});
24+
25+
test("usesFreeTrialApiKey should return false when config is null", () => {
26+
const result = usesFreeTrialApiKey(null);
27+
expect(result).toBe(false);
28+
});
29+
30+
test("usesFreeTrialApiKey should return false when config is undefined", () => {
31+
const result = usesFreeTrialApiKey(undefined as any);
32+
expect(result).toBe(false);
33+
});
34+
35+
test("usesFreeTrialApiKey should return false when no models have apiKeyLocation", () => {
36+
const config: BrowserSerializedContinueConfig = {
37+
modelsByRole: {
38+
chat: [
39+
{
40+
title: "Model 1",
41+
provider: "test",
42+
model: "test-model",
43+
underlyingProviderName: "test",
44+
},
45+
{
46+
title: "Model 2",
47+
provider: "test",
48+
model: "test-model-2",
49+
underlyingProviderName: "test",
50+
},
51+
],
52+
edit: [],
53+
apply: [],
54+
summarize: [],
55+
autocomplete: [],
56+
rerank: [],
57+
embed: [],
58+
},
59+
selectedModelByRole: {
60+
chat: null,
61+
edit: null,
62+
apply: null,
63+
summarize: null,
64+
autocomplete: null,
65+
rerank: null,
66+
embed: null,
67+
},
68+
contextProviders: [],
69+
slashCommands: [],
70+
tools: [],
71+
mcpServerStatuses: [],
72+
usePlatform: false,
73+
rules: [],
74+
};
75+
76+
const result = usesFreeTrialApiKey(config);
77+
expect(result).toBe(false);
78+
});
79+
80+
test("usesFreeTrialApiKey should return false when models have apiKeyLocation but none are free trial", () => {
81+
const config: BrowserSerializedContinueConfig = {
82+
modelsByRole: {
83+
chat: [
84+
{
85+
title: "Model 1",
86+
provider: "test",
87+
model: "test-model",
88+
apiKeyLocation: "user:testuser/api-key",
89+
underlyingProviderName: "test",
90+
},
91+
],
92+
edit: [
93+
{
94+
title: "Model 2",
95+
provider: "test",
96+
model: "test-model-2",
97+
apiKeyLocation: "organization:testorg/api-key",
98+
underlyingProviderName: "test",
99+
},
100+
],
101+
apply: [],
102+
summarize: [],
103+
autocomplete: [],
104+
rerank: [],
105+
embed: [],
106+
},
107+
selectedModelByRole: {
108+
chat: null,
109+
edit: null,
110+
apply: null,
111+
summarize: null,
112+
autocomplete: null,
113+
rerank: null,
114+
embed: null,
115+
},
116+
contextProviders: [],
117+
slashCommands: [],
118+
tools: [],
119+
mcpServerStatuses: [],
120+
usePlatform: false,
121+
rules: [],
122+
};
123+
124+
mockDecodeSecretLocation
125+
.mockReturnValueOnce({
126+
secretType: SecretType.User,
127+
userSlug: "testuser",
128+
secretName: "api-key",
129+
})
130+
.mockReturnValueOnce({
131+
secretType: SecretType.Organization,
132+
orgSlug: "testorg",
133+
secretName: "api-key",
134+
});
135+
136+
const result = usesFreeTrialApiKey(config);
137+
expect(result).toBe(false);
138+
expect(mockDecodeSecretLocation).toHaveBeenCalledTimes(2);
139+
});
140+
141+
test("usesFreeTrialApiKey should return true when at least one model uses free trial API key", () => {
142+
const config: BrowserSerializedContinueConfig = {
143+
modelsByRole: {
144+
chat: [
145+
{
146+
title: "Model 1",
147+
provider: "test",
148+
model: "test-model",
149+
apiKeyLocation: "user:testuser/api-key",
150+
underlyingProviderName: "test",
151+
},
152+
{
153+
title: "Free Trial Model",
154+
provider: "test",
155+
model: "free-trial-model",
156+
apiKeyLocation: "free_trial:owner/package/api-key",
157+
underlyingProviderName: "test",
158+
},
159+
],
160+
edit: [],
161+
apply: [],
162+
summarize: [],
163+
autocomplete: [],
164+
rerank: [],
165+
embed: [],
166+
},
167+
selectedModelByRole: {
168+
chat: null,
169+
edit: null,
170+
apply: null,
171+
summarize: null,
172+
autocomplete: null,
173+
rerank: null,
174+
embed: null,
175+
},
176+
contextProviders: [],
177+
slashCommands: [],
178+
tools: [],
179+
mcpServerStatuses: [],
180+
usePlatform: false,
181+
rules: [],
182+
};
183+
184+
mockDecodeSecretLocation
185+
.mockReturnValueOnce({
186+
secretType: SecretType.User,
187+
userSlug: "testuser",
188+
secretName: "api-key",
189+
})
190+
.mockReturnValueOnce({
191+
secretType: SecretType.FreeTrial,
192+
blockSlug: { ownerSlug: "owner", packageSlug: "package" },
193+
secretName: "api-key",
194+
});
195+
196+
const result = usesFreeTrialApiKey(config);
197+
expect(result).toBe(true);
198+
expect(mockDecodeSecretLocation).toHaveBeenCalledTimes(2);
199+
});
200+
201+
test("usesFreeTrialApiKey should return true when free trial model is in a different role", () => {
202+
const config: BrowserSerializedContinueConfig = {
203+
modelsByRole: {
204+
chat: [
205+
{
206+
title: "Model 1",
207+
provider: "test",
208+
model: "test-model",
209+
apiKeyLocation: "user:testuser/api-key",
210+
underlyingProviderName: "test",
211+
},
212+
],
213+
edit: [
214+
{
215+
title: "Free Trial Edit Model",
216+
provider: "test",
217+
model: "free-trial-edit-model",
218+
apiKeyLocation: "free_trial:owner/package/api-key",
219+
underlyingProviderName: "test",
220+
},
221+
],
222+
apply: [],
223+
summarize: [],
224+
autocomplete: [],
225+
rerank: [],
226+
embed: [],
227+
},
228+
selectedModelByRole: {
229+
chat: null,
230+
edit: null,
231+
apply: null,
232+
summarize: null,
233+
autocomplete: null,
234+
rerank: null,
235+
embed: null,
236+
},
237+
contextProviders: [],
238+
slashCommands: [],
239+
tools: [],
240+
mcpServerStatuses: [],
241+
usePlatform: false,
242+
rules: [],
243+
};
244+
245+
mockDecodeSecretLocation
246+
.mockReturnValueOnce({
247+
secretType: SecretType.User,
248+
userSlug: "testuser",
249+
secretName: "api-key",
250+
})
251+
.mockReturnValueOnce({
252+
secretType: SecretType.FreeTrial,
253+
blockSlug: { ownerSlug: "owner", packageSlug: "package" },
254+
secretName: "api-key",
255+
});
256+
257+
const result = usesFreeTrialApiKey(config);
258+
expect(result).toBe(true);
259+
});
260+
261+
test("usesFreeTrialApiKey should return false and log error when decodeSecretLocation throws", () => {
262+
const consoleSpy = jest.spyOn(console, "error").mockImplementation(() => {});
263+
264+
const config: BrowserSerializedContinueConfig = {
265+
modelsByRole: {
266+
chat: [
267+
{
268+
title: "Model 1",
269+
provider: "test",
270+
model: "test-model",
271+
apiKeyLocation: "invalid-secret-location",
272+
underlyingProviderName: "test",
273+
},
274+
],
275+
edit: [],
276+
apply: [],
277+
summarize: [],
278+
autocomplete: [],
279+
rerank: [],
280+
embed: [],
281+
},
282+
selectedModelByRole: {
283+
chat: null,
284+
edit: null,
285+
apply: null,
286+
summarize: null,
287+
autocomplete: null,
288+
rerank: null,
289+
embed: null,
290+
},
291+
contextProviders: [],
292+
slashCommands: [],
293+
tools: [],
294+
mcpServerStatuses: [],
295+
usePlatform: false,
296+
rules: [],
297+
};
298+
299+
mockDecodeSecretLocation.mockImplementation(() => {
300+
throw new Error("Invalid secret location format");
301+
});
302+
303+
const result = usesFreeTrialApiKey(config);
304+
expect(result).toBe(false);
305+
expect(consoleSpy).toHaveBeenCalledWith(
306+
"Error checking for free trial API key:",
307+
expect.any(Error),
308+
);
309+
310+
consoleSpy.mockRestore();
311+
});
312+
313+
test("usesFreeTrialApiKey should handle empty modelsByRole object", () => {
314+
const config: BrowserSerializedContinueConfig = {
315+
modelsByRole: {
316+
chat: [],
317+
edit: [],
318+
apply: [],
319+
summarize: [],
320+
autocomplete: [],
321+
rerank: [],
322+
embed: [],
323+
},
324+
selectedModelByRole: {
325+
chat: null,
326+
edit: null,
327+
apply: null,
328+
summarize: null,
329+
autocomplete: null,
330+
rerank: null,
331+
embed: null,
332+
},
333+
contextProviders: [],
334+
slashCommands: [],
335+
tools: [],
336+
mcpServerStatuses: [],
337+
usePlatform: false,
338+
rules: [],
339+
};
340+
341+
const result = usesFreeTrialApiKey(config);
342+
expect(result).toBe(false);
343+
expect(mockDecodeSecretLocation).not.toHaveBeenCalled();
344+
});

gui/src/util/freeTrialHelpers.ts renamed to core/config/usesFreeTrialApiKey.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { decodeSecretLocation, SecretType } from "@continuedev/config-yaml";
2-
import { BrowserSerializedContinueConfig } from "core";
2+
import { BrowserSerializedContinueConfig } from "..";
3+
34
/**
45
* Helper function to determine if the config uses a free trial API key
56
* @param config The serialized config object

core/control-plane/client.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,38 @@ export class ControlPlaneClient {
182182
return null;
183183
}
184184
}
185+
186+
/**
187+
* JetBrains does not support deep links, so we only check for `vsCodeUriScheme`
188+
* @param vsCodeUriScheme
189+
* @returns
190+
*/
191+
public async getModelsAddOnCheckoutUrl(
192+
vsCodeUriScheme?: string,
193+
): Promise<{ url: string } | null> {
194+
if (!(await this.isSignedIn())) {
195+
return null;
196+
}
197+
198+
try {
199+
const params = new URLSearchParams({
200+
// LocalProfileLoader ID
201+
profile_id: "local",
202+
});
203+
204+
if (vsCodeUriScheme) {
205+
params.set("vscode_uri_scheme", vsCodeUriScheme);
206+
}
207+
208+
const resp = await this.request(
209+
`ide/get-models-add-on-checkout-url?${params.toString()}`,
210+
{
211+
method: "GET",
212+
},
213+
);
214+
return (await resp.json()) as { url: string };
215+
} catch (e) {
216+
return null;
217+
}
218+
}
185219
}

0 commit comments

Comments
 (0)