| ... | ... | @@ -1,8 +1,10 @@ |
| 1 | | import type { RunResult } from "@cursor/sdk"; |
| 1 | import type { RunResult, SdkCredentialStore, StoredSdkCredentials } from "@cursor/sdk"; |
| 2 | 2 | import { ProviderInstanceId, TextGenerationError } from "@t3tools/contracts"; |
| 3 | 3 | import { CursorSettings } from "../settings.ts"; |
| 4 | 4 | import { describe, expect, it } from "@effect/vitest"; |
| 5 | import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; |
| 5 | 6 | import * as Effect from "effect/Effect"; |
| 7 | import * as Exit from "effect/Exit"; |
| 6 | 8 | import * as FileSystem from "effect/FileSystem"; |
| 7 | 9 | import * as Fiber from "effect/Fiber"; |
| 8 | 10 | import * as Layer from "effect/Layer"; |
| ... | ... | @@ -11,6 +13,7 @@ import * as Schema from "effect/Schema"; |
| 11 | 13 | import { createModelSelection } from "@t3tools/shared/model"; |
| 12 | 14 | import { beforeEach, vi } from "vite-plus/test"; |
| 13 | 15 | |
| 16 | import { makeCursorAuth } from "./auth.ts"; |
| 14 | 17 | import { makeCursorTextGeneration } from "./textGeneration.ts"; |
| 15 | 18 | import * as CursorSdk from "./CursorSdk.ts"; |
| 16 | 19 | |
| ... | ... | @@ -19,6 +22,8 @@ const cursorSdkMock = vi.hoisted(() => ({ |
| 19 | 22 | send: vi.fn<(prompt: string) => Promise<unknown>>(), |
| 20 | 23 | close: vi.fn(), |
| 21 | 24 | cancel: vi.fn(async () => {}), |
| 25 | storeOpen: vi.fn<(options: unknown) => Promise<unknown>>(), |
| 26 | storeDispose: vi.fn(async () => {}), |
| 22 | 27 | prompt: vi.fn<(prompt: string, options: unknown) => Promise<RunResult>>(async () => ({ |
| 23 | 28 | id: "run-cursor-text-generation-test", |
| 24 | 29 | status: "finished", |
| ... | ... | @@ -30,6 +35,7 @@ const cursorSdkMock = vi.hoisted(() => ({ |
| 30 | 35 | vi.mock("./sdk.ts", () => ({ |
| 31 | 36 | Agent: { create: cursorSdkMock.create }, |
| 32 | 37 | createAgentPlatform: vi.fn(), |
| 38 | SqliteLocalAgentStore: { open: cursorSdkMock.storeOpen }, |
| 33 | 39 | })); |
| 34 | 40 | |
| 35 | 41 | let hasCustomPolicy = false; |
| ... | ... | @@ -62,6 +68,9 @@ beforeEach(() => { |
| 62 | 68 | }); |
| 63 | 69 | cursorSdkMock.close.mockClear(); |
| 64 | 70 | cursorSdkMock.cancel.mockClear(); |
| 71 | cursorSdkMock.storeDispose.mockClear(); |
| 72 | cursorSdkMock.storeOpen.mockReset(); |
| 73 | cursorSdkMock.storeOpen.mockResolvedValue({ dispose: cursorSdkMock.storeDispose }); |
| 65 | 74 | cursorSdkMock.prompt.mockReset(); |
| 66 | 75 | cursorSdkMock.prompt.mockResolvedValue({ |
| 67 | 76 | id: "run-cursor-text-generation-test", |
| ... | ... | @@ -139,6 +148,7 @@ describe("CursorTextGeneration", () => { |
| 139 | 148 | }, |
| 140 | 149 | local: { |
| 141 | 150 | cwd: "/isolated-text-generation", |
| 151 | store: { dispose: cursorSdkMock.storeDispose }, |
| 142 | 152 | autoReview: false, |
| 143 | 153 | sandboxOptions: { enabled: true }, |
| 144 | 154 | settingSources: [], |
| ... | ... | @@ -148,6 +158,93 @@ describe("CursorTextGeneration", () => { |
| 148 | 158 | }).pipe(Effect.provide(layerTest)), |
| 149 | 159 | ); |
| 150 | 160 | |
| 161 | it.effect("disposes the agent store after the agent, whether the run succeeds or fails", () => |
| 162 | Effect.gen(function* () { |
| 163 | const textGeneration = yield* makeCursorTextGeneration(cursorSettings, { |
| 164 | CURSOR_API_KEY: "test-cursor-key", |
| 165 | }); |
| 166 | const input = { |
| 167 | cwd: process.cwd(), |
| 168 | branch: "feature/cursor-store", |
| 169 | stagedSummary: "M README.md", |
| 170 | stagedPatch: "diff --git a/README.md b/README.md", |
| 171 | modelSelection: createModelSelection(ProviderInstanceId.make("cursor"), "composer-2"), |
| 172 | }; |
| 173 | |
| 174 | yield* textGeneration.generateCommitMessage(input); |
| 175 | expect(cursorSdkMock.storeOpen).toHaveBeenCalledWith( |
| 176 | expect.objectContaining({ workspaceRef: "/isolated-text-generation" }), |
| 177 | ); |
| 178 | expect(cursorSdkMock.storeDispose).toHaveBeenCalledTimes(1); |
| 179 | expect(cursorSdkMock.close.mock.invocationCallOrder[0]).toBeLessThan( |
| 180 | cursorSdkMock.storeDispose.mock.invocationCallOrder[0]!, |
| 181 | ); |
| 182 | |
| 183 | cursorSdkMock.prompt.mockResolvedValueOnce({ |
| 184 | id: "run-cursor-text-generation-test", |
| 185 | status: "error", |
| 186 | }); |
| 187 | yield* Effect.flip(textGeneration.generateCommitMessage(input)); |
| 188 | expect(cursorSdkMock.storeDispose).toHaveBeenCalledTimes(2); |
| 189 | }).pipe(Effect.provide(layerTest)), |
| 190 | ); |
| 191 | |
| 192 | it.effect("closes the agent before its store when sign-out interrupts a run", () => |
| 193 | Effect.gen(function* () { |
| 194 | let credentials: StoredSdkCredentials | undefined = { |
| 195 | version: 1, |
| 196 | backendUrl: "https://api2.cursor.sh", |
| 197 | apiKey: "browser-key", |
| 198 | createdAtMs: 0, |
| 199 | }; |
| 200 | const store: SdkCredentialStore = { |
| 201 | load: async () => credentials, |
| 202 | save: async (next) => { |
| 203 | credentials = next; |
| 204 | }, |
| 205 | clear: async () => { |
| 206 | credentials = undefined; |
| 207 | }, |
| 208 | }; |
| 209 | const auth = yield* makeCursorAuth({ |
| 210 | instanceId: ProviderInstanceId.make("cursor"), |
| 211 | displayName: "Cursor", |
| 212 | enabled: true, |
| 213 | store, |
| 214 | onChanged: () => Effect.void, |
| 215 | }); |
| 216 | const started = Promise.withResolvers<void>(); |
| 217 | cursorSdkMock.prompt.mockImplementationOnce(() => { |
| 218 | started.resolve(); |
| 219 | return new Promise(() => {}); |
| 220 | }); |
| 221 | const generation = yield* makeCursorTextGeneration( |
| 222 | cursorSettings, |
| 223 | {}, |
| 224 | auth.requireApiKey, |
| 225 | auth.withAccess, |
| 226 | ); |
| 227 | const request = yield* generation |
| 228 | .generateCommitMessage({ |
| 229 | cwd: process.cwd(), |
| 230 | branch: "feature/cursor-sign-out", |
| 231 | stagedSummary: "M README.md", |
| 232 | stagedPatch: "diff --git a/README.md b/README.md", |
| 233 | modelSelection: createModelSelection(ProviderInstanceId.make("cursor"), "composer-2"), |
| 234 | }) |
| 235 | .pipe(Effect.forkScoped); |
| 236 | yield* Effect.promise(() => started.promise); |
| 237 | |
| 238 | yield* auth.controller.logout(Effect.void); |
| 239 | |
| 240 | expect(Exit.isFailure(yield* Fiber.await(request))).toBe(true); |
| 241 | expect(cursorSdkMock.storeDispose).toHaveBeenCalledTimes(1); |
| 242 | expect(cursorSdkMock.close.mock.invocationCallOrder[0]).toBeLessThan( |
| 243 | cursorSdkMock.storeDispose.mock.invocationCallOrder[0]!, |
| 244 | ); |
| 245 | }).pipe(Effect.scoped, Effect.provide(Layer.merge(layerTest, NodeCrypto.layer))), |
| 246 | ); |
| 247 | |
| 151 | 248 | it.effect("continues in the temp directory when the SDK cannot sandbox", () => |
| 152 | 249 | Effect.gen(function* () { |
| 153 | 250 | cursorSdkMock.create.mockImplementationOnce(async () => { |
| ... | ... | @@ -175,6 +272,7 @@ describe("CursorTextGeneration", () => { |
| 175 | 272 | expect(cursorSdkMock.create.mock.calls[1]?.[0]).toMatchObject({ |
| 176 | 273 | local: { |
| 177 | 274 | cwd: "/isolated-text-generation", |
| 275 | store: { dispose: cursorSdkMock.storeDispose }, |
| 178 | 276 | autoReview: false, |
| 179 | 277 | sandboxOptions: { enabled: false }, |
| 180 | 278 | settingSources: [], |