agent-dealer 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/server/dist/adapters/linear-inbox.js +9 -5
- package/bundle/server/dist/capacity/adapter.js +2 -0
- package/bundle/server/dist/capacity/claude-events.js +45 -10
- package/bundle/server/dist/capacity/claude-events.test.js +7 -0
- package/bundle/server/dist/capacity/claude-local-cache.js +788 -0
- package/bundle/server/dist/capacity/claude-local-cache.test.js +520 -0
- package/bundle/server/dist/capacity/codex-app-server.js +100 -10
- package/bundle/server/dist/capacity/codex-app-server.test.js +195 -2
- package/bundle/server/dist/capacity/cursor-individual-credentials.js +425 -0
- package/bundle/server/dist/capacity/cursor-individual-credentials.test.js +369 -0
- package/bundle/server/dist/capacity/cursor-individual.js +1037 -0
- package/bundle/server/dist/capacity/cursor-individual.test.js +1081 -0
- package/bundle/server/dist/capacity/muse-host.js +748 -0
- package/bundle/server/dist/capacity/muse-host.test.js +480 -0
- package/bundle/server/dist/capacity/muse-lifecycle.test.js +97 -0
- package/bundle/server/dist/capacity/muse.js +389 -91
- package/bundle/server/dist/capacity/muse.test.js +100 -61
- package/bundle/server/dist/capacity/service.js +1 -0
- package/bundle/server/dist/coordinator/muse-spawn.js +143 -1
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +27 -0
- package/bundle/server/dist/index.js +17 -2
- package/bundle/server/dist/repository/cursor-individual-billing.js +84 -0
- package/bundle/server/dist/repository/repository-mappings.js +46 -0
- package/bundle/server/dist/repository/repository-mappings.test.js +83 -0
- package/bundle/server/dist/repository/runtime-capacity.js +8 -3
- package/bundle/server/dist/repository/runtime-capacity.test.js +4 -0
- package/bundle/server/dist/routes/cursor-individual-billing.test.js +80 -0
- package/bundle/server/dist/routes/index.js +58 -9
- package/bundle/server/dist/routes/intake-linear.test.js +55 -1
- package/bundle/server/dist/routes/repository-mappings.js +22 -0
- package/bundle/server/dist/routes/repository-mappings.test.js +110 -0
- package/bundle/server/dist/routes/runtime-capacity.test.js +84 -8
- package/bundle/server/dist/runners/muse-code-jsonl.js +7 -1
- package/bundle/server/dist/runners/muse-serve-session.js +313 -0
- package/bundle/server/dist/runners/muse-serve-session.test.js +325 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/codex-YZSpL-SB.png +0 -0
- package/bundle/server/static-ui/assets/index-CYqRh_-S.css +1 -0
- package/bundle/server/static-ui/assets/index-UO4lHZw4.js +60 -0
- package/bundle/server/static-ui/assets/muse-CtdcC6ve.svg +33 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/index.d.ts +6 -6
- package/bundle/shared/dist/linear-intake.d.ts +71 -0
- package/bundle/shared/dist/linear-intake.js +90 -0
- package/bundle/shared/dist/linear-intake.test.js +80 -1
- package/bundle/shared/dist/runtime-capacity.d.ts +89 -0
- package/bundle/shared/dist/runtime-capacity.js +60 -0
- package/bundle/shared/dist/runtime-capacity.test.js +1 -0
- package/bundle/shared/package.json +1 -1
- package/dist/doctor.d.ts +57 -0
- package/dist/doctor.js +211 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +246 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-ChnI0F02.css +0 -1
- package/bundle/server/static-ui/assets/index-CyB1573Q.js +0 -60
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
// packages/server/src/capacity/cursor-individual.test.ts
|
|
2
|
+
//
|
|
3
|
+
// NOT-250: experimental Cursor Individual dashboard adapter — mocked-HTTP
|
|
4
|
+
// coverage only, never the real local login nor the live dashboard. Every
|
|
5
|
+
// test injects a mock fetch (all asserted URLs target the allowlisted mock
|
|
6
|
+
// host `https://www.cursor.com`, which the mock intercepts) and temporary
|
|
7
|
+
// fixture credential files. Fixture payloads follow the brief's
|
|
8
|
+
// usage-summary/current-period shape; nothing here assumes those routes are
|
|
9
|
+
// a supported contract.
|
|
10
|
+
import { test, before, beforeEach, afterEach } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
process.env.AGENT_DEALER_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cursor-indiv-"));
|
|
16
|
+
import { CURSOR_INDIVIDUAL_ALLOWED_ORIGINS, CURSOR_INDIVIDUAL_DEFAULT_ORIGIN, CURSOR_INDIVIDUAL_MAX_BODY_BYTES, CURSOR_INDIVIDUAL_OPT_IN_ENV, CURSOR_INDIVIDUAL_OPT_IN_VALUE, CURSOR_INDIVIDUAL_RUNTIME, CURSOR_INDIVIDUAL_USAGE_PATHS, cursorIndividualCapacityAdapter, cursorIndividualFailureReason, cursorIndividualPayloadToReadings, getCursorIndividualBillingSnapshot, ingestCursorIndividualObservation, isCursorIndividualExperimentalEnabled, readBoundedText, readCursorIndividualBilling, refreshCursorIndividualBilling, refreshCursorIndividualBillingIfStale, refreshCursorIndividualCapacityIfStale, resetCursorIndividualPollStateForTests, } from "./cursor-individual.js";
|
|
17
|
+
import Database from "better-sqlite3";
|
|
18
|
+
import { CURSOR_DESKTOP_ACCESS_TOKEN_KEY, CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV, CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV, CURSOR_INDIVIDUAL_HOME_ENV, cursorIndividualAuthHeader, } from "./cursor-individual-credentials.js";
|
|
19
|
+
const { migrate } = await import("../db/index.js");
|
|
20
|
+
const { clearCursorIndividualBilling, readCursorIndividualBillingRow, } = await import("../repository/cursor-individual-billing.js");
|
|
21
|
+
const { clearAllCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
22
|
+
const { clearAllRuntimeAvailability, runtimeAvailability, } = await import("../repository/runtime-availability.js");
|
|
23
|
+
const { getRuntimeCapacitySnapshot } = await import("./service.js");
|
|
24
|
+
const { normalizeAdapterWindow, DEFAULT_STALE_AFTER_MS } = await import("./adapter.js");
|
|
25
|
+
const MOCK_BASE = "https://www.cursor.com";
|
|
26
|
+
const SECRET = "fixture-individual-secret-xyz789";
|
|
27
|
+
const USER_ID = "user_fixture_individual_abc";
|
|
28
|
+
let credDir;
|
|
29
|
+
let savedOptIn;
|
|
30
|
+
let savedCredFile;
|
|
31
|
+
let savedRefresh;
|
|
32
|
+
let savedDesktopFile;
|
|
33
|
+
let savedHome;
|
|
34
|
+
before(() => {
|
|
35
|
+
migrate();
|
|
36
|
+
});
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
credDir = fs.mkdtempSync(path.join(os.tmpdir(), "dealer-cursor-indiv-creds-"));
|
|
39
|
+
savedOptIn = process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV];
|
|
40
|
+
savedCredFile = process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV];
|
|
41
|
+
savedRefresh = process.env.AGENT_DEALER_CURSOR_INDIVIDUAL_REFRESH;
|
|
42
|
+
savedDesktopFile = process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV];
|
|
43
|
+
savedHome = process.env[CURSOR_INDIVIDUAL_HOME_ENV];
|
|
44
|
+
delete process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV];
|
|
45
|
+
delete process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV];
|
|
46
|
+
delete process.env.AGENT_DEALER_CURSOR_INDIVIDUAL_REFRESH;
|
|
47
|
+
delete process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV];
|
|
48
|
+
delete process.env[CURSOR_INDIVIDUAL_HOME_ENV];
|
|
49
|
+
clearCursorIndividualBilling();
|
|
50
|
+
clearAllCapacitySnapshots();
|
|
51
|
+
clearAllRuntimeAvailability();
|
|
52
|
+
resetCursorIndividualPollStateForTests();
|
|
53
|
+
});
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
if (savedOptIn === undefined)
|
|
56
|
+
delete process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV];
|
|
57
|
+
else
|
|
58
|
+
process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV] = savedOptIn;
|
|
59
|
+
if (savedCredFile === undefined)
|
|
60
|
+
delete process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV];
|
|
61
|
+
else
|
|
62
|
+
process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = savedCredFile;
|
|
63
|
+
if (savedRefresh === undefined)
|
|
64
|
+
delete process.env.AGENT_DEALER_CURSOR_INDIVIDUAL_REFRESH;
|
|
65
|
+
else
|
|
66
|
+
process.env.AGENT_DEALER_CURSOR_INDIVIDUAL_REFRESH = savedRefresh;
|
|
67
|
+
if (savedDesktopFile === undefined)
|
|
68
|
+
delete process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV];
|
|
69
|
+
else
|
|
70
|
+
process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV] = savedDesktopFile;
|
|
71
|
+
if (savedHome === undefined)
|
|
72
|
+
delete process.env[CURSOR_INDIVIDUAL_HOME_ENV];
|
|
73
|
+
else
|
|
74
|
+
process.env[CURSOR_INDIVIDUAL_HOME_ENV] = savedHome;
|
|
75
|
+
fs.rmSync(credDir, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
function enable() {
|
|
78
|
+
process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV] = CURSOR_INDIVIDUAL_OPT_IN_VALUE;
|
|
79
|
+
}
|
|
80
|
+
function fixtureCredential(body = { token: SECRET, userId: USER_ID }) {
|
|
81
|
+
const file = path.join(credDir, "auth.json");
|
|
82
|
+
fs.writeFileSync(file, typeof body === "string" ? body : JSON.stringify(body));
|
|
83
|
+
process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = file;
|
|
84
|
+
}
|
|
85
|
+
/** Mock fetch keyed by path; records every call for assertion. */
|
|
86
|
+
function mockFetch(routes, calls = []) {
|
|
87
|
+
let count = 0;
|
|
88
|
+
return async (url, init) => {
|
|
89
|
+
count += 1;
|
|
90
|
+
if (init.signal.aborted)
|
|
91
|
+
throw new Error("aborted");
|
|
92
|
+
calls.push({ url, method: init.method, headers: init.headers });
|
|
93
|
+
const u = new URL(url);
|
|
94
|
+
const route = routes[u.pathname];
|
|
95
|
+
const resolved = typeof route === "function" ? route(count) : route;
|
|
96
|
+
if (resolved instanceof Error)
|
|
97
|
+
throw resolved;
|
|
98
|
+
if (!resolved)
|
|
99
|
+
return { status: 404, headers: {}, text: async () => "not found" };
|
|
100
|
+
const text = typeof resolved.body === "string" ? resolved.body : JSON.stringify(resolved.body ?? {});
|
|
101
|
+
const headers = {};
|
|
102
|
+
for (const [k, v] of Object.entries(resolved.headers ?? {}))
|
|
103
|
+
headers[k.toLowerCase()] = v;
|
|
104
|
+
return { status: resolved.status, headers, text: async () => text };
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function usagePayload(nowMs, over = {}) {
|
|
108
|
+
return {
|
|
109
|
+
cycleLabel: "September 2026",
|
|
110
|
+
cycleStart: "2026-09-01T00:00:00.000Z",
|
|
111
|
+
cycleEnd: new Date(nowMs + 8 * 24 * 3600_000).toISOString(),
|
|
112
|
+
usageValue: 7.5,
|
|
113
|
+
usageUnit: "USD",
|
|
114
|
+
usedPercent: 37.5,
|
|
115
|
+
...over,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function okRoutes(nowMs) {
|
|
119
|
+
return { [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: usagePayload(nowMs) } };
|
|
120
|
+
}
|
|
121
|
+
test("disabled reads N/A with no credential or endpoint access", async () => {
|
|
122
|
+
delete process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV];
|
|
123
|
+
assert.equal(isCursorIndividualExperimentalEnabled(), false);
|
|
124
|
+
fixtureCredential();
|
|
125
|
+
const calls = [];
|
|
126
|
+
const loadCredential = () => {
|
|
127
|
+
throw new Error("credential must not be read while disabled");
|
|
128
|
+
};
|
|
129
|
+
const fetchImpl = async () => {
|
|
130
|
+
throw new Error("HTTP must not be attempted while disabled");
|
|
131
|
+
};
|
|
132
|
+
const obs = await readCursorIndividualBilling({
|
|
133
|
+
baseUrl: MOCK_BASE,
|
|
134
|
+
fetchImpl,
|
|
135
|
+
loadCredential,
|
|
136
|
+
});
|
|
137
|
+
assert.equal(obs.failure?.kind, "disabled");
|
|
138
|
+
assert.equal(obs.enabled, false);
|
|
139
|
+
assert.equal(obs.configured, false);
|
|
140
|
+
assert.equal(obs.billing.source, "unavailable");
|
|
141
|
+
assert.equal(obs.billing.unavailableReason, "missing");
|
|
142
|
+
void calls;
|
|
143
|
+
// The adapter degrades to an explicit unavailable reading, same conditions.
|
|
144
|
+
const result = await cursorIndividualCapacityAdapter({
|
|
145
|
+
baseUrl: MOCK_BASE,
|
|
146
|
+
fetchImpl,
|
|
147
|
+
loadCredential,
|
|
148
|
+
}).read();
|
|
149
|
+
assert.equal(result.windows.length, 0);
|
|
150
|
+
assert.equal(result.unavailable.length, 1);
|
|
151
|
+
assert.equal(result.unavailable[0].windowKey, "billing_cycle");
|
|
152
|
+
assert.equal(result.unavailable[0].reason, "missing");
|
|
153
|
+
// Stale refreshes are strict no-ops while disabled.
|
|
154
|
+
await refreshCursorIndividualBillingIfStale();
|
|
155
|
+
await refreshCursorIndividualCapacityIfStale();
|
|
156
|
+
assert.equal(readCursorIndividualBillingRow(), null);
|
|
157
|
+
// And the serve path reads disabled without touching the database.
|
|
158
|
+
const snap = await getCursorIndividualBillingSnapshot();
|
|
159
|
+
assert.equal(snap.enabled, false);
|
|
160
|
+
assert.equal(snap.configured, false);
|
|
161
|
+
assert.equal(snap.unavailableReason, "missing");
|
|
162
|
+
});
|
|
163
|
+
test("opt-in with fixture credential/response reports cycle label/reset/remaining", async () => {
|
|
164
|
+
enable();
|
|
165
|
+
fixtureCredential();
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const calls = [];
|
|
168
|
+
const obs = await readCursorIndividualBilling({
|
|
169
|
+
baseUrl: MOCK_BASE,
|
|
170
|
+
fetchImpl: mockFetch(okRoutes(now), calls),
|
|
171
|
+
nowMs: now,
|
|
172
|
+
});
|
|
173
|
+
assert.equal(obs.failure, null);
|
|
174
|
+
assert.equal(obs.enabled, true);
|
|
175
|
+
assert.equal(obs.configured, true);
|
|
176
|
+
assert.equal(obs.viaSupportedApi, false);
|
|
177
|
+
assert.equal(obs.billing.cycleLabel, "September 2026");
|
|
178
|
+
assert.equal(obs.billing.cycleStart, "2026-09-01T00:00:00.000Z");
|
|
179
|
+
assert.equal(obs.billing.cycleEnd, new Date(now + 8 * 24 * 3600_000).toISOString());
|
|
180
|
+
assert.equal(obs.billing.usageValue, 7.5);
|
|
181
|
+
assert.equal(obs.billing.usageUnit, "USD");
|
|
182
|
+
// 37.5% used → 62.5 remaining — no unsupported windows guessed.
|
|
183
|
+
assert.equal(obs.billing.remainingPercent, 62.5);
|
|
184
|
+
assert.equal(obs.billing.source, "experimental_api");
|
|
185
|
+
assert.equal(obs.billing.unavailableReason, null);
|
|
186
|
+
assert.equal(obs.evidenceRef, "cursor-dashboard:usage-summary/current-period");
|
|
187
|
+
// Allowlisted origin, GET, credential header-only (never in URL/body).
|
|
188
|
+
assert.equal(calls.length, 1);
|
|
189
|
+
assert.ok(calls[0].url.startsWith(MOCK_BASE), `mock host only, got ${calls[0].url}`);
|
|
190
|
+
assert.equal(calls[0].method, "GET");
|
|
191
|
+
assert.equal(calls[0].headers.Cookie, cursorIndividualAuthHeader(USER_ID, SECRET));
|
|
192
|
+
const url = new URL(calls[0].url);
|
|
193
|
+
assert.ok(!url.search.includes(SECRET) && !url.pathname.includes(SECRET));
|
|
194
|
+
// The raw token appears nowhere outside the in-memory auth header.
|
|
195
|
+
const serialized = JSON.stringify({ billing: obs.billing, failure: obs.failure });
|
|
196
|
+
assert.ok(!serialized.includes(SECRET), "raw token must not appear in the observation");
|
|
197
|
+
});
|
|
198
|
+
test("adapter maps billing to one billing-cycle window without invented durations", async () => {
|
|
199
|
+
enable();
|
|
200
|
+
fixtureCredential();
|
|
201
|
+
const now = Date.now();
|
|
202
|
+
const result = await cursorIndividualCapacityAdapter({
|
|
203
|
+
baseUrl: MOCK_BASE,
|
|
204
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
205
|
+
nowMs: now,
|
|
206
|
+
}).read(now);
|
|
207
|
+
assert.equal(result.runtime, "cursor_local");
|
|
208
|
+
assert.equal(result.unavailable.length, 0);
|
|
209
|
+
assert.equal(result.windows.length, 1);
|
|
210
|
+
const w = result.windows[0];
|
|
211
|
+
assert.equal(w.windowKey, "billing_cycle");
|
|
212
|
+
assert.equal(w.providerBucket, "individual");
|
|
213
|
+
assert.equal(w.durationMinutes, null);
|
|
214
|
+
assert.equal(w.source, "experimental_api");
|
|
215
|
+
assert.equal(w.resetAt, new Date(now + 8 * 24 * 3600_000).toISOString());
|
|
216
|
+
// Normalization keeps the provider label (no 5H/1W invention) and recovers
|
|
217
|
+
// the remaining percent through the shared used scale.
|
|
218
|
+
const normalized = normalizeAdapterWindow("cursor_local", w, now);
|
|
219
|
+
assert.equal(normalized.displayLabel, "billing cycle");
|
|
220
|
+
assert.equal(normalized.remainingPercent, 62.5);
|
|
221
|
+
assert.equal(normalized.unavailableReason, null);
|
|
222
|
+
});
|
|
223
|
+
test("supported surface is preferred and skips credential + dashboard", async () => {
|
|
224
|
+
enable();
|
|
225
|
+
fixtureCredential();
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
const fetchImpl = async () => {
|
|
228
|
+
throw new Error("dashboard must not be touched when a supported API answers");
|
|
229
|
+
};
|
|
230
|
+
const obs = await readCursorIndividualBilling({
|
|
231
|
+
baseUrl: MOCK_BASE,
|
|
232
|
+
fetchImpl,
|
|
233
|
+
loadCredential: () => {
|
|
234
|
+
throw new Error("credential must not be read when a supported API answers");
|
|
235
|
+
},
|
|
236
|
+
supportedReader: () => ({
|
|
237
|
+
cycleLabel: "September 2026",
|
|
238
|
+
cycleEnd: new Date(now + 8 * 24 * 3600_000).toISOString(),
|
|
239
|
+
remainingPercent: 80,
|
|
240
|
+
}),
|
|
241
|
+
nowMs: now,
|
|
242
|
+
});
|
|
243
|
+
assert.equal(obs.failure, null);
|
|
244
|
+
assert.equal(obs.viaSupportedApi, true);
|
|
245
|
+
assert.equal(obs.billing.source, "supported_protocol");
|
|
246
|
+
assert.equal(obs.billing.cycleLabel, "September 2026");
|
|
247
|
+
assert.equal(obs.billing.remainingPercent, 80);
|
|
248
|
+
});
|
|
249
|
+
test("absent credential performs no HTTP and reads missing/unconfigured", async () => {
|
|
250
|
+
enable();
|
|
251
|
+
process.env[CURSOR_INDIVIDUAL_CREDENTIAL_FILE_ENV] = path.join(credDir, "does-not-exist.json");
|
|
252
|
+
const calls = [];
|
|
253
|
+
const obs = await readCursorIndividualBilling({
|
|
254
|
+
baseUrl: MOCK_BASE,
|
|
255
|
+
fetchImpl: mockFetch(okRoutes(Date.now()), calls),
|
|
256
|
+
});
|
|
257
|
+
assert.equal(obs.failure?.kind, "absent-credential");
|
|
258
|
+
assert.equal(obs.configured, false);
|
|
259
|
+
assert.equal(calls.length, 0);
|
|
260
|
+
await ingestCursorIndividualObservation(obs);
|
|
261
|
+
assert.equal(readCursorIndividualBillingRow(), null);
|
|
262
|
+
const snap = await getCursorIndividualBillingSnapshot();
|
|
263
|
+
assert.equal(snap.enabled, true);
|
|
264
|
+
assert.equal(snap.configured, false);
|
|
265
|
+
assert.equal(snap.unavailableReason, "missing");
|
|
266
|
+
});
|
|
267
|
+
test("changed credential format reads unparsable", async () => {
|
|
268
|
+
enable();
|
|
269
|
+
fixtureCredential({ brandNewShape: true });
|
|
270
|
+
const calls = [];
|
|
271
|
+
const obs = await readCursorIndividualBilling({
|
|
272
|
+
baseUrl: MOCK_BASE,
|
|
273
|
+
fetchImpl: mockFetch(okRoutes(Date.now()), calls),
|
|
274
|
+
});
|
|
275
|
+
assert.equal(obs.failure?.kind, "bad-credential");
|
|
276
|
+
assert.equal(obs.billing.unavailableReason, "unparsable");
|
|
277
|
+
assert.equal(calls.length, 0);
|
|
278
|
+
assert.equal(cursorIndividualFailureReason("bad-credential"), "unparsable");
|
|
279
|
+
});
|
|
280
|
+
test("endpoint drift falls through to the next candidate path", async () => {
|
|
281
|
+
enable();
|
|
282
|
+
fixtureCredential();
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
const calls = [];
|
|
285
|
+
const obs = await readCursorIndividualBilling({
|
|
286
|
+
baseUrl: MOCK_BASE,
|
|
287
|
+
fetchImpl: mockFetch({
|
|
288
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 404, body: {} },
|
|
289
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[1]]: { status: 200, body: usagePayload(now) },
|
|
290
|
+
}, calls),
|
|
291
|
+
nowMs: now,
|
|
292
|
+
});
|
|
293
|
+
assert.equal(obs.failure, null);
|
|
294
|
+
assert.equal(obs.billing.cycleLabel, "September 2026");
|
|
295
|
+
assert.equal(obs.billing.remainingPercent, 62.5);
|
|
296
|
+
assert.equal(calls.length, 2);
|
|
297
|
+
});
|
|
298
|
+
test("a 200 HTML SPA shell on the first candidate (not a 404) still falls through", async () => {
|
|
299
|
+
enable();
|
|
300
|
+
fixtureCredential();
|
|
301
|
+
const now = Date.now();
|
|
302
|
+
const calls = [];
|
|
303
|
+
const obs = await readCursorIndividualBilling({
|
|
304
|
+
baseUrl: MOCK_BASE,
|
|
305
|
+
fetchImpl: mockFetch({
|
|
306
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: {
|
|
307
|
+
status: 200,
|
|
308
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
309
|
+
body: "<!doctype html><html><body>app shell</body></html>",
|
|
310
|
+
},
|
|
311
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[1]]: { status: 200, body: usagePayload(now) },
|
|
312
|
+
}, calls),
|
|
313
|
+
nowMs: now,
|
|
314
|
+
});
|
|
315
|
+
assert.equal(obs.failure, null);
|
|
316
|
+
assert.equal(obs.billing.cycleLabel, "September 2026");
|
|
317
|
+
assert.equal(obs.billing.remainingPercent, 62.5);
|
|
318
|
+
assert.equal(calls.length, 2);
|
|
319
|
+
});
|
|
320
|
+
test("a 200 HTML SPA shell on every candidate reads unparsable, not missing", async () => {
|
|
321
|
+
enable();
|
|
322
|
+
fixtureCredential();
|
|
323
|
+
const obs = await readCursorIndividualBilling({
|
|
324
|
+
baseUrl: MOCK_BASE,
|
|
325
|
+
fetchImpl: mockFetch({
|
|
326
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: {
|
|
327
|
+
status: 200,
|
|
328
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
329
|
+
body: "<!doctype html><html><body>app shell</body></html>",
|
|
330
|
+
},
|
|
331
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[1]]: {
|
|
332
|
+
status: 200,
|
|
333
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
334
|
+
body: "<!doctype html><html><body>app shell</body></html>",
|
|
335
|
+
},
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
assert.equal(obs.failure?.kind, "malformed");
|
|
339
|
+
assert.equal(obs.billing.unavailableReason, "unparsable");
|
|
340
|
+
});
|
|
341
|
+
test("failure modes return explicit N/A without affecting runtime health", async () => {
|
|
342
|
+
enable();
|
|
343
|
+
fixtureCredential();
|
|
344
|
+
async function expectOutcome(routes, kind, reason) {
|
|
345
|
+
clearCursorIndividualBilling();
|
|
346
|
+
clearAllRuntimeAvailability();
|
|
347
|
+
const obs = await readCursorIndividualBilling({
|
|
348
|
+
baseUrl: MOCK_BASE,
|
|
349
|
+
fetchImpl: mockFetch(routes),
|
|
350
|
+
});
|
|
351
|
+
assert.equal(obs.failure?.kind, kind);
|
|
352
|
+
assert.equal(obs.billing.unavailableReason, reason);
|
|
353
|
+
await ingestCursorIndividualObservation(obs);
|
|
354
|
+
assert.equal(runtimeAvailability("cursor_local", Date.now()).available, true);
|
|
355
|
+
}
|
|
356
|
+
const ep = CURSOR_INDIVIDUAL_USAGE_PATHS[0];
|
|
357
|
+
await expectOutcome({ [ep]: { status: 401, body: {} } }, "forbidden", "missing");
|
|
358
|
+
await expectOutcome({ [ep]: { status: 403, body: {} } }, "forbidden", "missing");
|
|
359
|
+
await expectOutcome({ [ep]: { status: 429, body: {} } }, "rate-limited", "missing");
|
|
360
|
+
await expectOutcome({ [ep]: { status: 500, body: {} } }, "unavailable", "missing");
|
|
361
|
+
await expectOutcome({ [ep]: new Error("socket hang up") }, "unavailable", "missing");
|
|
362
|
+
// Both candidate paths absent: endpoint drift reads missing, not a parse verdict.
|
|
363
|
+
await expectOutcome({}, "unavailable", "missing");
|
|
364
|
+
// Malformed data reads unparsable.
|
|
365
|
+
await expectOutcome({ [ep]: { status: 200, body: { nonsense: true } } }, "malformed", "unparsable");
|
|
366
|
+
await expectOutcome({ [ep]: { status: 200, body: "not-json{{{" } }, "malformed", "unparsable");
|
|
367
|
+
await expectOutcome({ [ep]: { status: 200, body: "x".repeat(CURSOR_INDIVIDUAL_MAX_BODY_BYTES + 1) } }, "malformed", "unparsable");
|
|
368
|
+
});
|
|
369
|
+
test("timeout degrades to an explicit unavailable reason", async () => {
|
|
370
|
+
enable();
|
|
371
|
+
fixtureCredential();
|
|
372
|
+
const hanging = async (_url, init) => new Promise((_resolve, reject) => {
|
|
373
|
+
if (init.signal.aborted) {
|
|
374
|
+
reject(new Error("aborted"));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
init.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
378
|
+
});
|
|
379
|
+
const obs = await readCursorIndividualBilling({
|
|
380
|
+
baseUrl: MOCK_BASE,
|
|
381
|
+
fetchImpl: hanging,
|
|
382
|
+
timeoutMs: 20,
|
|
383
|
+
});
|
|
384
|
+
assert.equal(obs.failure?.kind, "unavailable");
|
|
385
|
+
assert.equal(obs.billing.unavailableReason, "missing");
|
|
386
|
+
});
|
|
387
|
+
test("the same deadline also covers a stalled body read, not just the header response", async () => {
|
|
388
|
+
enable();
|
|
389
|
+
fixtureCredential();
|
|
390
|
+
// Headers arrive immediately (200), but the body stream never delivers —
|
|
391
|
+
// the SAME per-hop timeout must still fire and abort it. Before the fix,
|
|
392
|
+
// the timer was cleared right after headers arrived, so a server that
|
|
393
|
+
// sent headers and then stalled left the request pending indefinitely.
|
|
394
|
+
const stallingBody = async (_url, init) => ({
|
|
395
|
+
status: 200,
|
|
396
|
+
headers: {},
|
|
397
|
+
text: () => new Promise((_resolve, reject) => {
|
|
398
|
+
init.signal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
|
|
399
|
+
}),
|
|
400
|
+
});
|
|
401
|
+
const started = Date.now();
|
|
402
|
+
const obs = await readCursorIndividualBilling({
|
|
403
|
+
baseUrl: MOCK_BASE,
|
|
404
|
+
fetchImpl: stallingBody,
|
|
405
|
+
timeoutMs: 20,
|
|
406
|
+
});
|
|
407
|
+
assert.ok(Date.now() - started < 5000, "the stalled body read must be aborted, not hang");
|
|
408
|
+
assert.equal(obs.failure?.kind, "unavailable");
|
|
409
|
+
assert.equal(obs.billing.unavailableReason, "missing");
|
|
410
|
+
});
|
|
411
|
+
test("unsafe redirects are rejected before the credential travels", async () => {
|
|
412
|
+
enable();
|
|
413
|
+
fixtureCredential();
|
|
414
|
+
const calls = [];
|
|
415
|
+
const obs = await readCursorIndividualBilling({
|
|
416
|
+
baseUrl: MOCK_BASE,
|
|
417
|
+
fetchImpl: mockFetch({
|
|
418
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: {
|
|
419
|
+
status: 302,
|
|
420
|
+
headers: { location: "https://evil.example/capture" },
|
|
421
|
+
body: {},
|
|
422
|
+
},
|
|
423
|
+
}, calls),
|
|
424
|
+
});
|
|
425
|
+
assert.equal(obs.failure?.kind, "unsafe-redirect");
|
|
426
|
+
assert.equal(obs.billing.unavailableReason, "missing");
|
|
427
|
+
// Exactly one request left the client — nothing followed the evil Location.
|
|
428
|
+
assert.equal(calls.length, 1);
|
|
429
|
+
assert.ok(!calls.some((c) => c.url.includes("evil.example")));
|
|
430
|
+
});
|
|
431
|
+
test("same-allowlist redirects are followed", async () => {
|
|
432
|
+
enable();
|
|
433
|
+
fixtureCredential();
|
|
434
|
+
const now = Date.now();
|
|
435
|
+
const calls = [];
|
|
436
|
+
const obs = await readCursorIndividualBilling({
|
|
437
|
+
baseUrl: MOCK_BASE,
|
|
438
|
+
fetchImpl: mockFetch({
|
|
439
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: {
|
|
440
|
+
status: 302,
|
|
441
|
+
headers: { location: "https://api.cursor.com/api/usage-summary" },
|
|
442
|
+
body: {},
|
|
443
|
+
},
|
|
444
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[1]]: { status: 200, body: usagePayload(now) },
|
|
445
|
+
}, calls),
|
|
446
|
+
nowMs: now,
|
|
447
|
+
});
|
|
448
|
+
// The redirect target answers under the other allowlisted origin.
|
|
449
|
+
assert.equal(obs.failure, null);
|
|
450
|
+
assert.equal(obs.billing.cycleLabel, "September 2026");
|
|
451
|
+
});
|
|
452
|
+
test("the default origin is the bare apex domain, and www's canonical redirect to it succeeds", async () => {
|
|
453
|
+
// www.cursor.com canonicalizes to cursor.com — that redirect target must
|
|
454
|
+
// itself be allowlisted, or the adapter would reject its own default.
|
|
455
|
+
assert.equal(CURSOR_INDIVIDUAL_DEFAULT_ORIGIN, "https://cursor.com");
|
|
456
|
+
assert.ok(CURSOR_INDIVIDUAL_ALLOWED_ORIGINS.includes("https://cursor.com"));
|
|
457
|
+
enable();
|
|
458
|
+
fixtureCredential();
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
const obs = await readCursorIndividualBilling({
|
|
461
|
+
baseUrl: MOCK_BASE, // https://www.cursor.com
|
|
462
|
+
fetchImpl: mockFetch({
|
|
463
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: (count) => count === 1
|
|
464
|
+
? { status: 301, headers: { location: `https://cursor.com${CURSOR_INDIVIDUAL_USAGE_PATHS[0]}` }, body: {} }
|
|
465
|
+
: { status: 200, body: usagePayload(now) },
|
|
466
|
+
}),
|
|
467
|
+
nowMs: now,
|
|
468
|
+
});
|
|
469
|
+
assert.equal(obs.failure, null);
|
|
470
|
+
assert.equal(obs.billing.cycleLabel, "September 2026");
|
|
471
|
+
});
|
|
472
|
+
test("non-allowlisted base URLs are rejected without any HTTP", async () => {
|
|
473
|
+
enable();
|
|
474
|
+
fixtureCredential();
|
|
475
|
+
const calls = [];
|
|
476
|
+
const obs = await readCursorIndividualBilling({
|
|
477
|
+
baseUrl: "https://evil.example",
|
|
478
|
+
fetchImpl: mockFetch({}, calls),
|
|
479
|
+
});
|
|
480
|
+
assert.equal(obs.failure?.kind, "unsafe-redirect");
|
|
481
|
+
assert.equal(calls.length, 0);
|
|
482
|
+
});
|
|
483
|
+
test("malformed payloads persist as N/A; transient failures keep last-known values", async () => {
|
|
484
|
+
enable();
|
|
485
|
+
fixtureCredential();
|
|
486
|
+
const now = Date.now();
|
|
487
|
+
const good = {
|
|
488
|
+
baseUrl: MOCK_BASE,
|
|
489
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
490
|
+
nowMs: now,
|
|
491
|
+
};
|
|
492
|
+
await refreshCursorIndividualBilling(good);
|
|
493
|
+
const stored = readCursorIndividualBillingRow();
|
|
494
|
+
assert.equal(stored?.cycleLabel, "September 2026");
|
|
495
|
+
assert.equal(stored?.remainingPercent, 62.5);
|
|
496
|
+
assert.equal(stored?.source, "experimental_api");
|
|
497
|
+
assert.ok(!JSON.stringify(stored).includes(SECRET), "stored row must not carry the token");
|
|
498
|
+
let snap = await getCursorIndividualBillingSnapshot(now);
|
|
499
|
+
assert.equal(snap.enabled, true);
|
|
500
|
+
assert.equal(snap.configured, true);
|
|
501
|
+
assert.equal(snap.unavailableReason, null);
|
|
502
|
+
assert.equal(snap.remainingPercent, 62.5);
|
|
503
|
+
assert.ok(!JSON.stringify(snap).includes(SECRET), "browser response must not carry the token");
|
|
504
|
+
// A later 500 keeps last-known values serving.
|
|
505
|
+
await refreshCursorIndividualBilling({
|
|
506
|
+
baseUrl: MOCK_BASE,
|
|
507
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 500, body: {} } }),
|
|
508
|
+
nowMs: now + 60_000,
|
|
509
|
+
});
|
|
510
|
+
snap = await getCursorIndividualBillingSnapshot(now + 60_000);
|
|
511
|
+
assert.equal(snap.unavailableReason, null);
|
|
512
|
+
assert.equal(snap.remainingPercent, 62.5);
|
|
513
|
+
// Malformed data overwrites with an explicit unparsable N/A.
|
|
514
|
+
await refreshCursorIndividualBilling({
|
|
515
|
+
baseUrl: MOCK_BASE,
|
|
516
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: { nope: 1 } } }),
|
|
517
|
+
nowMs: now + 120_000,
|
|
518
|
+
});
|
|
519
|
+
snap = await getCursorIndividualBillingSnapshot(now + 120_000);
|
|
520
|
+
assert.equal(snap.unavailableReason, "unparsable");
|
|
521
|
+
assert.equal(snap.remainingPercent, null);
|
|
522
|
+
});
|
|
523
|
+
test("a bad-credential response overwrites the stored billing row too — the other non-transient sibling (billing table)", async () => {
|
|
524
|
+
// ingestCursorIndividualObservation() has the same two-kind non-transient
|
|
525
|
+
// gate (malformed, bad-credential) as pollCursorIndividualShared's
|
|
526
|
+
// capacity-strip write — malformed-overwrites-billing is covered above;
|
|
527
|
+
// cover the bad-credential sibling at this (billing-table) layer too.
|
|
528
|
+
enable();
|
|
529
|
+
fixtureCredential();
|
|
530
|
+
const now = Date.now();
|
|
531
|
+
await refreshCursorIndividualBilling({
|
|
532
|
+
baseUrl: MOCK_BASE,
|
|
533
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
534
|
+
nowMs: now,
|
|
535
|
+
});
|
|
536
|
+
assert.equal(readCursorIndividualBillingRow()?.remainingPercent, 62.5);
|
|
537
|
+
// The credential file changes shape before the next refresh.
|
|
538
|
+
fixtureCredential({ brandNewShape: true });
|
|
539
|
+
await refreshCursorIndividualBilling({
|
|
540
|
+
baseUrl: MOCK_BASE,
|
|
541
|
+
fetchImpl: mockFetch(okRoutes(now + 60_000)),
|
|
542
|
+
nowMs: now + 60_000,
|
|
543
|
+
});
|
|
544
|
+
// Assert against the STORED ROW, not getCursorIndividualBillingSnapshot():
|
|
545
|
+
// the snapshot function has its own independent live credential re-check
|
|
546
|
+
// that reads `unparsable` whenever the CURRENT file is bad, regardless of
|
|
547
|
+
// whether ingestCursorIndividualObservation() actually overwrote the row —
|
|
548
|
+
// asserting through it would pass even if that gate's bad-credential
|
|
549
|
+
// branch were deleted.
|
|
550
|
+
const row = readCursorIndividualBillingRow();
|
|
551
|
+
assert.equal(row?.unavailableReason, "unparsable");
|
|
552
|
+
assert.equal(row?.remainingPercent, null);
|
|
553
|
+
});
|
|
554
|
+
test("stale, expired, and past-cycle snapshots read N/A with distinct reasons", async () => {
|
|
555
|
+
enable();
|
|
556
|
+
fixtureCredential();
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
const { writeCursorIndividualBillingRow } = await import("../repository/cursor-individual-billing.js");
|
|
559
|
+
const fresh = {
|
|
560
|
+
cycleLabel: "September 2026",
|
|
561
|
+
cycleStart: "2026-09-01T00:00:00.000Z",
|
|
562
|
+
cycleEnd: new Date(now + 8 * 24 * 3600_000).toISOString(),
|
|
563
|
+
usageValue: 7.5,
|
|
564
|
+
usageUnit: "USD",
|
|
565
|
+
remainingPercent: 62.5,
|
|
566
|
+
source: "experimental_api",
|
|
567
|
+
unavailableReason: null,
|
|
568
|
+
evidenceRef: "cursor-dashboard:usage-summary/current-period",
|
|
569
|
+
};
|
|
570
|
+
clearCursorIndividualBilling();
|
|
571
|
+
writeCursorIndividualBillingRow({
|
|
572
|
+
...fresh,
|
|
573
|
+
observedAt: new Date(now - 30 * 60_000).toISOString(),
|
|
574
|
+
freshUntil: new Date(now - 15 * 60_000).toISOString(),
|
|
575
|
+
expiresAt: new Date(now + 30 * 60_000).toISOString(),
|
|
576
|
+
});
|
|
577
|
+
let snap = await getCursorIndividualBillingSnapshot(now);
|
|
578
|
+
assert.equal(snap.remainingPercent, null);
|
|
579
|
+
assert.equal(snap.unavailableReason, "stale");
|
|
580
|
+
writeCursorIndividualBillingRow({
|
|
581
|
+
...fresh,
|
|
582
|
+
observedAt: new Date(now - 2 * 3600_000).toISOString(),
|
|
583
|
+
freshUntil: new Date(now - 3600_000).toISOString(),
|
|
584
|
+
expiresAt: new Date(now - 1000).toISOString(),
|
|
585
|
+
});
|
|
586
|
+
snap = await getCursorIndividualBillingSnapshot(now);
|
|
587
|
+
assert.equal(snap.unavailableReason, "expired");
|
|
588
|
+
// A finished billing cycle is never presented as current capacity.
|
|
589
|
+
writeCursorIndividualBillingRow({
|
|
590
|
+
...fresh,
|
|
591
|
+
cycleEnd: new Date(now - 1000).toISOString(),
|
|
592
|
+
observedAt: new Date(now - 60_000).toISOString(),
|
|
593
|
+
freshUntil: new Date(now + 600_000).toISOString(),
|
|
594
|
+
expiresAt: new Date(now + 3600_000).toISOString(),
|
|
595
|
+
});
|
|
596
|
+
snap = await getCursorIndividualBillingSnapshot(now);
|
|
597
|
+
assert.equal(snap.remainingPercent, null);
|
|
598
|
+
assert.equal(snap.unavailableReason, "expired");
|
|
599
|
+
});
|
|
600
|
+
test("capacity-strip refresh ingests the billing-cycle window; disabled writes nothing", async () => {
|
|
601
|
+
enable();
|
|
602
|
+
fixtureCredential();
|
|
603
|
+
const now = Date.now();
|
|
604
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
605
|
+
baseUrl: MOCK_BASE,
|
|
606
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
607
|
+
nowMs: now,
|
|
608
|
+
});
|
|
609
|
+
let snap = getRuntimeCapacitySnapshot(now);
|
|
610
|
+
const cursor = snap.runtimes.find((r) => r.runtime === "cursor_local");
|
|
611
|
+
assert.ok(cursor, "cursor_local entry exists from the stored window");
|
|
612
|
+
assert.equal(cursor.windows.length, 1);
|
|
613
|
+
assert.equal(cursor.windows[0].windowKey, "billing_cycle");
|
|
614
|
+
assert.equal(cursor.windows[0].displayLabel, "billing cycle");
|
|
615
|
+
assert.equal(cursor.windows[0].remainingPercent, 62.5);
|
|
616
|
+
assert.equal(cursor.windows[0].source, "experimental_api");
|
|
617
|
+
assert.equal(cursor.unavailableReason, null);
|
|
618
|
+
assert.ok(!JSON.stringify(snap).includes(SECRET));
|
|
619
|
+
// Disabled: strict no-op, existing rows untouched.
|
|
620
|
+
delete process.env[CURSOR_INDIVIDUAL_OPT_IN_ENV];
|
|
621
|
+
clearAllCapacitySnapshots();
|
|
622
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
623
|
+
baseUrl: MOCK_BASE,
|
|
624
|
+
fetchImpl: async () => {
|
|
625
|
+
throw new Error("no HTTP while disabled");
|
|
626
|
+
},
|
|
627
|
+
nowMs: now,
|
|
628
|
+
});
|
|
629
|
+
snap = getRuntimeCapacitySnapshot(now);
|
|
630
|
+
assert.ok(!snap.runtimes.some((r) => r.runtime === "cursor_local" && r.windows.length > 0), "no individual window is written while disabled");
|
|
631
|
+
});
|
|
632
|
+
test("capacity-strip refresh is stale-aware: a fresh stored window skips HTTP, concurrent callers single-flight", async () => {
|
|
633
|
+
enable();
|
|
634
|
+
fixtureCredential();
|
|
635
|
+
const now = Date.now();
|
|
636
|
+
let calls = 0;
|
|
637
|
+
const opts = {
|
|
638
|
+
baseUrl: MOCK_BASE,
|
|
639
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
640
|
+
nowMs: now,
|
|
641
|
+
};
|
|
642
|
+
const countingFetch = async (url, init) => {
|
|
643
|
+
calls += 1;
|
|
644
|
+
return opts.fetchImpl(url, init);
|
|
645
|
+
};
|
|
646
|
+
// Two calls one millisecond apart against an empty store: both see the
|
|
647
|
+
// stored snapshot as missing, so — absent single-flight — each would fire
|
|
648
|
+
// its own dashboard request. They must share one instead.
|
|
649
|
+
await Promise.all([
|
|
650
|
+
refreshCursorIndividualCapacityIfStale(now, { ...opts, fetchImpl: countingFetch }),
|
|
651
|
+
refreshCursorIndividualCapacityIfStale(now + 1, { ...opts, fetchImpl: countingFetch }),
|
|
652
|
+
]);
|
|
653
|
+
assert.equal(calls, 1, "concurrent refreshes single-flight into one HTTP call");
|
|
654
|
+
// A third call immediately after, against the now-fresh stored window,
|
|
655
|
+
// must not poll again either.
|
|
656
|
+
await refreshCursorIndividualCapacityIfStale(now + 2, { ...opts, fetchImpl: countingFetch });
|
|
657
|
+
assert.equal(calls, 1, "a fresh stored window is served with no further HTTP");
|
|
658
|
+
});
|
|
659
|
+
test("the billing-card and capacity-strip refresh paths share one poll, not two", async () => {
|
|
660
|
+
// The two paths read different stored tables (cursor_individual_billing
|
|
661
|
+
// vs runtime_capacity_snapshots), so each independently sees an empty
|
|
662
|
+
// database as stale. Without a shared single-flight, the billing card and
|
|
663
|
+
// the capacity strip mounting at once — the real Agents-page shape — fire
|
|
664
|
+
// two separate dashboard polls.
|
|
665
|
+
enable();
|
|
666
|
+
fixtureCredential();
|
|
667
|
+
const now = Date.now();
|
|
668
|
+
let calls = 0;
|
|
669
|
+
const baseFetch = mockFetch(okRoutes(now));
|
|
670
|
+
const countingFetch = async (url, init) => {
|
|
671
|
+
calls += 1;
|
|
672
|
+
return baseFetch(url, init);
|
|
673
|
+
};
|
|
674
|
+
const opts = { baseUrl: MOCK_BASE, fetchImpl: countingFetch, nowMs: now };
|
|
675
|
+
await Promise.all([
|
|
676
|
+
refreshCursorIndividualBillingIfStale(now, opts),
|
|
677
|
+
refreshCursorIndividualCapacityIfStale(now, opts),
|
|
678
|
+
]);
|
|
679
|
+
assert.equal(calls, 1, "both refresh paths share one HTTP call");
|
|
680
|
+
// The single poll's observation must have been ingested into BOTH stores.
|
|
681
|
+
const billingRow = readCursorIndividualBillingRow();
|
|
682
|
+
assert.equal(billingRow?.cycleLabel, "September 2026");
|
|
683
|
+
const snap = getRuntimeCapacitySnapshot(now);
|
|
684
|
+
const cursor = snap.runtimes.find((r) => r.runtime === "cursor_local");
|
|
685
|
+
assert.equal(cursor?.windows[0]?.remainingPercent, 62.5);
|
|
686
|
+
});
|
|
687
|
+
test("a transient shared-poll failure keeps the capacity strip's last-known window, same as the billing card", async () => {
|
|
688
|
+
enable();
|
|
689
|
+
fixtureCredential();
|
|
690
|
+
const now = Date.now();
|
|
691
|
+
// A successful poll populates both stores.
|
|
692
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
693
|
+
baseUrl: MOCK_BASE,
|
|
694
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
695
|
+
nowMs: now,
|
|
696
|
+
});
|
|
697
|
+
const before = getRuntimeCapacitySnapshot(now).runtimes.find((r) => r.runtime === "cursor_local");
|
|
698
|
+
assert.equal(before?.windows[0]?.remainingPercent, 62.5);
|
|
699
|
+
// Advance past the stale horizon; the next poll 500s — a transient
|
|
700
|
+
// failure. Both the billing table AND the capacity table must keep the
|
|
701
|
+
// last-known value instead of one of them overwriting it with `missing`.
|
|
702
|
+
const later = now + DEFAULT_STALE_AFTER_MS + 60_000;
|
|
703
|
+
await refreshCursorIndividualCapacityIfStale(later, {
|
|
704
|
+
baseUrl: MOCK_BASE,
|
|
705
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 500, body: {} } }),
|
|
706
|
+
nowMs: later,
|
|
707
|
+
});
|
|
708
|
+
const snap = getRuntimeCapacitySnapshot(later);
|
|
709
|
+
const cursor = snap.runtimes.find((r) => r.runtime === "cursor_local");
|
|
710
|
+
// The stored window is still THIS poll's window (source: experimental_api,
|
|
711
|
+
// freshUntil from the earlier successful poll) — read-time freshness
|
|
712
|
+
// classification names it `stale`, not the transient failure's `missing`.
|
|
713
|
+
assert.equal(cursor?.unavailableReason, "stale");
|
|
714
|
+
assert.equal(cursor?.windows.length, 1);
|
|
715
|
+
assert.equal(cursor?.windows[0]?.unavailableReason, "stale");
|
|
716
|
+
assert.equal(cursor?.windows[0]?.remainingPercent, null);
|
|
717
|
+
const billingSnap = await getCursorIndividualBillingSnapshot(later);
|
|
718
|
+
assert.equal(billingSnap.unavailableReason, "stale");
|
|
719
|
+
});
|
|
720
|
+
test("a malformed shared-poll response DOES overwrite the capacity strip, unlike a transient failure", async () => {
|
|
721
|
+
// The `!transient` gate must not swallow every failure — a genuine
|
|
722
|
+
// verdict on data that arrived (malformed/bad-credential) is not a
|
|
723
|
+
// transport hiccup and must still replace stale-but-stored data with an
|
|
724
|
+
// explicit `unparsable`, exactly like the billing table already does.
|
|
725
|
+
enable();
|
|
726
|
+
fixtureCredential();
|
|
727
|
+
const now = Date.now();
|
|
728
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
729
|
+
baseUrl: MOCK_BASE,
|
|
730
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
731
|
+
nowMs: now,
|
|
732
|
+
});
|
|
733
|
+
const before = getRuntimeCapacitySnapshot(now).runtimes.find((r) => r.runtime === "cursor_local");
|
|
734
|
+
assert.equal(before?.windows[0]?.remainingPercent, 62.5);
|
|
735
|
+
const later = now + DEFAULT_STALE_AFTER_MS + 60_000;
|
|
736
|
+
await refreshCursorIndividualCapacityIfStale(later, {
|
|
737
|
+
baseUrl: MOCK_BASE,
|
|
738
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: { nope: 1 } } }),
|
|
739
|
+
nowMs: later,
|
|
740
|
+
});
|
|
741
|
+
const cursor = getRuntimeCapacitySnapshot(later).runtimes.find((r) => r.runtime === "cursor_local");
|
|
742
|
+
assert.equal(cursor?.unavailableReason, "unparsable");
|
|
743
|
+
assert.equal(cursor?.windows[0]?.unavailableReason, "unparsable");
|
|
744
|
+
});
|
|
745
|
+
test("a bad-credential shared-poll response DOES overwrite the capacity strip too — the other non-transient sibling", async () => {
|
|
746
|
+
// `transient` treats `malformed` and `bad-credential` as one non-transient
|
|
747
|
+
// class (both should overwrite); the previous test only covers `malformed`
|
|
748
|
+
// at the capacity-strip level — cover the sibling the same way.
|
|
749
|
+
enable();
|
|
750
|
+
fixtureCredential();
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
753
|
+
baseUrl: MOCK_BASE,
|
|
754
|
+
fetchImpl: mockFetch(okRoutes(now)),
|
|
755
|
+
nowMs: now,
|
|
756
|
+
});
|
|
757
|
+
const before = getRuntimeCapacitySnapshot(now).runtimes.find((r) => r.runtime === "cursor_local");
|
|
758
|
+
assert.equal(before?.windows[0]?.remainingPercent, 62.5);
|
|
759
|
+
// The credential file changes shape before the next poll — the same
|
|
760
|
+
// "changed format" drift the credentials module itself calls unparsable.
|
|
761
|
+
fixtureCredential({ brandNewShape: true });
|
|
762
|
+
const later = now + DEFAULT_STALE_AFTER_MS + 60_000;
|
|
763
|
+
await refreshCursorIndividualCapacityIfStale(later, {
|
|
764
|
+
baseUrl: MOCK_BASE,
|
|
765
|
+
fetchImpl: mockFetch(okRoutes(later)),
|
|
766
|
+
nowMs: later,
|
|
767
|
+
});
|
|
768
|
+
const cursor = getRuntimeCapacitySnapshot(later).runtimes.find((r) => r.runtime === "cursor_local");
|
|
769
|
+
assert.equal(cursor?.unavailableReason, "unparsable");
|
|
770
|
+
assert.equal(cursor?.windows[0]?.unavailableReason, "unparsable");
|
|
771
|
+
});
|
|
772
|
+
test("payload parsing keeps reported values and rejects empty shapes", () => {
|
|
773
|
+
const now = Date.now();
|
|
774
|
+
const readings = cursorIndividualPayloadToReadings(usagePayload(now));
|
|
775
|
+
assert.ok(readings);
|
|
776
|
+
assert.equal(readings.cycleLabel, "September 2026");
|
|
777
|
+
assert.equal(readings.remainingPercent, 62.5);
|
|
778
|
+
// Remaining-direct and fraction scales normalize without guessing.
|
|
779
|
+
assert.equal(cursorIndividualPayloadToReadings({ remainingPercent: 80 })?.remainingPercent, 80);
|
|
780
|
+
assert.equal(cursorIndividualPayloadToReadings({ usedFraction: 0.2 })?.remainingPercent, 80);
|
|
781
|
+
assert.equal(cursorIndividualPayloadToReadings({ remaining_fraction: 0.5 })?.remainingPercent, 50);
|
|
782
|
+
// Out-of-range percents clamp instead of leaking impossible values.
|
|
783
|
+
assert.equal(cursorIndividualPayloadToReadings({ remainingPercent: 140 })?.remainingPercent, 100);
|
|
784
|
+
// Snake_case aliases tolerated; empty shapes read null (unparsable).
|
|
785
|
+
const snake = cursorIndividualPayloadToReadings({
|
|
786
|
+
cycle_label: "September 2026",
|
|
787
|
+
reset_at: new Date(now + 3600_000).toISOString(),
|
|
788
|
+
used_percent: 10,
|
|
789
|
+
});
|
|
790
|
+
assert.ok(snake);
|
|
791
|
+
assert.equal(snake.cycleLabel, "September 2026");
|
|
792
|
+
assert.equal(snake.remainingPercent, 90);
|
|
793
|
+
assert.equal(cursorIndividualPayloadToReadings(null), null);
|
|
794
|
+
assert.equal(cursorIndividualPayloadToReadings("nope"), null);
|
|
795
|
+
assert.equal(cursorIndividualPayloadToReadings({ nonsense: true }), null);
|
|
796
|
+
assert.equal(cursorIndividualPayloadToReadings({ usageUnit: "USD" }), null);
|
|
797
|
+
});
|
|
798
|
+
test("the live dashboard shape (billingCycleStart/End + nested individualUsage.plan.totalPercentUsed) parses", () => {
|
|
799
|
+
const now = Date.now();
|
|
800
|
+
const live = cursorIndividualPayloadToReadings({
|
|
801
|
+
billingCycleStart: "2026-09-01T00:00:00.000Z",
|
|
802
|
+
billingCycleEnd: new Date(now + 8 * 24 * 3600_000).toISOString(),
|
|
803
|
+
individualUsage: {
|
|
804
|
+
plan: {
|
|
805
|
+
totalPercentUsed: 37.5,
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
});
|
|
809
|
+
assert.ok(live);
|
|
810
|
+
assert.equal(live.cycleStart, "2026-09-01T00:00:00.000Z");
|
|
811
|
+
assert.equal(live.cycleEnd, new Date(now + 8 * 24 * 3600_000).toISOString());
|
|
812
|
+
// 37.5% used → 62.5 remaining, same scale as the top-level usedPercent path.
|
|
813
|
+
assert.equal(live.remainingPercent, 62.5);
|
|
814
|
+
// A top-level usedPercent still wins over a nested totalPercentUsed — the
|
|
815
|
+
// merge only fills gaps, it never lets the nested plan override a scale
|
|
816
|
+
// the top-level payload already reported.
|
|
817
|
+
const clash = cursorIndividualPayloadToReadings({
|
|
818
|
+
billingCycleStart: "2026-09-01T00:00:00.000Z",
|
|
819
|
+
usedPercent: 10,
|
|
820
|
+
individualUsage: { plan: { totalPercentUsed: 90 } },
|
|
821
|
+
});
|
|
822
|
+
assert.equal(clash?.remainingPercent, 90);
|
|
823
|
+
// A non-object individualUsage/plan is ignored, not a crash.
|
|
824
|
+
assert.equal(cursorIndividualPayloadToReadings({ individualUsage: "nope" })?.remainingPercent ?? null, null);
|
|
825
|
+
assert.equal(cursorIndividualPayloadToReadings({ individualUsage: { plan: "nope" } }), null);
|
|
826
|
+
});
|
|
827
|
+
test("readBoundedText aborts once the body exceeds the size cap, without buffering it all first", async () => {
|
|
828
|
+
const chunkSize = 64 * 1024;
|
|
829
|
+
const chunk = new Uint8Array(chunkSize).fill(97);
|
|
830
|
+
// A stream far larger than the cap (well over 10x) — an attacker/broken
|
|
831
|
+
// server sending an oversized or unbounded body. Only ~8-9 chunks are
|
|
832
|
+
// needed to cross the 512 KiB cap; the assertion below proves the read
|
|
833
|
+
// stops there instead of buffering the whole thing.
|
|
834
|
+
const availableChunks = 100;
|
|
835
|
+
let delivered = 0;
|
|
836
|
+
const stream = new ReadableStream({
|
|
837
|
+
pull(controller) {
|
|
838
|
+
if (delivered >= availableChunks) {
|
|
839
|
+
controller.close();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
delivered += 1;
|
|
843
|
+
controller.enqueue(chunk);
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
const res = new Response(stream);
|
|
847
|
+
const controller = new AbortController();
|
|
848
|
+
await assert.rejects(() => readBoundedText(res, controller.signal));
|
|
849
|
+
// The stream must have been cancelled well before the full body was
|
|
850
|
+
// pulled — i.e. the cap is enforced during the read, not after buffering
|
|
851
|
+
// it all. A few chunks of slack above the exact cap boundary is fine; the
|
|
852
|
+
// bulk of the 100-chunk body must never be read.
|
|
853
|
+
assert.ok(delivered <= 15, `expected the read to abort well before all ${availableChunks} chunks were pulled, got ${delivered}`);
|
|
854
|
+
// A body within the cap still reads through normally.
|
|
855
|
+
const smallStream = new ReadableStream({
|
|
856
|
+
start(controller) {
|
|
857
|
+
controller.enqueue(new TextEncoder().encode("hello"));
|
|
858
|
+
controller.close();
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
const smallRes = new Response(smallStream);
|
|
862
|
+
const text = await readBoundedText(smallRes, new AbortController().signal);
|
|
863
|
+
assert.equal(text, "hello");
|
|
864
|
+
// An already-aborted signal aborts the read too.
|
|
865
|
+
const abortedController = new AbortController();
|
|
866
|
+
abortedController.abort();
|
|
867
|
+
const anotherStream = new ReadableStream({
|
|
868
|
+
start(c) {
|
|
869
|
+
c.enqueue(new TextEncoder().encode("x"));
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
await assert.rejects(() => readBoundedText(new Response(anotherStream), abortedController.signal));
|
|
873
|
+
});
|
|
874
|
+
// ---------------------------------------------------------------------------
|
|
875
|
+
// NOT-267: desktop-login acquisition and one-window normalization of the
|
|
876
|
+
// live `/api/usage-summary` shape.
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
/** header.payload.signature with { sub } — no signature verification needed. */
|
|
879
|
+
function desktopJwt(sub) {
|
|
880
|
+
const b64url = (obj) => Buffer.from(JSON.stringify(obj)).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
881
|
+
return `${b64url({ alg: "none" })}.${b64url({ sub })}.sig`;
|
|
882
|
+
}
|
|
883
|
+
/** Point the desktop resolution at a fixture state.vscdb (real SQLite). */
|
|
884
|
+
function fixtureDesktopDb(token) {
|
|
885
|
+
const file = path.join(credDir, "state.vscdb");
|
|
886
|
+
const db = new Database(file);
|
|
887
|
+
try {
|
|
888
|
+
db.exec("CREATE TABLE ItemTable (key TEXT PRIMARY KEY, value TEXT)");
|
|
889
|
+
db.prepare("INSERT INTO ItemTable (key, value) VALUES (?, ?)").run(CURSOR_DESKTOP_ACCESS_TOKEN_KEY, token);
|
|
890
|
+
}
|
|
891
|
+
finally {
|
|
892
|
+
db.close();
|
|
893
|
+
}
|
|
894
|
+
process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV] = file;
|
|
895
|
+
return file;
|
|
896
|
+
}
|
|
897
|
+
/** Live-shape payload: cycle dates top-level, account total nested in the plan. */
|
|
898
|
+
function liveUsagePayload(nowMs, over = {}) {
|
|
899
|
+
return {
|
|
900
|
+
billingCycleStart: "2026-09-01T00:00:00.000Z",
|
|
901
|
+
billingCycleEnd: new Date(nowMs + 8 * 24 * 3600_000).toISOString(),
|
|
902
|
+
individualUsage: {
|
|
903
|
+
plan: { totalPercentUsed: 37.5 },
|
|
904
|
+
apiUsage: { apiPercentUsed: 91.2 },
|
|
905
|
+
},
|
|
906
|
+
teamUsage: { totalPercentUsed: 99.9 },
|
|
907
|
+
...over,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
test("a desktop-login read sends the desktop session cookie and normalizes one window", async () => {
|
|
911
|
+
enable();
|
|
912
|
+
// Default resolution (no pinned credential file): the desktop database
|
|
913
|
+
// answers, the empty Agent home does not.
|
|
914
|
+
process.env[CURSOR_INDIVIDUAL_HOME_ENV] = credDir;
|
|
915
|
+
const jwt = desktopJwt(USER_ID);
|
|
916
|
+
fixtureDesktopDb(jwt);
|
|
917
|
+
const now = Date.now();
|
|
918
|
+
const calls = [];
|
|
919
|
+
const obs = await readCursorIndividualBilling({
|
|
920
|
+
baseUrl: MOCK_BASE,
|
|
921
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: liveUsagePayload(now) } }, calls),
|
|
922
|
+
nowMs: now,
|
|
923
|
+
});
|
|
924
|
+
assert.equal(obs.failure, null);
|
|
925
|
+
assert.equal(obs.billing.remainingPercent, 62.5);
|
|
926
|
+
assert.equal(obs.billing.cycleStart, "2026-09-01T00:00:00.000Z");
|
|
927
|
+
assert.equal(obs.billing.cycleEnd, new Date(now + 8 * 24 * 3600_000).toISOString());
|
|
928
|
+
assert.equal(calls.length, 1);
|
|
929
|
+
assert.equal(calls[0].headers.Cookie, cursorIndividualAuthHeader(USER_ID, jwt));
|
|
930
|
+
assert.ok(!JSON.stringify(obs).includes(jwt), "raw desktop token stays in the header only");
|
|
931
|
+
});
|
|
932
|
+
test("default resolution with neither login performs no HTTP and reads absent-credential", async () => {
|
|
933
|
+
enable();
|
|
934
|
+
process.env[CURSOR_INDIVIDUAL_HOME_ENV] = credDir;
|
|
935
|
+
process.env[CURSOR_INDIVIDUAL_DESKTOP_STATE_FILE_ENV] = path.join(credDir, "missing.vscdb");
|
|
936
|
+
const calls = [];
|
|
937
|
+
const obs = await readCursorIndividualBilling({
|
|
938
|
+
baseUrl: MOCK_BASE,
|
|
939
|
+
fetchImpl: mockFetch({}, calls),
|
|
940
|
+
});
|
|
941
|
+
assert.equal(obs.failure?.kind, "absent-credential");
|
|
942
|
+
assert.equal(obs.configured, false);
|
|
943
|
+
assert.equal(calls.length, 0);
|
|
944
|
+
assert.equal(cursorIndividualFailureReason("absent-credential"), "missing");
|
|
945
|
+
});
|
|
946
|
+
test("live usage-summary shape writes exactly one billing_cycle snapshot and one billing row", async () => {
|
|
947
|
+
enable();
|
|
948
|
+
fixtureCredential();
|
|
949
|
+
const now = Date.now();
|
|
950
|
+
const cycleEnd = new Date(now + 8 * 24 * 3600_000).toISOString();
|
|
951
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
952
|
+
baseUrl: MOCK_BASE,
|
|
953
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: liveUsagePayload(now) } }),
|
|
954
|
+
nowMs: now,
|
|
955
|
+
});
|
|
956
|
+
const { listCapacitySnapshots } = await import("../repository/runtime-capacity.js");
|
|
957
|
+
const rows = listCapacitySnapshots(CURSOR_INDIVIDUAL_RUNTIME);
|
|
958
|
+
assert.equal(rows.length, 1);
|
|
959
|
+
assert.equal(rows[0].windowKey, "billing_cycle");
|
|
960
|
+
assert.equal(rows[0].remainingPercent, 62.5);
|
|
961
|
+
assert.equal(rows[0].resetAt, cycleEnd);
|
|
962
|
+
const billingRow = readCursorIndividualBillingRow();
|
|
963
|
+
assert.ok(billingRow, "one billing row is written");
|
|
964
|
+
assert.equal(billingRow.cycleStart, "2026-09-01T00:00:00.000Z");
|
|
965
|
+
assert.equal(billingRow.cycleEnd, cycleEnd);
|
|
966
|
+
assert.equal(billingRow.remainingPercent, 62.5);
|
|
967
|
+
const snap = await getCursorIndividualBillingSnapshot(now);
|
|
968
|
+
assert.equal(snap.unavailableReason, null);
|
|
969
|
+
assert.equal(snap.remainingPercent, 62.5);
|
|
970
|
+
});
|
|
971
|
+
test("with multiple Cursor pools the primary value is 100 - totalPercentUsed; no second window", async () => {
|
|
972
|
+
// totalPercentUsed 37.5 → 62.5 remaining — NOT 100 - 91.2 (API pool) and
|
|
973
|
+
// NOT 100 - 99.9 (team scope). The adapter emits exactly one window.
|
|
974
|
+
const readings = cursorIndividualPayloadToReadings(liveUsagePayload(Date.now()));
|
|
975
|
+
assert.ok(readings);
|
|
976
|
+
assert.equal(readings.remainingPercent, 62.5);
|
|
977
|
+
enable();
|
|
978
|
+
fixtureCredential();
|
|
979
|
+
const now = Date.now();
|
|
980
|
+
const result = await cursorIndividualCapacityAdapter({
|
|
981
|
+
baseUrl: MOCK_BASE,
|
|
982
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: liveUsagePayload(now) } }),
|
|
983
|
+
nowMs: now,
|
|
984
|
+
}).read(now);
|
|
985
|
+
assert.equal(result.windows.length, 1);
|
|
986
|
+
assert.equal(result.unavailable.length, 0);
|
|
987
|
+
assert.equal(result.windows[0].windowKey, "billing_cycle");
|
|
988
|
+
});
|
|
989
|
+
test("a pool-only scale (apiPercentUsed) is never substituted for the account total", async () => {
|
|
990
|
+
const poolOnly = cursorIndividualPayloadToReadings({
|
|
991
|
+
billingCycleStart: "2026-09-01T00:00:00.000Z",
|
|
992
|
+
billingCycleEnd: new Date(Date.now() + 8 * 24 * 3600_000).toISOString(),
|
|
993
|
+
individualUsage: { plan: { apiPercentUsed: 91.2 } },
|
|
994
|
+
});
|
|
995
|
+
// Cycle dates parse, but the remaining scale is absent — never 100 - 91.2.
|
|
996
|
+
assert.ok(poolOnly);
|
|
997
|
+
assert.equal(poolOnly.remainingPercent, null);
|
|
998
|
+
assert.equal(cursorIndividualPayloadToReadings({ individualUsage: { plan: { apiPercentUsed: 91.2 } } }), null);
|
|
999
|
+
// End to end: no usable scale reads unparsable with no window emitted.
|
|
1000
|
+
enable();
|
|
1001
|
+
fixtureCredential();
|
|
1002
|
+
const obs = await readCursorIndividualBilling({
|
|
1003
|
+
baseUrl: MOCK_BASE,
|
|
1004
|
+
fetchImpl: mockFetch({
|
|
1005
|
+
[CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: {
|
|
1006
|
+
status: 200,
|
|
1007
|
+
body: { individualUsage: { plan: { apiPercentUsed: 91.2 } } },
|
|
1008
|
+
},
|
|
1009
|
+
}),
|
|
1010
|
+
});
|
|
1011
|
+
assert.equal(obs.failure?.kind, "malformed");
|
|
1012
|
+
});
|
|
1013
|
+
test("the plan's explicit used/limit pair is a last resort when total percent is absent", async () => {
|
|
1014
|
+
const now = Date.now();
|
|
1015
|
+
const cycle = {
|
|
1016
|
+
billingCycleStart: "2026-09-01T00:00:00.000Z",
|
|
1017
|
+
billingCycleEnd: new Date(now + 8 * 24 * 3600_000).toISOString(),
|
|
1018
|
+
};
|
|
1019
|
+
// 30 used of a 100 limit → 70 remaining.
|
|
1020
|
+
assert.equal(cursorIndividualPayloadToReadings({ ...cycle, individualUsage: { plan: { used: 30, limit: 100 } } })
|
|
1021
|
+
?.remainingPercent, 70);
|
|
1022
|
+
// A present total percent always wins over the pair.
|
|
1023
|
+
assert.equal(cursorIndividualPayloadToReadings({
|
|
1024
|
+
...cycle,
|
|
1025
|
+
individualUsage: { plan: { totalPercentUsed: 37.5, used: 30, limit: 100 } },
|
|
1026
|
+
})?.remainingPercent, 62.5);
|
|
1027
|
+
// Degenerate pairs are not scales: zero limit, negative used.
|
|
1028
|
+
assert.equal(cursorIndividualPayloadToReadings({ ...cycle, individualUsage: { plan: { used: 30, limit: 0 } } })
|
|
1029
|
+
?.remainingPercent, null);
|
|
1030
|
+
assert.equal(cursorIndividualPayloadToReadings({ ...cycle, individualUsage: { plan: { used: -5, limit: 100 } } })
|
|
1031
|
+
?.remainingPercent, null);
|
|
1032
|
+
// Top-level numerics never form the plan pair: without individualUsage.plan
|
|
1033
|
+
// there is no usable scale, even when the names line up.
|
|
1034
|
+
assert.equal(cursorIndividualPayloadToReadings({ ...cycle, used: 30, limit: 100 })?.remainingPercent, null);
|
|
1035
|
+
});
|
|
1036
|
+
test("a completed live billing cycle reads expired with its start/end intact", async () => {
|
|
1037
|
+
enable();
|
|
1038
|
+
fixtureCredential();
|
|
1039
|
+
const now = Date.now();
|
|
1040
|
+
await refreshCursorIndividualCapacityIfStale(now, {
|
|
1041
|
+
baseUrl: MOCK_BASE,
|
|
1042
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: liveUsagePayload(now) } }),
|
|
1043
|
+
nowMs: now,
|
|
1044
|
+
});
|
|
1045
|
+
// Past the cycle end, the stored row still carries its dates — the read
|
|
1046
|
+
// model reports them as expired, never as current capacity.
|
|
1047
|
+
const pastEnd = Date.parse(new Date(now + 8 * 24 * 3600_000).toISOString()) + 1000;
|
|
1048
|
+
const snap = await getCursorIndividualBillingSnapshot(pastEnd);
|
|
1049
|
+
assert.equal(snap.unavailableReason, "expired");
|
|
1050
|
+
assert.equal(snap.remainingPercent, null);
|
|
1051
|
+
const row = readCursorIndividualBillingRow();
|
|
1052
|
+
assert.equal(row?.cycleStart, "2026-09-01T00:00:00.000Z");
|
|
1053
|
+
assert.equal(row?.remainingPercent, 62.5);
|
|
1054
|
+
});
|
|
1055
|
+
test("401/429/network failures keep last-good data and never leak the credential", async () => {
|
|
1056
|
+
enable();
|
|
1057
|
+
fixtureCredential();
|
|
1058
|
+
const now = Date.now();
|
|
1059
|
+
const good = {
|
|
1060
|
+
baseUrl: MOCK_BASE,
|
|
1061
|
+
fetchImpl: mockFetch({ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 200, body: liveUsagePayload(now) } }),
|
|
1062
|
+
nowMs: now,
|
|
1063
|
+
};
|
|
1064
|
+
await refreshCursorIndividualBilling(good);
|
|
1065
|
+
assert.equal(readCursorIndividualBillingRow()?.remainingPercent, 62.5);
|
|
1066
|
+
const later = now + 60_000;
|
|
1067
|
+
for (const routes of [
|
|
1068
|
+
{ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 401, body: {} } },
|
|
1069
|
+
{ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: { status: 429, body: {} } },
|
|
1070
|
+
{ [CURSOR_INDIVIDUAL_USAGE_PATHS[0]]: new Error("socket hang up") },
|
|
1071
|
+
]) {
|
|
1072
|
+
const obs = await readCursorIndividualBilling({ baseUrl: MOCK_BASE, fetchImpl: mockFetch(routes), nowMs: later });
|
|
1073
|
+
assert.ok(obs.failure, "failure is explicit");
|
|
1074
|
+
assert.equal(obs.billing.unavailableReason, "missing");
|
|
1075
|
+
assert.ok(!JSON.stringify(obs).includes(SECRET), "no credential material in the observation");
|
|
1076
|
+
await refreshCursorIndividualBilling({ baseUrl: MOCK_BASE, fetchImpl: mockFetch(routes), nowMs: later });
|
|
1077
|
+
const snap = await getCursorIndividualBillingSnapshot(later);
|
|
1078
|
+
assert.equal(snap.remainingPercent, 62.5);
|
|
1079
|
+
assert.equal(snap.unavailableReason, null);
|
|
1080
|
+
}
|
|
1081
|
+
});
|