@vacbo/opencode-anthropic-fix 0.0.44 → 0.1.1

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.
Files changed (61) hide show
  1. package/README.md +19 -0
  2. package/dist/bun-proxy.mjs +282 -55
  3. package/dist/opencode-anthropic-auth-cli.mjs +194 -55
  4. package/dist/opencode-anthropic-auth-plugin.js +1816 -594
  5. package/package.json +1 -1
  6. package/src/__tests__/billing-edge-cases.test.ts +84 -0
  7. package/src/__tests__/bun-proxy.parallel.test.ts +460 -0
  8. package/src/__tests__/debug-gating.test.ts +76 -0
  9. package/src/__tests__/decomposition-smoke.test.ts +92 -0
  10. package/src/__tests__/fingerprint-regression.test.ts +1 -1
  11. package/src/__tests__/helpers/conversation-history.smoke.test.ts +338 -0
  12. package/src/__tests__/helpers/conversation-history.ts +376 -0
  13. package/src/__tests__/helpers/deferred.smoke.test.ts +161 -0
  14. package/src/__tests__/helpers/deferred.ts +122 -0
  15. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +166 -0
  16. package/src/__tests__/helpers/in-memory-storage.ts +152 -0
  17. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +92 -0
  18. package/src/__tests__/helpers/mock-bun-proxy.ts +229 -0
  19. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +337 -0
  20. package/src/__tests__/helpers/plugin-fetch-harness.ts +401 -0
  21. package/src/__tests__/helpers/sse.smoke.test.ts +243 -0
  22. package/src/__tests__/helpers/sse.ts +288 -0
  23. package/src/__tests__/index.parallel.test.ts +711 -0
  24. package/src/__tests__/sanitization-regex.test.ts +65 -0
  25. package/src/__tests__/state-bounds.test.ts +110 -0
  26. package/src/account-identity.test.ts +213 -0
  27. package/src/account-identity.ts +108 -0
  28. package/src/accounts.dedup.test.ts +696 -0
  29. package/src/accounts.test.ts +2 -1
  30. package/src/accounts.ts +485 -191
  31. package/src/bun-fetch.test.ts +379 -0
  32. package/src/bun-fetch.ts +447 -174
  33. package/src/bun-proxy.ts +289 -57
  34. package/src/circuit-breaker.test.ts +274 -0
  35. package/src/circuit-breaker.ts +235 -0
  36. package/src/cli.test.ts +1 -0
  37. package/src/cli.ts +37 -18
  38. package/src/commands/router.ts +25 -5
  39. package/src/env.ts +1 -0
  40. package/src/headers/billing.ts +31 -13
  41. package/src/index.ts +224 -247
  42. package/src/oauth.ts +7 -1
  43. package/src/parent-pid-watcher.test.ts +219 -0
  44. package/src/parent-pid-watcher.ts +99 -0
  45. package/src/plugin-helpers.ts +112 -0
  46. package/src/refresh-helpers.ts +169 -0
  47. package/src/refresh-lock.test.ts +36 -9
  48. package/src/refresh-lock.ts +2 -2
  49. package/src/request/body.history.test.ts +398 -0
  50. package/src/request/body.ts +200 -13
  51. package/src/request/metadata.ts +6 -2
  52. package/src/response/index.ts +1 -1
  53. package/src/response/mcp.ts +60 -31
  54. package/src/response/streaming.test.ts +382 -0
  55. package/src/response/streaming.ts +403 -76
  56. package/src/storage.test.ts +127 -104
  57. package/src/storage.ts +152 -62
  58. package/src/system-prompt/builder.ts +33 -3
  59. package/src/system-prompt/sanitize.ts +12 -2
  60. package/src/token-refresh.test.ts +84 -1
  61. package/src/token-refresh.ts +14 -8
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Factory functions for creating conversation state in tests.
3
+ *
4
+ * Provides utilities for building Anthropic Messages API compatible
5
+ * conversation objects, messages, tool_use blocks, and tool_result blocks.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const conversation = makeConversation({
10
+ * messages: [
11
+ * makeMessage({ role: 'user', content: 'Hello' }),
12
+ * makeMessage({
13
+ * role: 'assistant',
14
+ * content: [makeToolUse({ name: 'read_file', input: { path: 'test.txt' } })]
15
+ * }),
16
+ * makeMessage({
17
+ * role: 'user',
18
+ * content: [makeToolResult({ toolUseId: 'tu_123', content: 'file contents' })]
19
+ * })
20
+ * ]
21
+ * });
22
+ * ```
23
+ */
24
+
25
+ import { randomUUID } from "node:crypto";
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Types
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export type MessageRole = "user" | "assistant";
32
+
33
+ export interface TextBlock {
34
+ type: "text";
35
+ text: string;
36
+ }
37
+
38
+ export interface ToolUseBlock {
39
+ type: "tool_use";
40
+ id: string;
41
+ name: string;
42
+ input: Record<string, unknown>;
43
+ }
44
+
45
+ export interface ToolResultBlock {
46
+ type: "tool_result";
47
+ tool_use_id: string;
48
+ content: string | Array<TextBlock | ImageBlock>;
49
+ is_error?: boolean;
50
+ }
51
+
52
+ export interface ImageBlock {
53
+ type: "image";
54
+ source: {
55
+ type: "base64";
56
+ media_type: string;
57
+ data: string;
58
+ };
59
+ }
60
+
61
+ export type MessageContent = string | Array<TextBlock | ToolUseBlock | ToolResultBlock | ImageBlock>;
62
+
63
+ export interface Message {
64
+ role: MessageRole;
65
+ content: MessageContent;
66
+ }
67
+
68
+ export interface Conversation {
69
+ messages: Message[];
70
+ metadata?: Record<string, unknown>;
71
+ }
72
+
73
+ export interface ConversationFactory {
74
+ /** Generate unique IDs (default: true) */
75
+ generateIds?: boolean;
76
+ /** Default prefix for generated IDs */
77
+ idPrefix?: string;
78
+ }
79
+
80
+ export interface MakeConversationOptions extends ConversationFactory {
81
+ /** Pre-populated messages array */
82
+ messages?: Message[];
83
+ /** Optional conversation metadata */
84
+ metadata?: Record<string, unknown>;
85
+ }
86
+
87
+ export interface MakeMessageOptions {
88
+ /** Message role (user or assistant) */
89
+ role?: MessageRole;
90
+ /** Message content - string or content blocks array */
91
+ content?: MessageContent;
92
+ }
93
+
94
+ export interface MakeToolUseOptions {
95
+ /** Unique tool use ID (auto-generated if not provided) */
96
+ id?: string;
97
+ /** Tool name */
98
+ name?: string;
99
+ /** Tool input parameters */
100
+ input?: Record<string, unknown>;
101
+ }
102
+
103
+ export interface MakeToolResultOptions {
104
+ /** ID of the tool_use this result corresponds to */
105
+ toolUseId?: string;
106
+ /** Result content - string or content blocks */
107
+ content?: string | Array<TextBlock | ImageBlock>;
108
+ /** Whether this result represents an error */
109
+ isError?: boolean;
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // ID Generation
114
+ // ---------------------------------------------------------------------------
115
+
116
+ let idCounter = 0;
117
+
118
+ /**
119
+ * Generate a unique tool use ID.
120
+ *
121
+ * @param prefix - ID prefix (default: "tu")
122
+ * @returns Unique ID string
123
+ */
124
+ export function generateToolUseId(prefix = "tu"): string {
125
+ return `${prefix}_${randomUUID().replace(/-/g, "").slice(0, 12)}_${++idCounter}`;
126
+ }
127
+
128
+ /**
129
+ * Reset the ID counter for deterministic tests.
130
+ */
131
+ export function resetIdCounter(): void {
132
+ idCounter = 0;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Factory Functions
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Create a conversation object with messages array.
141
+ *
142
+ * @param opts - Conversation options
143
+ * @returns Conversation object
144
+ *
145
+ * @example
146
+ * ```ts
147
+ * const conversation = makeConversation({
148
+ * messages: [makeMessage({ role: 'user', content: 'Hello' })],
149
+ * metadata: { sessionId: 'abc123' }
150
+ * });
151
+ * ```
152
+ */
153
+ export function makeConversation(opts: MakeConversationOptions = {}): Conversation {
154
+ return {
155
+ messages: opts.messages ?? [],
156
+ metadata: opts.metadata,
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Create a message with role and content.
162
+ *
163
+ * @param opts - Message options
164
+ * @returns Message object
165
+ *
166
+ * @example
167
+ * ```ts
168
+ * const msg = makeMessage({ role: 'user', content: 'Hello Claude' });
169
+ * const assistantMsg = makeMessage({
170
+ * role: 'assistant',
171
+ * content: [{ type: 'text', text: 'Hello!' }]
172
+ * });
173
+ * ```
174
+ */
175
+ export function makeMessage(opts: MakeMessageOptions = {}): Message {
176
+ const role = opts.role ?? "user";
177
+ const content = opts.content ?? "";
178
+
179
+ return { role, content };
180
+ }
181
+
182
+ /**
183
+ * Create a text content block.
184
+ *
185
+ * @param text - Text content
186
+ * @returns TextBlock object
187
+ */
188
+ export function makeTextBlock(text: string): TextBlock {
189
+ return { type: "text", text };
190
+ }
191
+
192
+ /**
193
+ * Create a tool_use block with id, name, and input.
194
+ *
195
+ * @param opts - Tool use options
196
+ * @returns ToolUseBlock object
197
+ *
198
+ * @example
199
+ * ```ts
200
+ * const toolUse = makeToolUse({
201
+ * name: 'read_file',
202
+ * input: { path: 'src/index.ts' }
203
+ * });
204
+ * ```
205
+ */
206
+ export function makeToolUse(opts: MakeToolUseOptions = {}): ToolUseBlock {
207
+ return {
208
+ type: "tool_use",
209
+ id: opts.id ?? generateToolUseId(),
210
+ name: opts.name ?? "unnamed_tool",
211
+ input: opts.input ?? {},
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Create a tool_result block with tool_use_id and content.
217
+ *
218
+ * @param opts - Tool result options
219
+ * @returns ToolResultBlock object
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * const toolResult = makeToolResult({
224
+ * toolUseId: 'tu_abc123',
225
+ * content: 'File contents here'
226
+ * });
227
+ * ```
228
+ */
229
+ export function makeToolResult(opts: MakeToolResultOptions = {}): ToolResultBlock {
230
+ return {
231
+ type: "tool_result",
232
+ tool_use_id: opts.toolUseId ?? generateToolUseId("tr"),
233
+ content: opts.content ?? "",
234
+ is_error: opts.isError ?? false,
235
+ };
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // Validation Helpers
240
+ // ---------------------------------------------------------------------------
241
+
242
+ /**
243
+ * Validate that a tool_use and tool_result pair match.
244
+ *
245
+ * @param toolUse - The tool_use block
246
+ * @param toolResult - The tool_result block
247
+ * @returns True if the pair is valid
248
+ */
249
+ export function validateToolPair(toolUse: ToolUseBlock, toolResult: ToolResultBlock): boolean {
250
+ return toolUse.id === toolResult.tool_use_id;
251
+ }
252
+
253
+ /**
254
+ * Find the tool_result corresponding to a tool_use in a message array.
255
+ *
256
+ * @param messages - Array of messages to search
257
+ * @param toolUseId - The tool_use ID to find the result for
258
+ * @returns The matching ToolResultBlock or undefined
259
+ */
260
+ export function findToolResult(messages: Message[], toolUseId: string): ToolResultBlock | undefined {
261
+ for (const message of messages) {
262
+ if (typeof message.content === "string") continue;
263
+
264
+ for (const block of message.content) {
265
+ if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
266
+ return block;
267
+ }
268
+ }
269
+ }
270
+ return undefined;
271
+ }
272
+
273
+ /**
274
+ * Check if all tool_use blocks in a conversation have matching tool_results.
275
+ *
276
+ * @param conversation - The conversation to validate
277
+ * @returns Object with validation results
278
+ */
279
+ export function validateConversationTools(conversation: Conversation): {
280
+ valid: boolean;
281
+ unmatchedToolUses: ToolUseBlock[];
282
+ unmatchedToolResults: ToolResultBlock[];
283
+ } {
284
+ const toolUses: ToolUseBlock[] = [];
285
+ const toolResults: ToolResultBlock[] = [];
286
+
287
+ // Collect all tool_use and tool_result blocks
288
+ for (const message of conversation.messages) {
289
+ if (typeof message.content === "string") continue;
290
+
291
+ for (const block of message.content) {
292
+ if (block.type === "tool_use") {
293
+ toolUses.push(block);
294
+ } else if (block.type === "tool_result") {
295
+ toolResults.push(block);
296
+ }
297
+ }
298
+ }
299
+
300
+ const unmatchedToolUses = toolUses.filter((tu) => !toolResults.some((tr) => tr.tool_use_id === tu.id));
301
+
302
+ const unmatchedToolResults = toolResults.filter((tr) => !toolUses.some((tu) => tu.id === tr.tool_use_id));
303
+
304
+ return {
305
+ valid: unmatchedToolUses.length === 0 && unmatchedToolResults.length === 0,
306
+ unmatchedToolUses,
307
+ unmatchedToolResults,
308
+ };
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Convenience Builders
313
+ // ---------------------------------------------------------------------------
314
+
315
+ /**
316
+ * Create a complete tool call exchange: assistant tool_use + user tool_result.
317
+ *
318
+ * @param toolName - Name of the tool
319
+ * @param toolInput - Tool input parameters
320
+ * @param resultContent - Result content string
321
+ * @returns Array of [toolUse, toolResult] pair
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * const [toolUse, toolResult] = makeToolExchange(
326
+ * 'read_file',
327
+ * { path: 'test.txt' },
328
+ * 'file contents'
329
+ * );
330
+ * ```
331
+ */
332
+ export function makeToolExchange(
333
+ toolName: string,
334
+ toolInput: Record<string, unknown>,
335
+ resultContent: string,
336
+ ): [ToolUseBlock, ToolResultBlock] {
337
+ const toolUse = makeToolUse({ name: toolName, input: toolInput });
338
+ const toolResult = makeToolResult({ toolUseId: toolUse.id, content: resultContent });
339
+ return [toolUse, toolResult];
340
+ }
341
+
342
+ /**
343
+ * Create a conversation with a complete tool call flow.
344
+ *
345
+ * @param userPrompt - Initial user message
346
+ * @param toolName - Tool to call
347
+ * @param toolInput - Tool input
348
+ * @param toolOutput - Tool output
349
+ * @returns Conversation with complete message flow
350
+ *
351
+ * @example
352
+ * ```ts
353
+ * const conv = makeToolConversation(
354
+ * 'Read the file',
355
+ * 'read_file',
356
+ * { path: 'test.txt' },
357
+ * 'file contents'
358
+ * );
359
+ * ```
360
+ */
361
+ export function makeToolConversation(
362
+ userPrompt: string,
363
+ toolName: string,
364
+ toolInput: Record<string, unknown>,
365
+ toolOutput: string,
366
+ ): Conversation {
367
+ const [toolUse, toolResult] = makeToolExchange(toolName, toolInput, toolOutput);
368
+
369
+ return makeConversation({
370
+ messages: [
371
+ makeMessage({ role: "user", content: userPrompt }),
372
+ makeMessage({ role: "assistant", content: [toolUse] }),
373
+ makeMessage({ role: "user", content: [toolResult] }),
374
+ ],
375
+ });
376
+ }
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { createDeferred, createDeferredQueue, nextTick } from "./deferred";
3
+
4
+ describe("deferred helpers", () => {
5
+ describe("createDeferred", () => {
6
+ it("should resolve with expected value", async () => {
7
+ const deferred = createDeferred<string>();
8
+
9
+ deferred.resolve("expected-value");
10
+
11
+ const result = await deferred.promise;
12
+ expect(result).toBe("expected-value");
13
+ });
14
+
15
+ it("should reject with expected reason", async () => {
16
+ const deferred = createDeferred<string>();
17
+ const error = new Error("test-error");
18
+
19
+ deferred.reject(error);
20
+
21
+ await expect(deferred.promise).rejects.toBe(error);
22
+ });
23
+
24
+ it("should track settled state", async () => {
25
+ const deferred = createDeferred<string>();
26
+
27
+ expect(deferred.settled).toBe(false);
28
+
29
+ deferred.resolve("done");
30
+
31
+ expect(deferred.settled).toBe(true);
32
+ await deferred.promise;
33
+ });
34
+
35
+ it("should ignore second resolve", async () => {
36
+ const deferred = createDeferred<string>();
37
+
38
+ deferred.resolve("first");
39
+ deferred.resolve("second");
40
+
41
+ const result = await deferred.promise;
42
+ expect(result).toBe("first");
43
+ });
44
+
45
+ it("should ignore reject after resolve", async () => {
46
+ const deferred = createDeferred<string>();
47
+
48
+ deferred.resolve("value");
49
+ deferred.reject(new Error("ignored"));
50
+
51
+ const result = await deferred.promise;
52
+ expect(result).toBe("value");
53
+ });
54
+ });
55
+
56
+ describe("createDeferredQueue", () => {
57
+ it("should resolve deferreds in FIFO order", async () => {
58
+ const queue = createDeferredQueue<string>();
59
+
60
+ const d1 = queue.enqueue();
61
+ const d2 = queue.enqueue();
62
+ const d3 = queue.enqueue();
63
+
64
+ expect(queue.pending).toBe(3);
65
+
66
+ queue.resolveNext("first");
67
+ queue.resolveNext("second");
68
+ queue.resolveNext("third");
69
+
70
+ expect(queue.pending).toBe(0);
71
+
72
+ const results = await Promise.all([d1.promise, d2.promise, d3.promise]);
73
+
74
+ expect(results).toEqual(["first", "second", "third"]);
75
+ });
76
+
77
+ it("should reject deferreds in FIFO order", async () => {
78
+ const queue = createDeferredQueue<string>();
79
+
80
+ const d1 = queue.enqueue();
81
+ const d2 = queue.enqueue();
82
+
83
+ const error1 = new Error("error-1");
84
+ const error2 = new Error("error-2");
85
+
86
+ queue.rejectNext(error1);
87
+ queue.rejectNext(error2);
88
+
89
+ await expect(d1.promise).rejects.toBe(error1);
90
+ await expect(d2.promise).rejects.toBe(error2);
91
+ });
92
+
93
+ it("should return false when resolving empty queue", () => {
94
+ const queue = createDeferredQueue<string>();
95
+
96
+ const result = queue.resolveNext("value");
97
+
98
+ expect(result).toBe(false);
99
+ });
100
+
101
+ it("should return false when rejecting empty queue", () => {
102
+ const queue = createDeferredQueue<string>();
103
+
104
+ const result = queue.rejectNext(new Error("test"));
105
+
106
+ expect(result).toBe(false);
107
+ });
108
+
109
+ it("should handle mixed resolve and reject", async () => {
110
+ const queue = createDeferredQueue<string>();
111
+
112
+ const d1 = queue.enqueue();
113
+ const d2 = queue.enqueue();
114
+ const d3 = queue.enqueue();
115
+
116
+ queue.resolveNext("success");
117
+ queue.rejectNext(new Error("failure"));
118
+ queue.resolveNext("another-success");
119
+
120
+ const r1 = await d1.promise;
121
+ expect(r1).toBe("success");
122
+
123
+ await expect(d2.promise).rejects.toThrow("failure");
124
+
125
+ const r3 = await d3.promise;
126
+ expect(r3).toBe("another-success");
127
+ });
128
+ });
129
+
130
+ describe("nextTick", () => {
131
+ it("should allow pending promises to settle", async () => {
132
+ const deferred = createDeferred<string>();
133
+ let resolved = false;
134
+
135
+ deferred.promise.then(() => {
136
+ resolved = true;
137
+ });
138
+
139
+ deferred.resolve("done");
140
+
141
+ expect(resolved).toBe(false);
142
+
143
+ await nextTick();
144
+
145
+ expect(resolved).toBe(true);
146
+ });
147
+
148
+ it("should resolve after microtask queue", async () => {
149
+ const order: string[] = [];
150
+
151
+ Promise.resolve().then(() => order.push("promise-1"));
152
+ Promise.resolve().then(() => order.push("promise-2"));
153
+
154
+ await nextTick();
155
+
156
+ order.push("after-nextTick");
157
+
158
+ expect(order).toEqual(["promise-1", "promise-2", "after-nextTick"]);
159
+ });
160
+ });
161
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Controllable promises for concurrency tests.
3
+ *
4
+ * Provides utilities for creating deferred promises that can be resolved/rejected
5
+ * externally, and a FIFO queue for managing multiple deferred promises.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const deferred = createDeferred<string>();
10
+ * setTimeout(() => deferred.resolve('done'), 100);
11
+ * const result = await deferred.promise; // 'done'
12
+ * ```
13
+ */
14
+
15
+ export interface Deferred<T> {
16
+ /** The promise that will resolve/reject when called */
17
+ promise: Promise<T>;
18
+ /** Resolve the promise with a value */
19
+ resolve: (value: T | PromiseLike<T>) => void;
20
+ /** Reject the promise with a reason */
21
+ reject: (reason?: unknown) => void;
22
+ /** Whether the promise has settled (resolved or rejected) */
23
+ settled: boolean;
24
+ }
25
+
26
+ /**
27
+ * Creates a deferred promise with external resolve/reject controls.
28
+ *
29
+ * Similar to Promise.withResolvers() but with additional settled tracking.
30
+ *
31
+ * @returns Deferred object with promise, resolve, reject, and settled state
32
+ */
33
+ export function createDeferred<T>(): Deferred<T> {
34
+ let resolve: (value: T | PromiseLike<T>) => void;
35
+ let reject: (reason?: unknown) => void;
36
+ let settled = false;
37
+
38
+ const promise = new Promise<T>((res, rej) => {
39
+ resolve = (value) => {
40
+ if (!settled) {
41
+ settled = true;
42
+ res(value);
43
+ }
44
+ };
45
+ reject = (reason) => {
46
+ if (!settled) {
47
+ settled = true;
48
+ rej(reason);
49
+ }
50
+ };
51
+ });
52
+
53
+ return {
54
+ promise,
55
+ resolve: resolve!,
56
+ reject: reject!,
57
+ get settled() {
58
+ return settled;
59
+ },
60
+ };
61
+ }
62
+
63
+ export interface DeferredQueue<T> {
64
+ /** Add a new deferred to the queue */
65
+ enqueue: () => Deferred<T>;
66
+ /** Resolve the next deferred in FIFO order */
67
+ resolveNext: (value: T | PromiseLike<T>) => boolean;
68
+ /** Reject the next deferred in FIFO order */
69
+ rejectNext: (reason?: unknown) => boolean;
70
+ /** Number of pending deferreds in the queue */
71
+ pending: number;
72
+ }
73
+
74
+ /**
75
+ * Creates a FIFO queue of deferred promises.
76
+ *
77
+ * Useful for testing ordered async operations like request queues,
78
+ * sequential processing, or race conditions.
79
+ *
80
+ * @returns Queue with enqueue, resolveNext, rejectNext, and pending count
81
+ */
82
+ export function createDeferredQueue<T>(): DeferredQueue<T> {
83
+ const queue: Deferred<T>[] = [];
84
+
85
+ return {
86
+ enqueue: () => {
87
+ const deferred = createDeferred<T>();
88
+ queue.push(deferred);
89
+ return deferred;
90
+ },
91
+ resolveNext: (value) => {
92
+ const next = queue.shift();
93
+ if (next) {
94
+ next.resolve(value);
95
+ return true;
96
+ }
97
+ return false;
98
+ },
99
+ rejectNext: (reason) => {
100
+ const next = queue.shift();
101
+ if (next) {
102
+ next.reject(reason);
103
+ return true;
104
+ }
105
+ return false;
106
+ },
107
+ get pending() {
108
+ return queue.length;
109
+ },
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Waits for one microtask tick.
115
+ *
116
+ * Useful for allowing pending promises to settle before assertions.
117
+ *
118
+ * @returns Promise that resolves after one microtask
119
+ */
120
+ export function nextTick(): Promise<void> {
121
+ return Promise.resolve();
122
+ }