1import { describe, expect, it } from "@effect/vitest";
2import { NodeServices } from "@effect/platform-node";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Path from "effect/Path";
7import * as HttpClient from "effect/http/HttpClient";
8import * as HttpClientResponse from "effect/http/HttpClientResponse";
9import * as CursorKeychain from "./CursorKeychain.ts";
10import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./usageLimits.ts";
11
12const withNodeServices = <A, E>(
13 effect: Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | CursorKeychain.CursorKeychain>,
14 accessToken: CursorKeychain.CursorKeychain["Service"]["accessToken"] = Effect.die(
15 "must not read Keychain",
16 ),
17) =>
18 effect.pipe(
19 Effect.provideService(CursorKeychain.CursorKeychain, { accessToken }),
20 Effect.provide(NodeServices.layer),
21 );
22
23describe("Cursor usage limits", () => {
24 const checkedAt = "2026-09-16T00:00:00.000Z";
25
26 it("uses the advertised percentages and billing-cycle reset", () => {
27 const limits = cursorUsageResponseToLimits(
28 {
29 billingCycleEnd: "1789876386000",
30 planUsage: { totalPercentUsed: 72.4, autoPercentUsed: 69.5, apiPercentUsed: 100 },
31 },
32 checkedAt,
33 );
34 expect(limits.windows).toEqual(
35 expect.arrayContaining([
36 {
37 id: "totalPercentUsed",
38 kind: "monthly",
39 label: "Overall",
40 usedPercent: 72.4,
41 resetsAt: "2026-09-20T03:53:06.000Z",
42 },
43 {
44 id: "autoPercentUsed",
45 kind: "monthly",
46 label: "Cursor Models",
47 usedPercent: 69.5,
48 resetsAt: "2026-09-20T03:53:06.000Z",
49 },
50 {
51 id: "apiPercentUsed",
52 kind: "monthly",
53 label: "Other Models",
54 usedPercent: 100,
55 resetsAt: "2026-09-20T03:53:06.000Z",
56 },
57 ]),
58 );
59 });
60
61 it("does not invent unused allowance for absent buckets", () => {
62 expect(cursorUsageResponseToLimits({ planUsage: {} }, checkedAt).unavailable?.reason).toBe(
63 "unsupported",
64 );
65 expect(
66 cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 0 } }, checkedAt).windows,
67 ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Overall", usedPercent: 0 }]);
68 expect(
69 cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 150 } }, checkedAt).windows,
70 ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Overall", usedPercent: 100 }]);
71 });
72
73 it.effect("reads the instance's credentials and endpoint even when usage enabled is false", () =>
74 Effect.gen(function* () {
75 yield* withNodeServices(
76 Effect.gen(function* () {
77 const fs = yield* FileSystem.FileSystem;
78 const path = yield* Path.Path;
79 const directory = yield* fs.makeTempDirectoryScoped();
80 yield* fs.makeDirectory(path.join(directory, "cursor"));
81 yield* fs.writeFileString(
82 path.join(directory, "cursor", "auth.json"),
83 '{"accessToken":"instance-token"}',
84 );
85 const client = HttpClient.make((request) => {
86 expect(request.url).toBe(
87 "https://cursor.example/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
88 );
89 expect(request.method).toBe("POST");
90 expect(request.headers.authorization).toBe("Bearer instance-token");
91 expect(request.headers["connect-protocol-version"]).toBe("1");
92 return Effect.succeed(
93 HttpClientResponse.fromWeb(
94 request,
95 Response.json({ enabled: false, planUsage: { totalPercentUsed: 42 } }),
96 ),
97 );
98 });
99 yield* fs.makeDirectory(path.join(directory, ".cursor"));
100 yield* fs.writeFileString(
101 path.join(directory, ".cursor", "auth.json"),
102 '{"accessToken":"instance-token"}',
103 );
104 for (const platform of ["linux", "darwin"] as const) {
105 const limits = yield* readCursorUsageLimits(
106 { apiEndpoint: "https://cursor.example/" },
107 { XDG_CONFIG_HOME: directory, HOME: directory, AGENT_CLI_CREDENTIAL_STORE: "file" },
108 ).pipe(
109 Effect.provideService(HostProcess.Platform, platform),
110 Effect.provideService(HttpClient.HttpClient, client),
111 );
112 expect(limits.windows[0]?.usedPercent).toBe(42);
113 }
114 }).pipe(Effect.scoped),
115 );
116 }),
117 );
118
119 it.effect(
120 "never reads stale files for keychain or memory logins, but accepts an explicit auth token",
121 () =>
122 Effect.gen(function* () {
123 for (const platform of ["linux", "darwin"] as const) {
124 for (const token of [undefined, "explicit-token"]) {
125 const limits = yield* withNodeServices(
126 readCursorUsageLimits(
127 { apiEndpoint: "" },
128 {
129 AGENT_CLI_CREDENTIAL_STORE: platform === "linux" ? "memory" : "default",
130 ...(token ? { CURSOR_AUTH_TOKEN: token } : {}),
131 },
132 false,
133 ).pipe(
134 Effect.provideService(HostProcess.Platform, platform),
135 Effect.provideService(
136 FileSystem.FileSystem,
137 FileSystem.makeNoop({
138 readFileString: () => Effect.die("must not read an unrelated credential file"),
139 }),
140 ),
141 Effect.provideService(
142 HttpClient.HttpClient,
143 HttpClient.make((request) => {
144 expect(token).toBe("explicit-token");
145 expect(request.headers.authorization).toBe("Bearer explicit-token");
146 return Effect.succeed(
147 HttpClientResponse.fromWeb(
148 request,
149 Response.json({ planUsage: { totalPercentUsed: 10 } }),
150 ),
151 );
152 }),
153 ),
154 ),
155 );
156 if (token) expect(limits.windows[0]?.usedPercent).toBe(10);
157 else expect(limits.unavailable?.reason).toBe("unsupported");
158 }
159 }
160 }),
161 );
162
163 it.effect("reports failed requests without exposing credentials or response bodies", () =>
164 Effect.gen(function* () {
165 const limits = yield* withNodeServices(
166 readCursorUsageLimits({ apiEndpoint: "" }, { CURSOR_AUTH_TOKEN: "private-token" }).pipe(
167 Effect.provideService(
168 HttpClient.HttpClient,
169 HttpClient.make((request) =>
170 Effect.succeed(
171 HttpClientResponse.fromWeb(
172 request,
173 new Response("private response", { status: 401 }),
174 ),
175 ),
176 ),
177 ),
178 ),
179 );
180 expect(limits.unavailable).toEqual({
181 reason: "probeFailed",
182 message: "Cursor could not read usage limits.",
183 });
184 }),
185 );
186
187 it.effect("does not use a stored login for an explicit API key", () =>
188 Effect.gen(function* () {
189 const limits = yield* withNodeServices(
190 readCursorUsageLimits({ apiEndpoint: "" }, { CURSOR_API_KEY: "different-account" }).pipe(
191 Effect.provideService(
192 HttpClient.HttpClient,
193 HttpClient.make(() => Effect.die("must not request usage")),
194 ),
195 ),
196 );
197 expect(limits.unavailable?.reason).toBe("unsupported");
198 }),
199 );
200
201 it.effect("reads the default macOS Cursor login from Keychain for limits", () =>
202 Effect.gen(function* () {
203 const limits = yield* withNodeServices(
204 readCursorUsageLimits({ apiEndpoint: "" }, {}, true).pipe(
205 Effect.provideService(HostProcess.Platform, "darwin"),
206 Effect.provideService(
207 FileSystem.FileSystem,
208 FileSystem.makeNoop({
209 readFileString: () => Effect.die("must not read a stale credential file"),
210 }),
211 ),
212 Effect.provideService(
213 HttpClient.HttpClient,
214 HttpClient.make((request) => {
215 expect(request.headers.authorization).toBe("Bearer keychain-token");
216 return Effect.succeed(
217 HttpClientResponse.fromWeb(
218 request,
219 Response.json({ planUsage: { totalPercentUsed: 42 } }),
220 ),
221 );
222 }),
223 ),
224 ),
225 Effect.succeed("keychain-token"),
226 );
227 expect(limits.windows[0]?.usedPercent).toBe(42);
228 }),
229 );
230
231 it.effect("reports a Keychain initialization failure without failing the provider refresh", () =>
232 Effect.gen(function* () {
233 const limits = yield* withNodeServices(
234 readCursorUsageLimits({ apiEndpoint: "" }, {}, true).pipe(
235 Effect.provideService(HostProcess.Platform, "darwin"),
236 Effect.provideService(
237 HttpClient.HttpClient,
238 HttpClient.make(() => Effect.die("must not request limits without a login")),
239 ),
240 ),
241 Effect.fail(
242 new CursorKeychain.CursorKeychainReadError({
243 cause: new Error("Keychain initialization failed"),
244 }),
245 ),
246 );
247 expect(limits.unavailable?.reason).toBe("probeFailed");
248 }),
249 );
250
251 it.effect("does not read Keychain or send its token to a custom endpoint", () =>
252 Effect.gen(function* () {
253 for (const [apiEndpoint, environment] of [
254 ["http://localhost:3000", {}],
255 ["", { CURSOR_API_ENDPOINT: "http://localhost:3000" }],
256 ["https://cursor-proxy.example", {}],
257 ["", { CURSOR_API_ENDPOINT: "https://cursor-proxy.example" }],
258 ] as const) {
259 const limits = yield* withNodeServices(
260 readCursorUsageLimits({ apiEndpoint }, environment, true).pipe(
261 Effect.provideService(HostProcess.Platform, "darwin"),
262 Effect.provideService(
263 HttpClient.HttpClient,
264 HttpClient.make(() => Effect.die("must not send a Keychain credential to a proxy")),
265 ),
266 ),
267 );
268 expect(limits.unavailable?.reason).toBe("unsupported");
269 expect(limits.unavailable?.message).toContain("default Cursor endpoint");
270 }
271 }),
272 );
273});