1import type { SDKModel } from "@cursor/sdk";
2import { describe, expect, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Schema from "effect/Schema";
5import type { CursorSettings } from "../settings.ts";
6import { CursorSettings as CursorSettingsSchema } from "../settings.ts";
7import { createModelCapabilities } from "@t3tools/shared/model";
8
9import {
10 buildCursorCapabilitiesFromSdkModel,
11 buildCursorDiscoveredModelsFromSdk,
12 buildCursorProviderSnapshot,
13 buildInitialCursorProviderSnapshot,
14 checkCursorProviderStatus,
15} from "./status.ts";
16import * as CursorSdkCatalog from "./CursorSdkCatalog.ts";
17
18const decodeCursorSettings = Schema.decodeSync(CursorSettingsSchema);
19
20function selectDescriptor(
21 id: string,
22 label: string,
23 options: ReadonlyArray<{ id: string; label: string; isDefault?: boolean }>,
24) {
25 return {
26 id,
27 label,
28 type: "select" as const,
29 options: [...options],
30 ...(options.find((option) => option.isDefault)?.id
31 ? { currentValue: options.find((option) => option.isDefault)?.id }
32 : {}),
33 };
34}
35
36function booleanDescriptor(id: string, label: string, currentValue?: boolean) {
37 return {
38 id,
39 label,
40 type: "boolean" as const,
41 ...(typeof currentValue === "boolean" ? { currentValue } : {}),
42 };
43}
44
45const baseCursorSettings: CursorSettings = decodeCursorSettings({
46 enabled: true,
47 customModels: [],
48});
49
50const sdkParameterizedModel = {
51 id: "claude-opus-4-8",
52 displayName: "Opus 4.8",
53 parameters: [
54 {
55 id: "thinking",
56 displayName: "Thinking",
57 values: [{ value: "false" }, { value: "true" }],
58 },
59 {
60 id: "context",
61 displayName: "Context",
62 values: [
63 { value: "300k", displayName: "300K" },
64 { value: "1m", displayName: "1M" },
65 ],
66 },
67 {
68 id: "effort",
69 displayName: "Effort",
70 values: [
71 { value: "low", displayName: "Low" },
72 { value: "high", displayName: "High" },
73 ],
74 },
75 {
76 id: "fast",
77 displayName: "Fast",
78 values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
79 },
80 ],
81 variants: [
82 {
83 displayName: "Opus 4.8",
84 isDefault: true,
85 params: [
86 { id: "thinking", value: "true" },
87 { id: "context", value: "1m" },
88 { id: "effort", value: "high" },
89 { id: "fast", value: "false" },
90 ],
91 },
92 ],
93} satisfies SDKModel;
94
95describe("buildInitialCursorProviderSnapshot", () => {
96 it.effect("uses SDK-specific pending status copy", () =>
97 Effect.gen(function* () {
98 const provider = yield* buildInitialCursorProviderSnapshot(baseCursorSettings);
99
100 expect(provider).toMatchObject({
101 status: "warning",
102 message: "Checking Cursor SDK availability...",
103 });
104 }),
105 );
106});
107
108describe("buildCursorProviderSnapshot", () => {
109 it("downgrades ready status to warning when SDK model discovery returns no models", () => {
110 expect(
111 buildCursorProviderSnapshot({
112 checkedAt: "2026-01-01T00:00:00.000Z",
113 cursorSettings: baseCursorSettings,
114 parsed: {
115 version: null,
116 status: "ready",
117 auth: { status: "authenticated", type: "api-key", label: "Cursor API key" },
118 },
119 discoveryWarning: "Cursor SDK model discovery returned no built-in models.",
120 }),
121 ).toMatchObject({
122 status: "warning",
123 message: "Cursor SDK model discovery returned no built-in models.",
124 models: [],
125 supportsConversationRollback: false,
126 });
127 });
128});
129
130describe("Cursor SDK model discovery", () => {
131 it("maps native SDK parameter ids and default variant values to model capabilities", () => {
132 expect(buildCursorCapabilitiesFromSdkModel(sdkParameterizedModel)).toEqual(
133 createModelCapabilities({
134 optionDescriptors: [
135 selectDescriptor("effort", "Effort", [
136 { id: "low", label: "Low" },
137 { id: "high", label: "High", isDefault: true },
138 ]),
139 selectDescriptor("contextWindow", "Context", [
140 { id: "300k", label: "300K" },
141 { id: "1m", label: "1M", isDefault: true },
142 ]),
143 booleanDescriptor("fastMode", "Fast", false),
144 booleanDescriptor("thinking", "Thinking", true),
145 ],
146 }),
147 );
148 });
149
150 it("filters invalid and duplicate SDK model entries", () => {
151 expect(
152 buildCursorDiscoveredModelsFromSdk([
153 sdkParameterizedModel,
154 { ...sdkParameterizedModel, displayName: "Duplicate" },
155 { id: "", displayName: "Invalid" },
156 ]),
157 ).toEqual([
158 {
159 slug: "claude-opus-4-8",
160 name: "Opus 4.8",
161 isCustom: false,
162 capabilities: buildCursorCapabilitiesFromSdkModel(sdkParameterizedModel),
163 },
164 ]);
165 });
166});
167
168describe("checkCursorProviderStatus", () => {
169 it.effect("uses the SDK catalog when CURSOR_API_KEY is configured", () =>
170 Effect.gen(function* () {
171 const provider = yield* checkCursorProviderStatus(
172 {
173 ...baseCursorSettings,
174 customModels: ["internal/cursor-model"],
175 },
176 { CURSOR_API_KEY: "test-cursor-key" },
177 ).pipe(
178 Effect.provide(
179 CursorSdkCatalog.layerTest((apiKey) => {
180 expect(apiKey).toBe("test-cursor-key");
181 return Effect.succeed({
182 user: {
183 apiKeyName: "test-key",
184 userEmail: "cursor@example.com",
185 createdAt: "2026-01-01T00:00:00.000Z",
186 },
187 models: [sdkParameterizedModel],
188 });
189 }),
190 ),
191 );
192
193 expect(provider).toMatchObject({
194 status: "ready",
195 auth: {
196 status: "authenticated",
197 type: "api-key",
198 label: "Cursor API key (test-key)",
199 email: "cursor@example.com",
200 },
201 models: [
202 { slug: "claude-opus-4-8", isCustom: false },
203 { slug: "internal/cursor-model", isCustom: true },
204 ],
205 });
206 }),
207 );
208
209 it.effect("surfaces SDK authentication failures", () =>
210 Effect.gen(function* () {
211 const provider = yield* checkCursorProviderStatus(baseCursorSettings, {
212 CURSOR_API_KEY: "invalid-test-key",
213 }).pipe(
214 Effect.provide(
215 CursorSdkCatalog.layerTest(() =>
216 Effect.fail(
217 new CursorSdkCatalog.CursorSdkCatalogError({
218 authenticationFailure: true,
219 cause: new Error("unauthorized"),
220 }),
221 ),
222 ),
223 ),
224 );
225
226 expect(provider).toMatchObject({
227 status: "error",
228 auth: { status: "unauthenticated" },
229 message: "Cursor SDK authentication failed. Check CURSOR_API_KEY.",
230 });
231 }),
232 );
233
234 it.effect("requires a Cursor API key without probing any external Cursor binary", () =>
235 Effect.gen(function* () {
236 const provider = yield* checkCursorProviderStatus(baseCursorSettings).pipe(
237 Effect.provide(
238 CursorSdkCatalog.layerTest(() =>
239 Effect.die("SDK catalog must not be used without CURSOR_API_KEY"),
240 ),
241 ),
242 );
243
244 expect(provider).toMatchObject({
245 installed: true,
246 status: "error",
247 auth: { status: "unauthenticated" },
248 message: "Sign in with Cursor or add CURSOR_API_KEY in provider settings.",
249 });
250 }),
251 );
252});