1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as HttpClient from "effect/http/HttpClient";
4import * as HttpClientResponse from "effect/http/HttpClientResponse";
5import { cursorUsageResponseToLimits, readCursorUsageLimits } from "./usageLimits.ts";
6
7describe("Cursor usage limits", () => {
8 const checkedAt = "2026-09-16T00:00:00.000Z";
9
10 it("uses the advertised percentages and billing-cycle reset", () => {
11 const limits = cursorUsageResponseToLimits(
12 {
13 billingCycleEnd: "1789876386000",
14 planUsage: { totalPercentUsed: 72.4, autoPercentUsed: 69.5, apiPercentUsed: 100 },
15 },
16 checkedAt,
17 );
18 expect(limits.windows).toEqual(
19 expect.arrayContaining([
20 {
21 id: "totalPercentUsed",
22 kind: "monthly",
23 label: "Overall",
24 usedPercent: 72.4,
25 resetsAt: "2026-09-20T03:53:06.000Z",
26 },
27 {
28 id: "autoPercentUsed",
29 kind: "monthly",
30 label: "Cursor Models",
31 usedPercent: 69.5,
32 resetsAt: "2026-09-20T03:53:06.000Z",
33 },
34 {
35 id: "apiPercentUsed",
36 kind: "monthly",
37 label: "Other Models",
38 usedPercent: 100,
39 resetsAt: "2026-09-20T03:53:06.000Z",
40 },
41 ]),
42 );
43 });
44
45 it("reports the billing-cycle length so pace can be computed", () => {
46 const planUsage = { totalPercentUsed: 40, autoPercentUsed: 30, apiPercentUsed: 20 };
47 const durations = (billingCycleStart: string | number | undefined, billingCycleEnd: string) =>
48 cursorUsageResponseToLimits(
49 { billingCycleStart, billingCycleEnd, planUsage },
50 checkedAt,
51 ).windows.map((window) => window.windowDurationMins);
52 expect(durations("1794045847072", "1796637847072")).toEqual([43_200, 43_200, 43_200]);
53 expect(durations(1_796_637_847_072, "1799316247072")).toEqual([44_640, 44_640, 44_640]);
54 });
55
56 it("omits the cycle length when the start is missing or invalid", () => {
57 for (const billingCycleStart of [undefined, "", "0", "not-a-number", "1796637847072"]) {
58 expect(
59 cursorUsageResponseToLimits(
60 {
61 billingCycleStart,
62 billingCycleEnd: "1796637847072",
63 planUsage: { totalPercentUsed: 40 },
64 },
65 checkedAt,
66 ).windows,
67 ).toEqual([
68 {
69 id: "totalPercentUsed",
70 kind: "monthly",
71 label: "Overall",
72 usedPercent: 40,
73 resetsAt: "2026-12-07T10:04:07.072Z",
74 },
75 ]);
76 }
77 });
78
79 it("retains unified totals for account aggregation", () => {
80 const windows = cursorUsageResponseToLimits(
81 { planUsage: { totalPercentUsed: 1.77, autoPercentUsed: 1.77 } },
82 checkedAt,
83 ).windows;
84 expect(windows.map((window) => window.id)).toEqual(["autoPercentUsed", "totalPercentUsed"]);
85 expect(
86 cursorUsageResponseToLimits(
87 { planUsage: { totalPercentUsed: 100, apiPercentUsed: 100 } },
88 checkedAt,
89 ).windows.map((window) => window.id),
90 ).toEqual(["apiPercentUsed", "totalPercentUsed"]);
91 });
92
93 it("does not invent unused allowance for absent buckets", () => {
94 expect(cursorUsageResponseToLimits({ planUsage: {} }, checkedAt).unavailable?.reason).toBe(
95 "unsupported",
96 );
97 expect(
98 cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 0 } }, checkedAt).windows,
99 ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Overall", usedPercent: 0 }]);
100 expect(
101 cursorUsageResponseToLimits({ planUsage: { totalPercentUsed: 150 } }, checkedAt).windows,
102 ).toEqual([{ id: "totalPercentUsed", kind: "monthly", label: "Overall", usedPercent: 100 }]);
103 });
104
105 it.effect("requires explicit credentials and accepts an auth token", () =>
106 Effect.gen(function* () {
107 for (const token of [undefined, "explicit-token"]) {
108 const limits = yield* readCursorUsageLimits(
109 { apiEndpoint: "" },
110 token ? { CURSOR_AUTH_TOKEN: token } : {},
111 ).pipe(
112 Effect.provideService(
113 HttpClient.HttpClient,
114 HttpClient.make((request) => {
115 expect(token).toBe("explicit-token");
116 expect(request.headers.authorization).toBe("Bearer explicit-token");
117 return Effect.succeed(
118 HttpClientResponse.fromWeb(
119 request,
120 Response.json({ planUsage: { totalPercentUsed: 10 } }),
121 ),
122 );
123 }),
124 ),
125 );
126 if (token) expect(limits.windows[0]?.usedPercent).toBe(10);
127 else expect(limits.unavailable?.reason).toBe("unsupported");
128 }
129 }),
130 );
131
132 it.effect("reports failed requests without exposing credentials or response bodies", () =>
133 Effect.gen(function* () {
134 const limits = yield* readCursorUsageLimits(
135 { apiEndpoint: "" },
136 { CURSOR_AUTH_TOKEN: "private-token" },
137 ).pipe(
138 Effect.provideService(
139 HttpClient.HttpClient,
140 HttpClient.make((request) =>
141 Effect.succeed(
142 HttpClientResponse.fromWeb(
143 request,
144 new Response("private response", { status: 401 }),
145 ),
146 ),
147 ),
148 ),
149 );
150 expect(limits.unavailable).toEqual({
151 reason: "probeFailed",
152 message: "Cursor could not read usage limits.",
153 });
154 }),
155 );
156
157 it.effect("exchanges an API key to read its account limits", () =>
158 Effect.gen(function* () {
159 const requests: Array<readonly [string, string | undefined]> = [];
160 const limits = yield* readCursorUsageLimits(
161 { apiEndpoint: "" },
162 { CURSOR_API_KEY: "api-key" },
163 ).pipe(
164 Effect.provideService(
165 HttpClient.HttpClient,
166 HttpClient.make((request) => {
167 requests.push([request.url, request.headers.authorization]);
168 return Effect.succeed(
169 HttpClientResponse.fromWeb(
170 request,
171 Response.json(
172 request.url.endsWith("/auth/exchange_user_api_key")
173 ? { accessToken: "api-key-access-token" }
174 : { planUsage: { totalPercentUsed: 37 } },
175 ),
176 ),
177 );
178 }),
179 ),
180 );
181 expect(requests).toEqual([
182 ["https://api2.cursor.sh/auth/exchange_user_api_key", "Bearer api-key"],
183 [
184 "https://api2.cursor.sh/aiserver.v1.DashboardService/GetCurrentPeriodUsage",
185 "Bearer api-key-access-token",
186 ],
187 ]);
188 expect(limits.windows[0]?.usedPercent).toBe(37);
189 }),
190 );
191
192 it.effect("does not send an API key to a custom endpoint", () =>
193 Effect.gen(function* () {
194 for (const [apiEndpoint, environment] of [
195 ["http://localhost:3000", { CURSOR_API_KEY: "api-key" }],
196 ["", { CURSOR_API_ENDPOINT: "http://localhost:3000", CURSOR_API_KEY: "api-key" }],
197 ["", { CURSOR_API_ENDPOINT: "https://cursor-proxy.example", CURSOR_API_KEY: "api-key" }],
198 ["https://cursor-proxy.example", { CURSOR_API_KEY: "api-key" }],
199 ] as const) {
200 const limits = yield* readCursorUsageLimits({ apiEndpoint }, environment).pipe(
201 Effect.provideService(
202 HttpClient.HttpClient,
203 HttpClient.make(() => Effect.die("must not send an API key to a proxy")),
204 ),
205 );
206 expect(limits.unavailable?.reason).toBe("unsupported");
207 expect(limits.unavailable?.message).toContain("default Cursor endpoint");
208 }
209 }),
210 );
211});