Open History to see the last commit
1// @effect-diagnostics nodeBuiltinImport:off
2import type { AgentOptions, SDKModel } from "@cursor/sdk";
3import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
4import * as NodeServices from "@effect/platform-node/NodeServices";
5import { expect, it } from "@effect/vitest";
6import {
7 MessageId,
8 NodeId,
9 ProjectId,
10 ProviderInstanceId,
11 ProviderSessionId,
12 RunAttemptId,
13 RunId,
14 ThreadId,
15} from "@t3tools/contracts";
16import * as DateTime from "effect/DateTime";
17import * as Fiber from "effect/Fiber";
18import * as FileSystem from "effect/FileSystem";
19import * as Effect from "effect/Effect";
20import * as Layer from "effect/Layer";
21import * as Option from "effect/Option";
22import * as Stream from "effect/Stream";
23import { vi } from "vite-plus/test";
24import * as HttpClient from "effect/http/HttpClient";
25import * as HttpClientResponse from "effect/http/HttpClientResponse";
26import * as Deferred from "effect/Deferred";
27
28import {
29 DEFAULT_HANDOFF_TOKEN_CAP,
30 handoffBudget,
31} from "@t3tools/provider-core/server/handoffBudget";
32import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
33import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
34import { CursorDriver } from "./driver.ts";
35import * as CursorAgentSdk from "./CursorAgentSdk.ts";
36import * as CursorSdk from "./CursorSdk.ts";
37import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
38import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
39import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
40import { loadCursorSdk } from "./sdk.ts";
41
42const { Cursor } = loadCursorSdk();
43
44const layerTest = TestProviderHost.layer({ runBackgroundWork: false }).pipe(
45 Layer.provideMerge(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer)),
46 Layer.provideMerge(IdAllocator.layer),
47 Layer.provideMerge(McpProviderSessions.layer),
48 Layer.provideMerge(CursorSdk.layer.pipe(Layer.provide(NodeServices.layer))),
49 Layer.provideMerge(
50 Layer.mock(CursorAgentSdk.CursorAgentSdkRunner)({
51 open: () => Effect.die("Maintenance resolution must not open a Cursor session"),
52 }),
53 ),
54 Layer.provideMerge(
55 Layer.succeed(
56 ProviderEventLoggers.ProviderEventLoggers,
57 ProviderEventLoggers.NoOpProviderEventLoggers,
58 ),
59 ),
60 Layer.provideMerge(
61 Layer.succeed(
62 HttpClient.HttpClient,
63 HttpClient.make(() => Effect.die("Disabled Cursor must not make an HTTP request")),
64 ),
65 ),
66);
67
68it.layer(layerTest)("CursorDriver", (it) => {
69 it.effect(
70 "persists browser credentials, uses them for chat, and closes the SDK session on logout",
71 () =>
72 Effect.gen(function* () {
73 const login = vi.spyOn(Cursor.auth, "login").mockImplementation(async (options) => {
74 options?.onLoginUrl?.("https://cursor.com/loginDeepControl?challenge=test-only");
75 await options?.store?.save({
76 version: 1,
77 backendUrl: "https://api2.cursor.sh",
78 apiKey: "instance-browser-key",
79 createdAtMs: 0,
80 apiKeyExpiresAtMs: 4_000_000_000_000,
81 email: "cursor@example.com",
82 });
83 return { apiKey: "instance-browser-key", apiKeyExpiresAtMs: 4_000_000_000_000 };
84 });
85 const requests: Array<{ url: string; authorization: string | undefined }> = [];
86 const client = HttpClient.make((request) => {
87 requests.push({ url: request.url, authorization: request.headers.authorization });
88 return Effect.succeed(
89 HttpClientResponse.fromWeb(
90 request,
91 Response.json(
92 request.url.endsWith("/v1/me")
93 ? {
94 apiKeyName: "T3 Code",
95 createdAt: "2026-01-01T00:00:00.000Z",
96 userEmail: "cursor@example.com",
97 }
98 : { items: [{ id: "auto", displayName: "Auto" }] },
99 ),
100 ),
101 );
102 });
103 yield* Effect.addFinalizer(() =>
104 Effect.sync(() => {
105 login.mockRestore();
106 }),
107 );
108 const input = {
109 instanceId: ProviderInstanceId.make("cursor-browser-persisted"),
110 displayName: "Personal Cursor",
111 enabled: true,
112 environment: [{ name: "CURSOR_API_KEY", value: "", sensitive: true }],
113 config: CursorDriver.defaultConfig(),
114 };
115 const openedKeys: Array<string | undefined> = [];
116 let closed = 0;
117 const instance = yield* CursorDriver.create(input).pipe(
118 Effect.provideService(HttpClient.HttpClient, client),
119 Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {
120 assertComplete: Effect.void,
121 open: (request) =>
122 Effect.sync(() => {
123 openedKeys.push(request.options.apiKey);
124 return {
125 agentId: "browser-auth-agent",
126 listMessages: Effect.succeed([]),
127 send: () => Effect.die("This test only opens a session"),
128 close: Effect.sync(() => {
129 closed += 1;
130 }),
131 };
132 }),
133 }),
134 );
135 expect((yield* instance.snapshot.refresh).auth.status).toBe("unauthenticated");
136 yield* instance.auth!.start("client");
137 yield* instance.auth!.subscribe("client").pipe(
138 Stream.filter((state) => state.phase === "succeeded"),
139 Stream.runHead,
140 Effect.map(Option.getOrThrow),
141 );
142 expect(requests).toContainEqual({
143 url: "https://api.cursor.com/v1/me",
144 authorization: "Bearer instance-browser-key",
145 });
146 expect(yield* instance.snapshot.getSnapshot).toMatchObject({
147 status: "ready",
148 auth: {
149 status: "authenticated",
150 type: "browser",
151 canLogout: true,
152 email: "cursor@example.com",
153 },
154 setup: { canAuthenticate: true, canInstall: false },
155 });
156 const threadId = ThreadId.make("cursor-browser-thread");
157 const modelSelection = { instanceId: input.instanceId, model: "auto" };
158 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
159 runtimeMode: "full-access",
160 interactionMode: "default",
161 cwd: process.cwd(),
162 });
163 const runtime = yield* instance.orchestrationAdapter.openSession({
164 threadId,
165 providerSessionId: ProviderSessionId.make("cursor-browser-session"),
166 modelSelection,
167 runtimePolicy,
168 });
169 yield* runtime.ensureThread({ threadId, modelSelection, runtimePolicy });
170 expect(openedKeys).toEqual(["instance-browser-key"]);
171 expect(closed).toBe(0);
172 const recreated = yield* CursorDriver.create(input).pipe(
173 Effect.provideService(HttpClient.HttpClient, client),
174 );
175 expect((yield* recreated.snapshot.refresh).auth.status).toBe("authenticated");
176 yield* instance.auth!.logout(Effect.void);
177 expect(closed).toBe(1);
178 expect((yield* instance.snapshot.getSnapshot).auth.status).toBe("unauthenticated");
179 expect((yield* recreated.snapshot.refresh).auth.status).toBe("unauthenticated");
180 expect(
181 (yield* Effect.exit(runtime.ensureThread({ threadId, modelSelection, runtimePolicy })))
182 ._tag,
183 ).toBe("Failure");
184 expect(openedKeys).toEqual(["instance-browser-key"]);
185 expect(closed).toBe(1);
186 }),
187 );
188
189 it.effect("budgets handoffs for the SDK model across catalog refreshes and option changes", () =>
190 Effect.gen(function* () {
191 const fileSystem = yield* FileSystem.FileSystem;
192 const workspace = yield* fileSystem.makeTempDirectoryScoped({ prefix: "cursor-window-" });
193 const catalog = yield* Deferred.make<Array<SDKModel>>();
194 const catalogRequested = yield* Deferred.make<void>();
195 let failUser = false;
196 const client = HttpClient.make((request) =>
197 Effect.gen(function* () {
198 if (request.url.endsWith("/v1/me"))
199 return HttpClientResponse.fromWeb(
200 request,
201 Response.json(
202 failUser
203 ? { error: { message: "Cursor is unreachable" } }
204 : { apiKeyName: "T3 Code", createdAt: "2026-01-01T00:00:00.000Z" },
205 { status: failUser ? 503 : 200 },
206 ),
207 );
208 yield* Deferred.succeed(catalogRequested, undefined);
209 const models = yield* Deferred.await(catalog);
210 return HttpClientResponse.fromWeb(request, Response.json({ items: models }));
211 }),
212 );
213 const instanceId = ProviderInstanceId.make("cursor-catalog-window");
214 const openedModels: Array<AgentOptions["model"]> = [];
215 const sentModels: Array<AgentOptions["model"]> = [];
216 const instance = yield* CursorDriver.create({
217 instanceId,
218 displayName: "Cursor",
219 enabled: true,
220 environment: [{ name: "CURSOR_API_KEY", value: "catalog-window-key", sensitive: true }],
221 config: CursorDriver.defaultConfig(),
222 }).pipe(
223 Effect.provideService(HttpClient.HttpClient, client),
224 Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {
225 assertComplete: Effect.void,
226 open: (request) =>
227 Effect.sync(() => {
228 openedModels.push(request.options.model);
229 return {
230 agentId: "native-cursor-window",
231 listMessages: Effect.succeed([]),
232 close: Effect.void,
233 send: (input) =>
234 Effect.sync(() => {
235 sentModels.push(input.options?.model);
236 const runId = `native-cursor-window-run-${sentModels.length}`;
237 return {
238 agentId: "native-cursor-window",
239 runId,
240 cancel: Effect.void,
241 wait: Effect.succeed({
242 id: runId,
243 requestId: runId,
244 status: "finished" as const,
245 model: { id: "claude-opus-5-5" },
246 durationMs: 1,
247 }),
248 };
249 }),
250 };
251 }),
252 }),
253 );
254 const selection = (contextWindow?: string) => ({
255 instanceId,
256 model: "claude-opus-5-5",
257 ...(contextWindow === undefined
258 ? {}
259 : { options: [{ id: "contextWindow", value: contextWindow }] }),
260 });
261 const threadId = ThreadId.make("cursor-catalog-window-thread");
262 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
263 runtimeMode: "full-access",
264 interactionMode: "default",
265 cwd: workspace,
266 });
267 const runtime = yield* instance.orchestrationAdapter.openSession({
268 threadId,
269 providerSessionId: ProviderSessionId.make("cursor-catalog-window-session"),
270 modelSelection: selection("1m"),
271 runtimePolicy,
272 });
273 const refresh = yield* instance.snapshot.refresh.pipe(Effect.forkScoped);
274 yield* Deferred.await(catalogRequested);
275 expect(runtime.getModelContextWindow?.(selection())).toBeUndefined();
276 const providerThread = yield* runtime.ensureThread({
277 threadId,
278 modelSelection: selection("1m"),
279 runtimePolicy,
280 });
281 const now = yield* DateTime.now;
282 const startTurn = Effect.fnUntraced(function* (contextWindow?: string) {
283 const modelSelection = selection(contextWindow);
284 const ordinal = sentModels.length + 1;
285 yield* runtime.startTurn({
286 threadId,
287 providerThread,
288 modelSelection,
289 runtimePolicy,
290 runId: RunId.make(`cursor-window-run-${ordinal}`),
291 runOrdinal: ordinal,
292 providerTurnOrdinal: ordinal,
293 attemptId: RunAttemptId.make(`cursor-window-attempt-${ordinal}`),
294 rootNodeId: NodeId.make(`cursor-window-root-${ordinal}`),
295 appThread: {
296 id: threadId,
297 projectId: ProjectId.make("cursor-window-project"),
298 createdBy: "user",
299 creationSource: "web",
300 title: "Cursor context window",
301 providerInstanceId: instanceId,
302 modelSelection,
303 runtimeMode: "full-access",
304 interactionMode: "default",
305 branch: null,
306 worktreePath: null,
307 activeProviderThreadId: providerThread.id,
308 lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
309 forkedFrom: null,
310 createdAt: now,
311 updatedAt: now,
312 archivedAt: null,
313 settledOverride: null,
314 settledAt: null,
315 lastVisitedAt: null,
316 deletedAt: null,
317 },
318 message: {
319 messageId: MessageId.make(`cursor-window-message-${ordinal}`),
320 createdBy: "user",
321 creationSource: "web",
322 text: "Continue.",
323 attachments: [],
324 },
325 });
326 yield* runtime.events.pipe(
327 Stream.takeUntil((event) => event.type === "turn.terminal"),
328 Stream.runDrain,
329 );
330 const window = runtime.getModelContextWindow?.(modelSelection);
331 return {
332 model: sentModels.at(-1),
333 window,
334 allowance: handoffBudget({
335 tokenCap: DEFAULT_HANDOFF_TOKEN_CAP,
336 userText: "Continue.",
337 attachments: [],
338 providerThread,
339 nativeContextEstimate: 400_000,
340 modelContextWindow: window,
341 }),
342 };
343 });
344 const beforeCatalog = yield* startTurn("1m");
345 yield* Deferred.succeed(catalog, [
346 {
347 id: "claude-opus-5-5",
348 displayName: "Claude Opus 5.5",
349 parameters: [
350 {
351 id: "context",
352 displayName: "Context",
353 values: [{ value: "300k" }, { value: "1m" }],
354 },
355 ],
356 variants: [
357 {
358 displayName: "Claude Opus 5.5",
359 isDefault: true,
360 params: [{ id: "context", value: "1m" }],
361 },
362 ],
363 },
364 ]);
365 expect((yield* Fiber.join(refresh)).status).toBe("ready");
366 const catalogDefault = yield* startTurn();
367 const explicitWindow = yield* startTurn("500k");
368 const unknownWindow = yield* startTurn("max");
369 failUser = true;
370 expect((yield* instance.snapshot.refresh).status).toBe("error");
371 const retainedDefault = yield* startTurn();
372
373 expect(openedModels).toEqual([
374 {
375 id: "claude-opus-5-5",
376 params: [{ id: "context", value: "1m" }],
377 },
378 ]);
379 expect({
380 beforeCatalog,
381 catalogDefault,
382 explicitWindow,
383 unknownWindow,
384 retainedDefault,
385 }).toEqual({
386 beforeCatalog: {
387 model: { id: "claude-opus-5-5", params: [{ id: "context", value: "1m" }] },
388 window: 1_000_000,
389 allowance: DEFAULT_HANDOFF_TOKEN_CAP,
390 },
391 catalogDefault: {
392 model: { id: "claude-opus-5-5", params: [{ id: "context", value: "1m" }] },
393 window: 1_000_000,
394 allowance: DEFAULT_HANDOFF_TOKEN_CAP,
395 },
396 explicitWindow: {
397 model: { id: "claude-opus-5-5", params: [{ id: "context", value: "500k" }] },
398 window: 500_000,
399 allowance: 0,
400 },
401 unknownWindow: {
402 model: { id: "claude-opus-5-5", params: [{ id: "context", value: "max" }] },
403 window: undefined,
404 allowance: 0,
405 },
406 retainedDefault: catalogDefault,
407 });
408 }),
409 );
410});