Skip to content
Source
Sign inSign up

status.test.ts

434 lines13.4 KB

1import type { SDKModel } from "@cursor/sdk";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Fiber from "effect/Fiber";
5import * as Schema from "effect/Schema";
6import * as TestClock from "effect/testing/TestClock";
7import type { CursorSettings } from "../settings.ts";
8import { CursorSettings as CursorSettingsSchema } from "../settings.ts";
9import { createModelCapabilities } from "@t3tools/shared/model";
10​
11import {
12 buildCursorCapabilitiesFromSdkModel,
13 buildCursorDiscoveredModelsFromSdk,
14 buildCursorProviderSnapshot,
15 checkCursorProviderStatus,
16} from "./status.ts";
17import * as CursorSdkCatalog from "./CursorSdkCatalog.ts";
18import { loadCursorSdk } from "./sdk.ts";
19​
20const { CursorSdkError } = loadCursorSdk();
21​
22const decodeCursorSettings = Schema.decodeSync(CursorSettingsSchema);
23​
24function selectDescriptor(
25 id: string,
26 label: string,
27 options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,
28) {
29 return {
30 id,
31 label,
32 type: "select" as const,
33 options: [...options],
34 ...(options.find((option) => option.isDefault)?.id
35 ? { currentValue: options.find((option) => option.isDefault)?.id }
36 : {}),
37 };
39​
40function booleanDescriptor(id: string, label: string, currentValue?: boolean) {
41 return {
42 id,
43 label,
44 type: "boolean" as const,
45 ...(typeof currentValue === "boolean" ? { currentValue } : {}),
46 };
48​
49const baseCursorSettings: CursorSettings = decodeCursorSettings({
50 enabled: true,
51 customModels: [],
52});
53​
54const sdkParameterizedModel = {
55 id: "claude-opus-4-8",
56 displayName: "Opus 4.8",
57 parameters: [
58 {
59 id: "thinking",
60 displayName: "Thinking",
61 values: [{ value: "false" }, { value: "true" }],
62 },
63 {
64 id: "context",
65 displayName: "Context",
66 values: [
67 { value: "300k", displayName: "300K" },
68 { value: "1m", displayName: "1M" },
69 ],
70 },
71 {
72 id: "effort",
73 displayName: "Effort",
74 values: [
75 { value: "low", displayName: "Low" },
76 { value: "high", displayName: "High" },
77 ],
78 },
79 {
80 id: "fast",
81 displayName: "Fast",
82 values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
83 },
84 ],
85 variants: [
86 {
87 displayName: "Opus 4.8",
88 isDefault: true,
89 params: [
90 { id: "thinking", value: "true" },
91 { id: "context", value: "1m" },
92 { id: "effort", value: "high" },
93 { id: "fast", value: "false" },
94 ],
95 },
96 ],
97} satisfies SDKModel;
98​
99describe("buildCursorProviderSnapshot", () => {
100 it("downgrades ready status to warning when SDK model discovery returns no models", () => {
101 expect(
102 buildCursorProviderSnapshot({
103 checkedAt: "2026-01-01T00:00:00.000Z",
104 cursorSettings: baseCursorSettings,
105 parsed: {
106 version: null,
107 status: "ready",
108 auth: { status: "authenticated", type: "api-key", label: "Cursor API key" },
109 },
110 discoveryWarning: "Cursor SDK model discovery returned no built-in models.",
111 }),
112 ).toMatchObject({
113 status: "warning",
114 message: "Cursor SDK model discovery returned no built-in models.",
115 models: [],
116 supportsConversationRollback: false,
117 });
118 });
119});
120​
121describe("Cursor SDK model discovery", () => {
122 it("maps native SDK parameter ids and default variant values to model capabilities", () => {
123 expect(buildCursorCapabilitiesFromSdkModel(sdkParameterizedModel)).toEqual(
124 createModelCapabilities({
125 optionDescriptors: [
126 selectDescriptor("effort", "Effort", [
127 { id: "low", label: "Low" },
128 { id: "high", label: "High", isDefault: true },
129 ]),
130 selectDescriptor("contextWindow", "Context", [
131 { id: "300k", label: "300K" },
132 { id: "1m", label: "1M", isDefault: true },
133 ]),
134 booleanDescriptor("fastMode", "Fast", false),
135 booleanDescriptor("thinking", "Thinking", true),
136 ],
137 }),
138 );
139 });
140​
141 it("filters invalid and duplicate SDK model entries", () => {
142 expect(
143 buildCursorDiscoveredModelsFromSdk([
144 sdkParameterizedModel,
145 { ...sdkParameterizedModel, displayName: "Duplicate" },
146 { id: "", displayName: "Invalid" },
147 ]),
148 ).toEqual([
149 {
150 slug: "claude-opus-4-8",
151 name: "Opus 4.8",
152 isCustom: false,
153 capabilities: buildCursorCapabilitiesFromSdkModel(sdkParameterizedModel),
154 },
155 ]);
156 });
157});
158​
159describe("checkCursorProviderStatus", () => {
160 it.effect("uses the SDK catalog when CURSOR_API_KEY is configured", () =>
161 Effect.gen(function* () {
162 const provider = yield* checkCursorProviderStatus(
163 {
164 ...baseCursorSettings,
165 customModels: ["internal/cursor-model"],
166 },
167 { CURSOR_API_KEY: "test-cursor-key" },
168 ).pipe(
169 Effect.provide(
170 CursorSdkCatalog.layerTest((apiKey) => {
171 expect(apiKey).toBe("test-cursor-key");
172 return Effect.succeed({
173 user: {
174 apiKeyName: "test-key",
175 userEmail: "cursor@example.com",
176 createdAt: "2026-01-01T00:00:00.000Z",
177 },
178 models: [sdkParameterizedModel],
179 });
180 }),
181 ),
182 );
183​
184 expect(provider).toMatchObject({
185 status: "ready",
186 auth: {
187 status: "authenticated",
188 type: "api-key",
189 label: "Cursor API key (test-key)",
190 email: "cursor@example.com",
191 },
192 models: [
193 { slug: "claude-opus-4-8", isCustom: false },
194 { slug: "internal/cursor-model", isCustom: true },
195 ],
196 });
197 }),
198 );
199​
200 it.effect("allows slow SDK discovery with a configured readiness timeout", () =>
201 Effect.gen(function* () {
202 const fiber = yield* checkCursorProviderStatus(baseCursorSettings, {
203 CURSOR_API_KEY: "test-key",
204 T3CODE_CURSOR_PROBE_TIMEOUT_MS: "30000",
205 }).pipe(
206 Effect.provide(
207 CursorSdkCatalog.layerTest(() =>
208 Effect.sleep("20 seconds").pipe(
209 Effect.as({
210 user: {
211 apiKeyName: "test",
212 userEmail: "cursor@example.com",
213 createdAt: "2026-01-01",
214 },
215 models: [sdkParameterizedModel],
216 }),
217 ),
218 ),
219 ),
220 Effect.forkChild,
221 );
222 yield* TestClock.adjust("20 seconds");
223 expect((yield* Fiber.join(fiber)).status).toBe("ready");
224 }),
225 );
226​
227 it.effect("surfaces SDK authentication failures", () =>
228 Effect.gen(function* () {
229 const provider = yield* checkCursorProviderStatus(baseCursorSettings, {
230 CURSOR_API_KEY: "invalid-test-key",
231 }).pipe(
232 Effect.provide(
233 CursorSdkCatalog.layerTest(() =>
234 Effect.fail(
235 new CursorSdkCatalog.CursorSdkCatalogError({
236 authenticationFailure: true,
237 cause: new Error("unauthorized"),
238 }),
239 ),
240 ),
241 ),
242 );
243​
244 expect(provider).toMatchObject({
245 status: "error",
246 auth: { status: "unauthenticated" },
247 message: "Cursor SDK authentication failed. Check CURSOR_API_KEY.",
248 });
249 }),
250 );
251​
252 it.effect.each([
253 {
254 authenticationType: "browser" as const,
255 message:
256 "Cursor needs a paid plan to run in T3 Code. Upgrade this account or sign in with a different one.",
257 },
258 {
259 authenticationType: "api-key" as const,
260 message:
261 "Cursor needs a paid plan to run in T3 Code. Upgrade the account that owns CURSOR_API_KEY or use a key from a paid account.",
262 },
263 ])(
264 "explains a rejected plan for $authenticationType authentication",
265 ({ authenticationType, message }) =>
266 Effect.gen(function* () {
267 const provider = yield* checkCursorProviderStatus(
268 baseCursorSettings,
269 { CURSOR_API_KEY: "free-plan-key" },
270 authenticationType,
271 ).pipe(
272 Effect.provide(
273 CursorSdkCatalog.layerTest(() =>
274 Effect.fail(
275 new CursorSdkCatalog.CursorSdkCatalogError({
276 authenticationFailure: false,
277 code: "plan_required",
278 cause: new CursorSdkError(
279 "[plan_required] Cloud Agent is not available for free users. Please upgrade to Pro.",
280 { code: "plan_required", status: 403 },
281 ),
282 }),
283 ),
284 ),
285 ),
286 );
287​
288 expect(provider).toMatchObject({
289 status: "error",
290 auth: { status: "unknown" },
291 message,
292 });
293 }),
294 );
295​
296 it.effect.each([
297 {
298 name: "keeps models when /v1/me is blocked",
299 meFailure: new CursorSdkError("[feature_unavailable] Not available for this team.", {
300 code: "feature_unavailable",
301 status: 403,
302 }),
303 expected: {
304 status: "ready",
305 auth: { status: "authenticated", type: "api-key", label: "Cursor API key" },
306 models: [{ slug: "claude-opus-4-8", isCustom: false }],
307 },
308 },
309 {
310 name: "keeps models when /v1/me returns a 403 without a code",
311 meFailure: new CursorSdkError("[unknown] Forbidden", { code: "unknown", status: 403 }),
312 expected: {
313 status: "ready",
314 auth: { status: "authenticated", type: "api-key", label: "Cursor API key" },
315 models: [{ slug: "claude-opus-4-8", isCustom: false }],
316 },
317 },
318 {
319 name: "keeps the paid-plan message when /v1/me returns plan_required",
320 meFailure: new CursorSdkError("[plan_required] Not available for free users.", {
321 code: "plan_required",
322 status: 403,
323 }),
324 expected: {
325 status: "error",
326 auth: { status: "unknown" },
327 message:
328 "Cursor needs a paid plan to run in T3 Code. Upgrade the account that owns CURSOR_API_KEY or use a key from a paid account.",
329 },
330 },
331 {
332 name: "fails when /v1/me has a server error",
333 meFailure: new CursorSdkError("[unknown] Service unavailable", {
334 code: "unknown",
335 status: 503,
336 }),
337 expected: {
338 status: "error",
339 auth: { status: "unknown" },
340 message: "Cursor SDK catalog request failed. Check server logs for details.",
341 },
342 },
343 {
344 name: "rejects a 401 even when its code is feature_unavailable",
345 meFailure: new CursorSdkError("Unauthorized", {
346 code: "feature_unavailable",
347 status: 401,
348 }),
349 expected: {
350 status: "error",
351 auth: { status: "unauthenticated" },
352 message: "Cursor SDK authentication failed. Check CURSOR_API_KEY.",
353 },
354 },
355 {
356 name: "fails a key that /v1/me rejects",
357 meFailure: new CursorSdkError("Unauthorized", { status: 401 }),
358 expected: {
359 status: "error",
360 auth: { status: "unauthenticated" },
361 message: "Cursor SDK authentication failed. Check CURSOR_API_KEY.",
362 },
363 },
364 ])("$name", ({ meFailure, expected }) =>
365 Effect.gen(function* () {
366 const catalog = yield* CursorSdkCatalog.makeCursorSdkCatalog({
367 readUser: () =>
368 Effect.fail(
369 new CursorSdkCatalog.CursorSdkCatalogError({
370 authenticationFailure: meFailure.status === 401,
371 ...(meFailure.status === undefined ? {} : { status: meFailure.status }),
372 ...(meFailure.code === undefined ? {} : { code: meFailure.code }),
373 cause: meFailure,
374 }),
375 ),
376 readModels: () => Effect.succeed([sdkParameterizedModel]),
377 });
378 const provider = yield* checkCursorProviderStatus(baseCursorSettings, {
379 CURSOR_API_KEY: "enterprise-key",
380 }).pipe(Effect.provideService(CursorSdkCatalog.CursorSdkCatalog, catalog));
381​
382 expect(provider).toMatchObject(expected);
383 }),
384 );
385​
386 it.effect("does not report a single slow catalog read as a provider error", () =>
387 Effect.gen(function* () {
388 let reads = 0;
389 const fiber = yield* checkCursorProviderStatus(baseCursorSettings, {
390 CURSOR_API_KEY: "test-cursor-key",
391 }).pipe(
392 Effect.provide(
393 CursorSdkCatalog.layerTest(() => {
394 reads += 1;
395 return reads === 1
396 ? Effect.never
397 : Effect.succeed({
398 user: {
399 apiKeyName: "test-key",
400 userEmail: "cursor@example.com",
401 createdAt: "2026-01-01T00:00:00.000Z",
402 },
403 models: [sdkParameterizedModel],
404 });
405 }),
406 ),
407 Effect.forkChild({ startImmediately: true }),
408 );
409 yield* TestClock.adjust("1 minute");
410 const provider = yield* Fiber.join(fiber);
411​
412 expect(provider.status).not.toBe("error");
413 }),
414 );
415​
416 it.effect("requires a Cursor API key without probing any external Cursor binary", () =>
417 Effect.gen(function* () {
418 const provider = yield* checkCursorProviderStatus(baseCursorSettings).pipe(
419 Effect.provide(
420 CursorSdkCatalog.layerTest(() =>
421 Effect.die("SDK catalog must not be used without CURSOR_API_KEY"),
422 ),
423 ),
424 );
425​
426 expect(provider).toMatchObject({
427 installed: true,
428 status: "error",
429 auth: { status: "unauthenticated" },
430 message: "Sign in with Cursor or add CURSOR_API_KEY in provider settings.",
431 });
432 }),
433 );
434});