Skip to content
Source
Sign inSign up

CursorSdkCatalog.test.ts

183 lines6.8 KB

1import type { SDKModel, SDKUser } from "@cursor/sdk";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Exit from "effect/Exit";
5import * as Cause from "effect/Cause";
6import * as Deferred from "effect/Deferred";
7import * as Fiber from "effect/Fiber";
8import * as Layer from "effect/Layer";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientResponse from "effect/http/HttpClientResponse";
11import { vi } from "vite-plus/test";
12​
13const sdkLoad = vi.hoisted(() =>
14 vi.fn(() => {
15 throw new Error("Catalog must not load the SDK");
16 }),
17);
18vi.mock("./sdk.ts", () => ({ loadCursorSdk: sdkLoad, isCursorSdkError: () => false }));
19​
20import * as CursorSdkCatalog from "./CursorSdkCatalog.ts";
21​
22const user = {
23 apiKeyName: "test-key",
24 userEmail: "cursor@example.com",
25 createdAt: "2026-01-01T00:00:00.000Z",
26} satisfies SDKUser;
27​
28const model = {
29 id: "claude-opus-4-8",
30 displayName: "Opus 4.8",
31 parameters: [],
32} satisfies SDKModel;
33​
34const catalogError = () =>
35 new CursorSdkCatalog.CursorSdkCatalogError({
36 authenticationFailure: false,
37 cause: new Error("catalog unavailable"),
38 });
39​
40describe("CursorSdkCatalog", () => {
41 it.effect.each([
42 { route: "me", status: 200, body: user, expected: "ready" },
43 {
44 route: "me",
45 status: 401,
46 body: { error: { code: "feature_unavailable" } },
47 expected: "unauthenticated",
48 },
49 {
50 route: "me",
51 status: 403,
52 body: { error: { code: "feature_unavailable" } },
53 expected: "no-user",
54 },
55 { route: "me", status: 403, body: "Forbidden", expected: "no-user" },
56 { route: "me", status: 403, body: { code: "plan_required" }, expected: "plan" },
57 { route: "models", status: 403, body: { error: { code: "plan_required" } }, expected: "plan" },
58 { route: "models", status: 403, body: {}, expected: "blocked" },
59 { route: "me", status: 503, body: "Unavailable", expected: "error" },
60 { route: "models", status: 200, body: { items: [{ id: "missing-name" }] }, expected: "error" },
61 { route: "me", status: 200, body: "invalid JSON", expected: "error" },
62 ])(
63 "reads $route HTTP $status as $expected without loading the SDK",
64 ({ route, status, body, expected }) =>
65 Effect.gen(function* () {
66 sdkLoad.mockClear();
67 const requests: Array<string> = [];
68 const models = [
69 {
70 ...model,
71 aliases: ["opus"],
72 parameters: [{ id: "fast", values: [{ value: "true" }] }],
73 variants: [
74 {
75 displayName: "Fast",
76 params: [{ id: "fast", value: "true" }],
77 isDefault: true,
78 isMaxMode: true,
79 },
80 ],
81 },
82 ];
83 const client = HttpClient.make((request) => {
84 expect(request.headers.authorization).toBe("Bearer test-key");
85 expect(request.method).toBe("GET");
86 requests.push(request.url);
87 const matches = request.url === `https://api.cursor.com/v1/${route}`;
88 const responseBody = matches
89 ? body
90 : request.url.endsWith("/me")
91 ? user
92 : { items: models };
93 const response =
94 typeof responseBody === "string"
95 ? new Response(responseBody, { status: matches ? status : 200 })
96 : Response.json(responseBody, { status: matches ? status : 200 });
97 return Effect.succeed(HttpClientResponse.fromWeb(request, response));
98 });
99 const catalog = yield* CursorSdkCatalog.CursorSdkCatalog.pipe(
100 Effect.provide(
101 CursorSdkCatalog.layer.pipe(
102 Layer.provide(Layer.succeed(HttpClient.HttpClient, client)),
103 ),
104 ),
105 );
106 expect(sdkLoad).not.toHaveBeenCalled();
107 const result = yield* catalog.read("test-key").pipe(Effect.exit);
108 expect(requests).toContain(`https://api.cursor.com/v1/${route}`);
109 if (expected === "ready" || expected === "no-user") {
110 expect(Exit.isSuccess(result)).toBe(true);
111 if (Exit.isSuccess(result))
112 expect(result.value).toEqual({
113 user: expected === "no-user" ? undefined : user,
114 models,
115 });
116 } else {
117 expect(Exit.isFailure(result)).toBe(true);
118 if (Exit.isFailure(result))
119 expect(Cause.squash(result.cause)).toMatchObject({
120 authenticationFailure: expected === "unauthenticated",
121 planRequired: expected === "plan",
122 accountBlocked: expected === "blocked",
123 });
124 }
125 expect(sdkLoad).not.toHaveBeenCalled();
126 }),
127 );
128​
129 it.effect("shares concurrent first model discovery until the response arrives", () =>
130 Effect.gen(function* () {
131 const modelsEntered = yield* Deferred.make<void>();
132 const secondUserEntered = yield* Deferred.make<void>();
133 const response = yield* Deferred.make<ReadonlyArray<SDKModel>>();
134 let userCalls = 0;
135 let modelCalls = 0;
136 const catalog = yield* CursorSdkCatalog.makeCursorSdkCatalog({
137 readUser: () =>
138 Effect.gen(function* () {
139 userCalls += 1;
140 if (userCalls === 2) yield* Deferred.succeed(secondUserEntered, undefined);
141 return user;
142 }),
143 readModels: () =>
144 Effect.gen(function* () {
145 modelCalls += 1;
146 yield* Deferred.succeed(modelsEntered, undefined);
147 return yield* Deferred.await(response);
148 }),
149 });
150 expect(modelCalls).toBe(0);
151 const first = yield* catalog.read("test-key").pipe(Effect.forkChild);
152 yield* Deferred.await(modelsEntered);
153 const second = yield* catalog.read("test-key").pipe(Effect.forkChild);
154 yield* Deferred.await(secondUserEntered);
155 expect(modelCalls).toBe(1);
156 yield* Deferred.succeed(response, [model]);
157 expect((yield* Fiber.join(first)).models).toEqual([model]);
158 expect((yield* Fiber.join(second)).models).toEqual([model]);
159 }),
160 );
161​
162 it.effect("does not cache failed or empty model discovery", () =>
163 Effect.gen(function* () {
164 let modelCalls = 0;
165 const catalog = yield* CursorSdkCatalog.makeCursorSdkCatalog({
166 readUser: () => Effect.succeed(user),
167 readModels: () =>
168 Effect.suspend(() => {
169 modelCalls += 1;
170 if (modelCalls === 1) return Effect.fail(catalogError());
171 if (modelCalls === 2) return Effect.succeed([]);
172 return Effect.succeed([model]);
173 }),
174 });
175​
176 expect(Exit.isFailure(yield* Effect.exit(catalog.read("test-key")))).toBe(true);
177 expect((yield* catalog.read("test-key")).models).toEqual([]);
178 expect((yield* catalog.read("test-key")).models).toEqual([model]);
179 expect((yield* catalog.read("test-key")).models).toEqual([model]);
180 expect(modelCalls).toBe(3);
181 }),
182 );
183});