1// @effect-diagnostics nodeBuiltinImport:off
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { expect, it } from "@effect/vitest";
4import { ProviderInstanceId, ProviderSessionId, ThreadId } from "@t3tools/contracts";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
8import * as Stream from "effect/Stream";
9import { vi } from "vite-plus/test";
10import * as HttpClient from "effect/http/HttpClient";
11import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
12
13import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
14import * as TestProviderHost from "@t3tools/provider-testing/TestProviderHost";
15import { CursorDriver } from "./driver.ts";
16import * as CursorAgentSdk from "./CursorAgentSdk.ts";
17import * as CursorSdk from "./CursorSdk.ts";
18import * as CursorKeychain from "./CursorKeychain.ts";
19import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
20import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";
21import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
22import { Cursor } from "./sdk.ts";
23
24const layerTest = TestProviderHost.layer({ runBackgroundWork: false }).pipe(
25 Layer.provideMerge(NodeServices.layer),
26 Layer.provideMerge(IdAllocator.layer),
27 Layer.provideMerge(McpProviderSessions.layer),
28 Layer.provideMerge(CursorSdk.layer),
29 Layer.provideMerge(
30 Layer.mock(CursorAgentSdk.CursorAgentSdkRunner)({
31 open: () => Effect.die("Maintenance resolution must not open a Cursor session"),
32 }),
33 ),
34 Layer.provideMerge(
35 Layer.succeed(CursorKeychain.CursorKeychain, {
36 accessToken: Effect.die("The driver test must not read the Keychain"),
37 }),
38 ),
39 Layer.provideMerge(
40 Layer.succeed(
41 ProviderEventLoggers.ProviderEventLoggers,
42 ProviderEventLoggers.NoOpProviderEventLoggers,
43 ),
44 ),
45 Layer.provideMerge(
46 Layer.succeed(
47 HttpClient.HttpClient,
48 HttpClient.make(() => Effect.die("Disabled Cursor must not make an HTTP request")),
49 ),
50 ),
51);
52
53it.layer(layerTest)("CursorDriver", (it) => {
54 it.effect(
55 "persists browser credentials, uses them for chat, and closes the SDK session on logout",
56 () =>
57 Effect.gen(function* () {
58 const login = vi.spyOn(Cursor.auth, "login").mockImplementation(async (options) => {
59 options?.onLoginUrl?.("https://cursor.com/loginDeepControl?challenge=test-only");
60 await options?.store?.save({
61 version: 1,
62 backendUrl: "https://api2.cursor.sh",
63 apiKey: "instance-browser-key",
64 createdAtMs: 0,
65 apiKeyExpiresAtMs: 4_000_000_000_000,
66 email: "cursor@example.com",
67 });
68 return { apiKey: "instance-browser-key", apiKeyExpiresAtMs: 4_000_000_000_000 };
69 });
70 const me = vi.spyOn(Cursor, "me").mockResolvedValue({
71 apiKeyName: "T3 Code",
72 createdAt: "2026-01-01T00:00:00.000Z",
73 userEmail: "cursor@example.com",
74 });
75 const models = vi
76 .spyOn(Cursor.models, "list")
77 .mockResolvedValue([{ id: "auto", displayName: "Auto" }]);
78 yield* Effect.addFinalizer(() =>
79 Effect.sync(() => {
80 login.mockRestore();
81 me.mockRestore();
82 models.mockRestore();
83 }),
84 );
85 const input = {
86 instanceId: ProviderInstanceId.make("cursor-browser-persisted"),
87 displayName: "Personal Cursor",
88 enabled: true,
89 environment: [{ name: "CURSOR_API_KEY", value: "", sensitive: true }],
90 config: CursorDriver.defaultConfig(),
91 };
92 const openedKeys: Array<string | undefined> = [];
93 let closed = 0;
94 const instance = yield* CursorDriver.create(input).pipe(
95 Effect.provideService(CursorAgentSdk.CursorAgentSdkRunner, {
96 assertComplete: Effect.void,
97 open: (request) =>
98 Effect.sync(() => {
99 openedKeys.push(request.options.apiKey);
100 return {
101 agentId: "browser-auth-agent",
102 listMessages: Effect.succeed([]),
103 send: () => Effect.die("This test only opens a session"),
104 close: Effect.sync(() => {
105 closed += 1;
106 }),
107 };
108 }),
109 }),
110 );
111 expect((yield* instance.snapshot.refresh).auth.status).toBe("unauthenticated");
112 yield* instance.auth!.start("client");
113 yield* instance.auth!.subscribe("client").pipe(
114 Stream.filter((state) => state.phase === "succeeded"),
115 Stream.runHead,
116 Effect.map(Option.getOrThrow),
117 );
118 expect(me).toHaveBeenCalledWith({ apiKey: "instance-browser-key" });
119 expect(yield* instance.snapshot.getSnapshot).toMatchObject({
120 status: "ready",
121 auth: {
122 status: "authenticated",
123 type: "browser",
124 canLogout: true,
125 email: "cursor@example.com",
126 },
127 setup: { canAuthenticate: true, canInstall: false },
128 });
129 const threadId = ThreadId.make("cursor-browser-thread");
130 const modelSelection = { instanceId: input.instanceId, model: "auto" };
131 const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
132 runtimeMode: "full-access",
133 interactionMode: "default",
134 cwd: process.cwd(),
135 });
136 const runtime = yield* instance.orchestrationAdapter.openSession({
137 threadId,
138 providerSessionId: ProviderSessionId.make("cursor-browser-session"),
139 modelSelection,
140 runtimePolicy,
141 });
142 yield* runtime.ensureThread({ threadId, modelSelection, runtimePolicy });
143 expect(openedKeys).toEqual(["instance-browser-key"]);
144 expect(closed).toBe(0);
145 const recreated = yield* CursorDriver.create(input);
146 expect((yield* recreated.snapshot.refresh).auth.status).toBe("authenticated");
147 yield* instance.auth!.logout(Effect.void);
148 expect(closed).toBe(1);
149 expect((yield* instance.snapshot.getSnapshot).auth.status).toBe("unauthenticated");
150 expect((yield* recreated.snapshot.refresh).auth.status).toBe("unauthenticated");
151 expect(
152 (yield* Effect.exit(runtime.ensureThread({ threadId, modelSelection, runtimePolicy })))
153 ._tag,
154 ).toBe("Failure");
155 expect(openedKeys).toEqual(["instance-browser-key"]);
156 expect(closed).toBe(1);
157 }).pipe(Effect.scoped),
158 );
159
160 it.effect("keeps the bundled SDK manual-only without probing or updating cursor-agent", () =>
161 Effect.gen(function* () {
162 const instance = yield* CursorDriver.create({
163 instanceId: ProviderInstanceId.make("cursor-sdk"),
164 displayName: "Cursor test",
165 enabled: false,
166 environment: [],
167 config: CursorDriver.defaultConfig(),
168 });
169 expect((yield* instance.snapshot.resolveMaintenance()).update).toBeNull();
170 expect((yield* instance.snapshot.refresh).status).toBe("disabled");
171 }).pipe(
172 Effect.provideService(
173 ChildProcessSpawner.ChildProcessSpawner,
174 ChildProcessSpawner.make(() => Effect.die("SDK maintenance must not spawn a process")),
175 ),
176 Effect.scoped,
177 ),
178 );
179});