Skip to content

Commit e84e1e9

Browse files
tt-a1itestikun
authored andcommitted
fix(usage): observe rejected input on pre-cancelled waits
1 parent 80b8e9b commit e84e1e9

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

‎extensions/usage/providers/utils.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export async function waitForUsage<T>(
8282
promise: Promise<T>,
8383
signal: AbortSignal,
8484
): Promise<T> {
85+
// The accepted refresh keeps running even when this wait is already cancelled.
86+
if (signal.aborted) void promise.catch(() => undefined);
8587
signal.throwIfAborted();
8688
let onAbort: (() => void) | undefined;
8789
const aborted = new Promise<never>((_resolve, reject) => {

‎tests/extensions/usage/providers.test.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import assert from "node:assert/strict";
2+
import { spawnSync } from "node:child_process";
23
import test from "node:test";
34
import { getAntigravityUserAgent } from "../../../extensions/ai-providers/antigravity/oauth.ts";
45
import {
@@ -37,6 +38,71 @@ test("a pre-cancelled quota wait cannot return an already-fulfilled result", asy
3738
);
3839
});
3940

41+
for (const timing of ["immediate", "delayed", "during-wait"]) {
42+
test(`cancelled quota waits observe ${timing} rejection without leaking listeners`, () => {
43+
const utilsUrl = new URL(
44+
"../../../extensions/usage/providers/utils.ts",
45+
import.meta.url,
46+
).href;
47+
const result = spawnSync(
48+
process.execPath,
49+
[
50+
"--experimental-strip-types",
51+
"--input-type=module",
52+
"--eval",
53+
`
54+
import assert from "node:assert/strict";
55+
import { getEventListeners } from "node:events";
56+
import { waitForUsage } from ${JSON.stringify(utilsUrl)};
57+
const timing = ${JSON.stringify(timing)};
58+
const unhandled = [];
59+
process.on("unhandledRejection", (error) => unhandled.push(error));
60+
const controller = new AbortController();
61+
const reason = new Error("cancelled fixture");
62+
const failure = new Error("quota failed fixture");
63+
let rejectInput;
64+
const input = timing === "immediate"
65+
? Promise.reject(failure)
66+
: new Promise((_resolve, reject) => { rejectInput = reject; });
67+
if (timing !== "during-wait") controller.abort(reason);
68+
const waiting = waitForUsage(input, controller.signal);
69+
if (timing === "during-wait") {
70+
assert.equal(getEventListeners(controller.signal, "abort").length, 1);
71+
controller.abort(reason);
72+
} else {
73+
assert.equal(getEventListeners(controller.signal, "abort").length, 0);
74+
}
75+
await assert.rejects(waiting, (error) => error === reason);
76+
assert.equal(getEventListeners(controller.signal, "abort").length, 0);
77+
rejectInput?.(failure);
78+
await new Promise((resolve) => setImmediate(resolve));
79+
await new Promise((resolve) => setImmediate(resolve));
80+
assert.deepEqual(unhandled, []);
81+
process.stdout.write("observed");
82+
`,
83+
],
84+
{ encoding: "utf8", timeout: 10_000 },
85+
);
86+
assert.equal(result.error, undefined);
87+
assert.equal(result.status, 0, result.stderr);
88+
assert.equal(result.stdout, "observed");
89+
});
90+
}
91+
92+
test("uncancelled quota waits preserve resolved values and rejected errors", async () => {
93+
const controller = new AbortController();
94+
const value = { quota: 42 };
95+
assert.equal(
96+
await waitForUsage(Promise.resolve(value), controller.signal),
97+
value,
98+
);
99+
const failure = new Error("quota failed fixture");
100+
await assert.rejects(
101+
waitForUsage(Promise.reject(failure), controller.signal),
102+
(error: unknown) => error === failure,
103+
);
104+
});
105+
40106
test("Cursor uses its session cookie, authoritative percentages and independent account metadata", async () => {
41107
const calls: Array<{ url: string; init?: RequestInit }> = [];
42108
const report = await cursorAdapter.fetchUsage(

0 commit comments

Comments
 (0)