1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import { ProviderSessionId, ThreadId } from "@t3tools/contracts";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { vi } from "vite-plus/test";
7
8import * as ProviderEventLoggers from "@t3tools/provider-core/server/ProviderEventLoggers";
9import * as CursorAgentSdk from "./CursorAgentSdk.ts";
10import * as CursorSdk from "./CursorSdk.ts";
11
12const cursorSdkMock = vi.hoisted(() => {
13 const closeExecutionOrder: Array<string> = [];
14 const runOperations: Array<string> = [];
15 let agentCloseFailure: unknown;
16 let staleRunActive = false;
17 const runWait = vi.fn(async () => ({
18 id: "run-cursor-agent-sdk-test",
19 requestId: "request-cursor-agent-sdk-test",
20 status: "finished",
21 result: "ok",
22 model: { id: "default" },
23 durationMs: 1,
24 }));
25 const runCancel = vi.fn(async () => {});
26 const agentClose = vi.fn(() => {
27 closeExecutionOrder.push("sdk:agent.close");
28 if (agentCloseFailure !== undefined) {
29 throw agentCloseFailure;
30 }
31 });
32 const send = vi.fn(
33 async (
34 _message: unknown,
35 options: { readonly onDelta?: (input: { readonly update: unknown }) => Promise<void> },
36 ) => {
37 runOperations.push("run.start");
38 if (staleRunActive) {
39 throw new Error("Agent agent-cursor-agent-sdk-test already has active run");
40 }
41 await options.onDelta?.({
42 update: {
43 type: "assistant-message-chunk",
44 text: "hello",
45 },
46 });
47 return {
48 id: "run-cursor-agent-sdk-test",
49 agentId: "agent-cursor-agent-sdk-test",
50 wait: runWait,
51 cancel: runCancel,
52 };
53 },
54 );
55 const create = vi.fn(async () => ({
56 agentId: "agent-cursor-agent-sdk-test",
57 send,
58 close: agentClose,
59 }));
60 const resume = vi.fn(async () => ({
61 agentId: "agent-cursor-agent-sdk-test",
62 send,
63 close: agentClose,
64 }));
65 const listRuns = vi.fn(async () => {
66 runOperations.push("agent.listRuns");
67 return {
68 items: staleRunActive ? [{ id: "run-stale", status: "running" }] : [],
69 };
70 });
71 const cancelRun = vi.fn(async () => {
72 runOperations.push("agent.cancelRun");
73 staleRunActive = false;
74 });
75
76 return {
77 agentClose,
78 cancelRun,
79 closeExecutionOrder,
80 create,
81 listRuns,
82 resume,
83 runOperations,
84 runCancel,
85 runWait,
86 send,
87 setAgentCloseFailure: (failure: unknown) => {
88 agentCloseFailure = failure;
89 },
90 setStaleRunActive: (active: boolean) => {
91 staleRunActive = active;
92 },
93 };
94});
95
96vi.mock("./sdk.ts", () => ({
97 Agent: {
98 create: cursorSdkMock.create,
99 resume: cursorSdkMock.resume,
100 listRuns: cursorSdkMock.listRuns,
101 cancelRun: cursorSdkMock.cancelRun,
102 messages: {
103 list: vi.fn(async () => []),
104 },
105 },
106 createAgentPlatform: vi.fn(),
107 SqliteLocalAgentStore: { open: vi.fn() },
108}));
109
110const layerTest = CursorAgentSdk.layer.pipe(
111 Layer.provide(
112 Layer.succeed(ProviderEventLoggers.ProviderEventLoggers, {
113 native: {
114 filePath: "cursor-agent-sdk-test.log",
115 write: (event: unknown) =>
116 Effect.sync(() => {
117 if (
118 typeof event === "object" &&
119 event !== null &&
120 Reflect.get(Reflect.get(event, "event"), "payload")?.type === "agent.close"
121 ) {
122 cursorSdkMock.closeExecutionOrder.push("protocol:agent.close");
123 }
124 }),
125 close: () => Effect.void,
126 },
127 canonical: undefined,
128 }),
129 ),
130 Layer.provide(CursorSdk.layer),
131 Layer.provide(NodeServices.layer),
132);
133
134describe("CursorAgentSdkRunner", () => {
135 it.effect("leaves a resumed agent alone when its next send succeeds", () =>
136 Effect.gen(function* () {
137 cursorSdkMock.runOperations.length = 0;
138 cursorSdkMock.setStaleRunActive(false);
139
140 const runner = yield* CursorAgentSdk.CursorAgentSdkRunner;
141 const session = yield* runner.open({
142 operation: "resume",
143 agentId: "agent-cursor-agent-sdk-test",
144 options: { model: { id: "default" }, mode: "agent", local: { cwd: process.cwd() } },
145 threadId: ThreadId.make("thread-cursor-agent-sdk-normal-resume-test"),
146 providerSessionId: ProviderSessionId.make(
147 "provider-session-cursor-agent-sdk-normal-resume-test",
148 ),
149 });
150 const run = yield* session.send({ message: "continue" });
151 yield* run.wait;
152
153 assert.deepStrictEqual(cursorSdkMock.runOperations, ["run.start"]);
154 }).pipe(Effect.provide(layerTest)),
155 );
156
157 it.effect("retries a resumed send after cancelling its abandoned local run", () =>
158 Effect.gen(function* () {
159 cursorSdkMock.runOperations.length = 0;
160 cursorSdkMock.listRuns.mockClear();
161 cursorSdkMock.cancelRun.mockClear();
162 cursorSdkMock.setStaleRunActive(true);
163
164 const runner = yield* CursorAgentSdk.CursorAgentSdkRunner;
165 const session = yield* runner.open({
166 operation: "resume",
167 agentId: "agent-cursor-agent-sdk-test",
168 options: { model: { id: "default" }, mode: "agent", local: { cwd: process.cwd() } },
169 threadId: ThreadId.make("thread-cursor-agent-sdk-stale-run-test"),
170 providerSessionId: ProviderSessionId.make(
171 "provider-session-cursor-agent-sdk-stale-run-test",
172 ),
173 });
174 assert.deepStrictEqual(cursorSdkMock.runOperations, []);
175 const run = yield* session.send({ message: "continue" });
176 yield* run.wait;
177
178 assert.deepStrictEqual(cursorSdkMock.runOperations, [
179 "run.start",
180 "agent.listRuns",
181 "agent.cancelRun",
182 "run.start",
183 ]);
184 assert.deepStrictEqual(cursorSdkMock.listRuns.mock.calls[0], [
185 "agent-cursor-agent-sdk-test",
186 { runtime: "local", cwd: process.cwd(), limit: 1 },
187 ]);
188 assert.deepStrictEqual(cursorSdkMock.cancelRun.mock.calls[0], [
189 "run-stale",
190 {
191 runtime: "local",
192 cwd: process.cwd(),
193 },
194 ]);
195 }).pipe(Effect.provide(layerTest)),
196 );
197
198 it.effect("logs agent.close before invoking the Cursor SDK", () =>
199 Effect.gen(function* () {
200 cursorSdkMock.agentClose.mockClear();
201 cursorSdkMock.closeExecutionOrder.length = 0;
202 cursorSdkMock.setAgentCloseFailure(undefined);
203
204 const runner = yield* CursorAgentSdk.CursorAgentSdkRunner;
205 const session = yield* runner.open({
206 operation: "create",
207 options: {
208 model: { id: "default" },
209 mode: "agent",
210 local: {
211 cwd: process.cwd(),
212 },
213 },
214 threadId: ThreadId.make("thread-cursor-agent-sdk-close-order-test"),
215 providerSessionId: ProviderSessionId.make(
216 "provider-session-cursor-agent-sdk-close-order-test",
217 ),
218 });
219
220 cursorSdkMock.closeExecutionOrder.length = 0;
221 yield* session.close;
222
223 assert.deepStrictEqual(cursorSdkMock.closeExecutionOrder, [
224 "protocol:agent.close",
225 "sdk:agent.close",
226 ]);
227 assert.equal(cursorSdkMock.agentClose.mock.calls.length, 1);
228 }).pipe(Effect.provide(layerTest)),
229 );
230
231 it.effect("logs agent.close even when the Cursor SDK close fails", () =>
232 Effect.gen(function* () {
233 cursorSdkMock.agentClose.mockClear();
234 cursorSdkMock.closeExecutionOrder.length = 0;
235 const closeFailure = new Error("agent close failed");
236 cursorSdkMock.setAgentCloseFailure(closeFailure);
237
238 const runner = yield* CursorAgentSdk.CursorAgentSdkRunner;
239 const session = yield* runner.open({
240 operation: "create",
241 options: {
242 model: { id: "default" },
243 mode: "agent",
244 local: {
245 cwd: process.cwd(),
246 },
247 },
248 threadId: ThreadId.make("thread-cursor-agent-sdk-close-failure-test"),
249 providerSessionId: ProviderSessionId.make(
250 "provider-session-cursor-agent-sdk-close-failure-test",
251 ),
252 });
253
254 cursorSdkMock.closeExecutionOrder.length = 0;
255 const error = yield* Effect.flip(session.close);
256
257 assert.instanceOf(error, CursorAgentSdk.CursorAgentSdkRunnerError);
258 assert.equal(error.method, "agent.close");
259 assert.strictEqual(error.cause, closeFailure);
260 assert.deepStrictEqual(cursorSdkMock.closeExecutionOrder, [
261 "protocol:agent.close",
262 "sdk:agent.close",
263 ]);
264 assert.equal(cursorSdkMock.agentClose.mock.calls.length, 1);
265 cursorSdkMock.setAgentCloseFailure(undefined);
266 }).pipe(Effect.provide(layerTest)),
267 );
268
269 it.effect("surfaces interaction callback failures through run.wait", () =>
270 Effect.gen(function* () {
271 cursorSdkMock.create.mockClear();
272 cursorSdkMock.runWait.mockClear();
273 cursorSdkMock.send.mockClear();
274
275 const runner = yield* CursorAgentSdk.CursorAgentSdkRunner;
276 const session = yield* runner.open({
277 operation: "create",
278 options: {
279 model: { id: "default" },
280 mode: "agent",
281 local: {
282 cwd: process.cwd(),
283 autoReview: false,
284 sandboxOptions: { enabled: false },
285 enableAgentRetries: true,
286 },
287 },
288 threadId: ThreadId.make("thread-cursor-agent-sdk-test"),
289 providerSessionId: ProviderSessionId.make("provider-session-cursor-agent-sdk-test"),
290 });
291
292 const callbackFailure = new Error("delta callback failed");
293 const run = yield* session.send({
294 message: "hello",
295 onDelta: () => Effect.fail(callbackFailure),
296 });
297
298 const error = yield* Effect.flip(run.wait);
299
300 assert.instanceOf(error, CursorAgentSdk.CursorAgentSdkRunnerError);
301 assert.equal(error.method, "run.wait");
302 assert.strictEqual(error.cause, callbackFailure);
303 assert.equal(cursorSdkMock.create.mock.calls.length, 1);
304 assert.equal(cursorSdkMock.send.mock.calls.length, 1);
305 assert.equal(cursorSdkMock.runWait.mock.calls.length, 1);
306 }).pipe(Effect.provide(layerTest)),
307 );
308});