wraptc 1.0.2 → 1.0.4

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 (71) hide show
  1. package/bin/wraptc +4 -4
  2. package/package.json +2 -2
  3. package/src/cli/__tests__/cli.test.ts +337 -0
  4. package/src/cli/index.ts +149 -0
  5. package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
  6. package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
  7. package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
  8. package/src/core/__tests__/integration/integration.test.ts +241 -0
  9. package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
  10. package/src/core/__tests__/test-utils.ts +136 -0
  11. package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
  12. package/src/core/__tests__/unit/basic-test.test.ts +44 -0
  13. package/src/core/__tests__/unit/basic.test.ts +12 -0
  14. package/src/core/__tests__/unit/config.test.ts +244 -0
  15. package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
  16. package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
  17. package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
  18. package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
  19. package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
  20. package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
  21. package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
  22. package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
  23. package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
  24. package/src/core/__tests__/unit/router.test.ts +967 -0
  25. package/src/core/__tests__/unit/state.test.ts +1079 -0
  26. package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
  27. package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
  28. package/src/core/adapters/builtin/codex.ts +35 -0
  29. package/src/core/adapters/builtin/gemini.ts +34 -0
  30. package/src/core/adapters/builtin/index.ts +31 -0
  31. package/src/core/adapters/builtin/mock-coder.ts +148 -0
  32. package/src/core/adapters/builtin/qwen.ts +34 -0
  33. package/src/core/adapters/define.ts +48 -0
  34. package/src/core/adapters/index.ts +43 -0
  35. package/src/core/adapters/loader.ts +143 -0
  36. package/src/core/adapters/provider-bridge.ts +190 -0
  37. package/src/core/adapters/runner.ts +437 -0
  38. package/src/core/adapters/types.ts +172 -0
  39. package/src/core/config.ts +290 -0
  40. package/src/core/define-provider.ts +212 -0
  41. package/src/core/error-patterns.ts +147 -0
  42. package/src/core/index.ts +130 -0
  43. package/src/core/memory-monitor.ts +171 -0
  44. package/src/core/plugin/builtin.ts +87 -0
  45. package/src/core/plugin/index.ts +34 -0
  46. package/src/core/plugin/registry.ts +350 -0
  47. package/src/core/plugin/types.ts +209 -0
  48. package/src/core/provider-factory.ts +397 -0
  49. package/src/core/provider-loader.ts +171 -0
  50. package/src/core/providers/codex.ts +56 -0
  51. package/src/core/providers/configurable.ts +637 -0
  52. package/src/core/providers/custom.ts +261 -0
  53. package/src/core/providers/gemini.ts +41 -0
  54. package/src/core/providers/index.ts +383 -0
  55. package/src/core/providers/opencode.ts +168 -0
  56. package/src/core/providers/qwen-code.ts +41 -0
  57. package/src/core/router.ts +370 -0
  58. package/src/core/state.ts +258 -0
  59. package/src/core/types.ts +206 -0
  60. package/src/core/unified/capabilities.ts +184 -0
  61. package/src/core/unified/errors.ts +141 -0
  62. package/src/core/unified/index.ts +29 -0
  63. package/src/core/unified/output.ts +189 -0
  64. package/src/core/wrap-terminalcoder.ts +245 -0
  65. package/src/mcp/__tests__/server.test.ts +295 -0
  66. package/src/mcp/server.ts +284 -0
  67. package/src/test-fixtures/mock-coder.sh +194 -0
  68. package/dist/cli/index.js +0 -16501
  69. package/dist/core/index.js +0 -7531
  70. package/dist/mcp/server.js +0 -14568
  71. package/dist/wraptc-1.0.2.tgz +0 -0
@@ -0,0 +1,1079 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { StateManager } from "../../state";
5
+ import type { FullState } from "../../types";
6
+
7
+ describe("StateManager", () => {
8
+ let stateManager: StateManager;
9
+ let tempStatePath: string;
10
+ let tempStateDir: string;
11
+ let originalHome: string | undefined;
12
+
13
+ beforeEach(async () => {
14
+ // Use unique paths for each test to prevent interference
15
+ const testId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
16
+ tempStatePath = `/tmp/test-state-${testId}.json`;
17
+ tempStateDir = `/tmp/test-state-dir-${testId}`;
18
+
19
+ // Mock HOME to prevent conflicts with real user config
20
+ originalHome = process.env.HOME;
21
+ process.env.HOME = `/tmp/test-home-${testId}`;
22
+
23
+ // Clean up any existing test files (shouldn't exist but be safe)
24
+ try {
25
+ await unlink(tempStatePath);
26
+ } catch {
27
+ // File doesn't exist, that's fine
28
+ }
29
+
30
+ try {
31
+ await unlink(join(tempStateDir, "state.json"));
32
+ } catch {
33
+ // File doesn't exist, that's fine
34
+ }
35
+
36
+ stateManager = new StateManager({ statePath: tempStatePath });
37
+ });
38
+
39
+ afterEach(async () => {
40
+ // Restore original HOME
41
+ process.env.HOME = originalHome;
42
+
43
+ // Clean up test files
44
+ try {
45
+ await unlink(tempStatePath);
46
+ } catch {
47
+ // File doesn't exist, that's fine
48
+ }
49
+
50
+ try {
51
+ await unlink(join(tempStateDir, "state.json"));
52
+ } catch {
53
+ // File doesn't exist, that's fine
54
+ }
55
+ });
56
+
57
+ describe("Basic functionality", () => {
58
+ test("can create StateManager instance", () => {
59
+ expect(stateManager).toBeDefined();
60
+ expect(stateManager).toBeInstanceOf(StateManager);
61
+ });
62
+
63
+ test("has getState method", () => {
64
+ expect(typeof (stateManager as any).getState).toBe("function");
65
+ });
66
+
67
+ test("has getStatePath method", () => {
68
+ expect(typeof (stateManager as any).getStatePath).toBe("function");
69
+ });
70
+
71
+ test("can call getState", () => {
72
+ const state = (stateManager as any).getState();
73
+ expect(state).toBeDefined();
74
+ expect(state.version).toBe("1.0.0");
75
+ expect(state.providers).toEqual({});
76
+ });
77
+
78
+ test("can call getStatePath", () => {
79
+ const path = (stateManager as any).getStatePath();
80
+ expect(path).toBe(tempStatePath);
81
+ });
82
+ });
83
+
84
+ describe("Initialization", () => {
85
+ test("creates initial state when file doesn't exist", async () => {
86
+ await stateManager.initialize();
87
+
88
+ const state = (stateManager as any).getState();
89
+ expect(state).toBeDefined();
90
+ expect(state.version).toBe("1.0.0");
91
+ expect(state.providers).toEqual({});
92
+ });
93
+
94
+ test("loads existing state from file", async () => {
95
+ // Create a test state file with today's date to prevent reset
96
+ const today = new Date();
97
+ const todayISO = new Date(
98
+ today.getFullYear(),
99
+ today.getMonth(),
100
+ today.getDate(),
101
+ ).toISOString();
102
+
103
+ const testState: FullState = {
104
+ version: "1.0.0",
105
+ providers: {
106
+ "test-provider": {
107
+ lastUsedAt: todayISO,
108
+ requestsToday: 5,
109
+ lastReset: todayISO,
110
+ outOfCreditsUntil: undefined,
111
+ lastErrors: ["error1", "error2"],
112
+ },
113
+ },
114
+ };
115
+
116
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
117
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
118
+
119
+ // Create new state manager to load the file
120
+ const newStateManager = new StateManager({ statePath: tempStatePath });
121
+ await newStateManager.initialize();
122
+
123
+ const state = (newStateManager as any).getState();
124
+ expect(state).toBeDefined();
125
+ expect(state.version).toBe("1.0.0");
126
+ expect(state.providers["test-provider"]).toBeDefined();
127
+ expect(state.providers["test-provider"].requestsToday).toBe(5);
128
+ expect(state.providers["test-provider"].lastErrors).toEqual(["error1", "error2"]);
129
+ });
130
+
131
+ test("handles invalid JSON in state file gracefully", async () => {
132
+ // Create an invalid state file
133
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
134
+ await writeFile(tempStatePath, '{"invalid": json}', "utf-8");
135
+
136
+ // Should initialize gracefully with fresh state (logs error but doesn't throw)
137
+ await stateManager.initialize();
138
+
139
+ // Should have fresh initial state
140
+ const state = stateManager.getState();
141
+ expect(state.version).toBe("1.0.0");
142
+ expect(Object.keys(state.providers)).toHaveLength(0);
143
+ });
144
+
145
+ test("handles file read errors gracefully", async () => {
146
+ // This test acknowledges that some file system errors may occur
147
+ // but the state manager should handle them gracefully
148
+ expect(true).toBe(true); // Placeholder - actual testing would require mocking
149
+ });
150
+ });
151
+
152
+ describe("getProviderState", () => {
153
+ test("can call getProviderState for new provider", async () => {
154
+ const state = await (stateManager as any).getProviderState("test-provider");
155
+ expect(state).toBeDefined();
156
+ expect(state.requestsToday).toBe(0);
157
+ expect(state.lastErrors).toEqual([]);
158
+ expect(state.outOfCreditsUntil).toBeUndefined();
159
+ });
160
+
161
+ test("returns existing provider state", async () => {
162
+ // First call creates the provider
163
+ await (stateManager as any).getProviderState("test-provider");
164
+
165
+ // Second call should return the same state
166
+ const state = await (stateManager as any).getProviderState("test-provider");
167
+ expect(state).toBeDefined();
168
+ expect(state.requestsToday).toBe(0);
169
+ });
170
+ });
171
+
172
+ describe("recordSuccess", () => {
173
+ test("can call recordSuccess", async () => {
174
+ await (stateManager as any).recordSuccess("test-provider");
175
+ });
176
+
177
+ test("increments request count", async () => {
178
+ await (stateManager as any).recordSuccess("test-provider");
179
+ const state = await (stateManager as any).getProviderState("test-provider");
180
+ expect(state.requestsToday).toBe(1);
181
+ });
182
+
183
+ test("updates lastUsedAt", async () => {
184
+ const before = new Date();
185
+ await (stateManager as any).recordSuccess("test-provider");
186
+ const after = new Date();
187
+
188
+ const state = await (stateManager as any).getProviderState("test-provider");
189
+ expect(state.lastUsedAt).toBeDefined();
190
+ const lastUsedAt = new Date(state.lastUsedAt!);
191
+ expect(lastUsedAt.getTime()).toBeGreaterThanOrEqual(before.getTime());
192
+ expect(lastUsedAt.getTime()).toBeLessThanOrEqual(after.getTime());
193
+ });
194
+
195
+ test("clears lastErrors on success", async () => {
196
+ // Add an error first
197
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "test error");
198
+
199
+ // Then record success
200
+ await (stateManager as any).recordSuccess("test-provider");
201
+
202
+ const state = await (stateManager as any).getProviderState("test-provider");
203
+ expect(state.lastErrors).toEqual([]);
204
+ });
205
+ });
206
+
207
+ describe("recordError", () => {
208
+ test("can call recordError", async () => {
209
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "test error");
210
+ });
211
+
212
+ test("records error in lastErrors", async () => {
213
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "test error");
214
+ const state = await (stateManager as any).getProviderState("test-provider");
215
+ expect(state.lastErrors.length).toBe(1);
216
+ expect(state.lastErrors[0]).toContain("TRANSIENT");
217
+ expect(state.lastErrors[0]).toContain("test error");
218
+ });
219
+
220
+ test("limits error history to 10 errors", async () => {
221
+ // Add 15 errors
222
+ for (let i = 0; i < 15; i++) {
223
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", `error ${i}`);
224
+ }
225
+
226
+ const state = await (stateManager as any).getProviderState("test-provider");
227
+ expect(state.lastErrors.length).toBe(10);
228
+ // Should keep the last 10 errors
229
+ expect(state.lastErrors[0]).toContain("error 5");
230
+ expect(state.lastErrors[9]).toContain("error 14");
231
+ });
232
+ });
233
+
234
+ describe("markOutOfCredits", () => {
235
+ test("can call markOutOfCredits", async () => {
236
+ const futureDate = new Date(Date.now() + 3600000);
237
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
238
+ });
239
+
240
+ test("sets outOfCreditsUntil", async () => {
241
+ const futureDate = new Date(Date.now() + 3600000);
242
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
243
+ const state = await (stateManager as any).getProviderState("test-provider");
244
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
245
+ });
246
+ });
247
+
248
+ describe("resetProvider", () => {
249
+ test("can call resetProvider", async () => {
250
+ await (stateManager as any).resetProvider("test-provider");
251
+ });
252
+
253
+ test("resets provider state", async () => {
254
+ // Set up some state
255
+ await (stateManager as any).recordSuccess("test-provider");
256
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "error");
257
+ const futureDate = new Date(Date.now() + 3600000);
258
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
259
+
260
+ // Reset
261
+ await (stateManager as any).resetProvider("test-provider");
262
+
263
+ const state = await (stateManager as any).getProviderState("test-provider");
264
+ expect(state.requestsToday).toBe(0);
265
+ expect(state.lastErrors).toEqual([]);
266
+ expect(state.outOfCreditsUntil).toBeUndefined();
267
+ });
268
+ });
269
+
270
+ describe("resetAll", () => {
271
+ test("can call resetAll", async () => {
272
+ await (stateManager as any).resetAll();
273
+ });
274
+
275
+ test("resets all state", async () => {
276
+ // Add some state
277
+ await (stateManager as any).getProviderState("provider1");
278
+ await (stateManager as any).getProviderState("provider2");
279
+
280
+ await (stateManager as any).resetAll();
281
+
282
+ const state = (stateManager as any).getState();
283
+ expect(state.providers).toEqual({});
284
+ });
285
+ });
286
+
287
+ describe("Persistence and File Operations", () => {
288
+ test("saves state to file", async () => {
289
+ // Record some activity
290
+ await (stateManager as any).recordSuccess("test-provider");
291
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "test error");
292
+
293
+ // Force immediate save by calling save directly
294
+ await (stateManager as any).save();
295
+
296
+ // Check that file was created
297
+ const fileContent = await readFile(tempStatePath, "utf-8");
298
+ const savedState = JSON.parse(fileContent);
299
+
300
+ expect(savedState).toBeDefined();
301
+ expect(savedState.version).toBe("1.0.0");
302
+ expect(savedState.providers["test-provider"]).toBeDefined();
303
+ expect(savedState.providers["test-provider"].requestsToday).toBe(1);
304
+ expect(savedState.providers["test-provider"].lastErrors.length).toBe(1);
305
+ });
306
+
307
+ test("performs atomic file operations", async () => {
308
+ // Record some activity
309
+ await (stateManager as any).recordSuccess("test-provider");
310
+
311
+ // Force immediate save
312
+ await (stateManager as any).save();
313
+
314
+ // Check that temp file was cleaned up
315
+ let tempFileExists = false;
316
+ try {
317
+ await readFile(`${tempStatePath}.tmp`, "utf-8");
318
+ tempFileExists = true;
319
+ } catch (error) {
320
+ // This is expected - temp file should not exist
321
+ expect((error as NodeJS.ErrnoException).code).toBe("ENOENT");
322
+ }
323
+
324
+ if (tempFileExists) {
325
+ throw new Error("Temporary file should have been cleaned up");
326
+ }
327
+
328
+ // Check that main file exists
329
+ const fileContent = await readFile(tempStatePath, "utf-8");
330
+ const savedState = JSON.parse(fileContent);
331
+ expect(savedState.providers["test-provider"]).toBeDefined();
332
+ });
333
+
334
+ test("creates directory structure if it doesn't exist", async () => {
335
+ const nestedPath = join(tempStateDir, "subdir", "state.json");
336
+ const nestedStateManager = new StateManager({ statePath: nestedPath });
337
+
338
+ // Record some activity
339
+ await (nestedStateManager as any).recordSuccess("test-provider");
340
+
341
+ // Force immediate save
342
+ await (nestedStateManager as any).save();
343
+
344
+ // Check that file was created in nested directory
345
+ const fileContent = await readFile(nestedPath, "utf-8");
346
+ const savedState = JSON.parse(fileContent);
347
+ expect(savedState.providers["test-provider"]).toBeDefined();
348
+ });
349
+
350
+ test("handles file write errors gracefully", async () => {
351
+ // Create state manager with a path in a read-only directory
352
+ // This is a bit tricky to test without actually having a read-only directory
353
+ // We'll acknowledge this is tested indirectly through other mechanisms
354
+ expect(true).toBe(true); // Placeholder - actual testing would require specific setup
355
+ });
356
+
357
+ test("debounces saves to avoid too many writes", async () => {
358
+ // Record multiple activities quickly
359
+ await (stateManager as any).recordSuccess("test-provider-1");
360
+ await (stateManager as any).recordSuccess("test-provider-2");
361
+ await (stateManager as any).recordError("test-provider-1", "TRANSIENT", "test error");
362
+
363
+ // Wait for debounce period to complete
364
+ await new Promise((resolve) => setTimeout(resolve, 1100));
365
+
366
+ // Check that file was saved
367
+ const fileContent = await readFile(tempStatePath, "utf-8");
368
+ const savedState = JSON.parse(fileContent);
369
+
370
+ expect(savedState.providers["test-provider-1"]).toBeDefined();
371
+ expect(savedState.providers["test-provider-2"]).toBeDefined();
372
+ expect(savedState.providers["test-provider-1"].lastErrors.length).toBe(1);
373
+ });
374
+ });
375
+
376
+ describe("Daily Counter Reset", () => {
377
+ test("resets daily counters when needed", async () => {
378
+ // Create a state file with old data
379
+ const yesterday = new Date();
380
+ yesterday.setDate(yesterday.getDate() - 1);
381
+ const yesterdayISO = new Date(
382
+ yesterday.getFullYear(),
383
+ yesterday.getMonth(),
384
+ yesterday.getDate(),
385
+ ).toISOString();
386
+
387
+ const testState: FullState = {
388
+ version: "1.0.0",
389
+ providers: {
390
+ "test-provider": {
391
+ lastUsedAt: yesterday.toISOString(),
392
+ requestsToday: 10,
393
+ lastReset: yesterdayISO,
394
+ outOfCreditsUntil: new Date(Date.now() + 3600000).toISOString(),
395
+ lastErrors: ["old error"],
396
+ },
397
+ },
398
+ };
399
+
400
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
401
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
402
+
403
+ // Create new state manager to load the file
404
+ const newStateManager = new StateManager({ statePath: tempStatePath });
405
+ await newStateManager.initialize();
406
+
407
+ const state = (newStateManager as any).getState();
408
+ const providerState = state.providers["test-provider"];
409
+
410
+ // Daily counters should be reset because lastReset was yesterday
411
+ expect(providerState.requestsToday).toBe(0);
412
+ expect(providerState.outOfCreditsUntil).toBeUndefined();
413
+ // But errors should be preserved until explicitly cleared
414
+ expect(providerState.lastErrors).toEqual(["old error"]);
415
+ });
416
+
417
+ test("preserves daily counters when not needed", async () => {
418
+ // Create a state file with today's data
419
+ const today = new Date();
420
+ const todayISO = new Date(
421
+ today.getFullYear(),
422
+ today.getMonth(),
423
+ today.getDate(),
424
+ ).toISOString();
425
+
426
+ const testState: FullState = {
427
+ version: "1.0.0",
428
+ providers: {
429
+ "test-provider": {
430
+ lastUsedAt: today.toISOString(),
431
+ requestsToday: 5,
432
+ lastReset: todayISO,
433
+ outOfCreditsUntil: undefined,
434
+ lastErrors: ["recent error"],
435
+ },
436
+ },
437
+ };
438
+
439
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
440
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
441
+
442
+ // Create new state manager to load the file
443
+ const newStateManager = new StateManager({ statePath: tempStatePath });
444
+ await newStateManager.initialize();
445
+
446
+ const state = (newStateManager as any).getState();
447
+ const providerState = state.providers["test-provider"];
448
+
449
+ // Daily counters should be preserved because lastReset was today
450
+ expect(providerState.requestsToday).toBe(5);
451
+ expect(providerState.lastErrors).toEqual(["recent error"]);
452
+ });
453
+
454
+ test("manual reset clears provider counters", async () => {
455
+ // Set up some state
456
+ await (stateManager as any).recordSuccess("test-provider");
457
+ await (stateManager as any).recordSuccess("test-provider");
458
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "test error");
459
+
460
+ const futureDate = new Date(Date.now() + 3600000);
461
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
462
+
463
+ // Verify state before reset
464
+ let state = await (stateManager as any).getProviderState("test-provider");
465
+ expect(state.requestsToday).toBe(2);
466
+ expect(state.lastErrors.length).toBe(1);
467
+ expect(state.outOfCreditsUntil).toBeDefined();
468
+
469
+ // Manual reset
470
+ await (stateManager as any).resetProvider("test-provider");
471
+
472
+ // Verify state after reset
473
+ state = await (stateManager as any).getProviderState("test-provider");
474
+ expect(state.requestsToday).toBe(0);
475
+ expect(state.lastErrors).toEqual([]);
476
+ expect(state.outOfCreditsUntil).toBeUndefined();
477
+ });
478
+
479
+ test("manual reset preserves other provider states", async () => {
480
+ // Set up state for two providers
481
+ await (stateManager as any).recordSuccess("provider-1");
482
+ await (stateManager as any).recordSuccess("provider-1");
483
+ await (stateManager as any).recordSuccess("provider-2");
484
+ await (stateManager as any).recordError("provider-1", "TRANSIENT", "error1");
485
+ await (stateManager as any).recordError("provider-2", "PERMANENT", "error2");
486
+
487
+ // Verify state before reset
488
+ let state1 = await (stateManager as any).getProviderState("provider-1");
489
+ let state2 = await (stateManager as any).getProviderState("provider-2");
490
+ expect(state1.requestsToday).toBe(2);
491
+ expect(state2.requestsToday).toBe(1);
492
+ expect(state1.lastErrors.length).toBe(1);
493
+ expect(state2.lastErrors.length).toBe(1);
494
+
495
+ // Reset only provider-1
496
+ await (stateManager as any).resetProvider("provider-1");
497
+
498
+ // Verify that provider-1 is reset but provider-2 is unchanged
499
+ state1 = await (stateManager as any).getProviderState("provider-1");
500
+ state2 = await (stateManager as any).getProviderState("provider-2");
501
+ expect(state1.requestsToday).toBe(0);
502
+ expect(state1.lastErrors).toEqual([]);
503
+ expect(state2.requestsToday).toBe(1);
504
+ expect(state2.lastErrors.length).toBe(1);
505
+ expect(state2.lastErrors[0]).toContain("error2");
506
+ });
507
+
508
+ test("counter persistence across multiple operations", async () => {
509
+ // Record several successes
510
+ await (stateManager as any).recordSuccess("test-provider");
511
+ await (stateManager as any).recordSuccess("test-provider");
512
+ await (stateManager as any).recordSuccess("test-provider");
513
+
514
+ // Check counter
515
+ let state = await (stateManager as any).getProviderState("test-provider");
516
+ expect(state.requestsToday).toBe(3);
517
+ expect(state.lastErrors.length).toBe(0); // No errors yet
518
+
519
+ // Record an error (should not affect counter)
520
+ await (stateManager as any).recordError("test-provider", "TRANSIENT", "test error");
521
+ state = await (stateManager as any).getProviderState("test-provider");
522
+ expect(state.requestsToday).toBe(3);
523
+ expect(state.lastErrors.length).toBe(1);
524
+
525
+ // Record more successes (this should clear errors)
526
+ await (stateManager as any).recordSuccess("test-provider");
527
+ await (stateManager as any).recordSuccess("test-provider");
528
+
529
+ // Check final counter
530
+ state = await (stateManager as any).getProviderState("test-provider");
531
+ expect(state.requestsToday).toBe(5);
532
+ expect(state.lastErrors.length).toBe(0); // Errors should be cleared by success
533
+ });
534
+
535
+ test("edge case: midnight transition", async () => {
536
+ // Create a state file with yesterday's date at 23:59:59
537
+ const yesterday = new Date();
538
+ yesterday.setDate(yesterday.getDate() - 1);
539
+ yesterday.setHours(23, 59, 59, 999); // Just before midnight
540
+
541
+ const yesterdayMidnight = new Date(
542
+ yesterday.getFullYear(),
543
+ yesterday.getMonth(),
544
+ yesterday.getDate(),
545
+ );
546
+
547
+ const testState: FullState = {
548
+ version: "1.0.0",
549
+ providers: {
550
+ "test-provider": {
551
+ lastUsedAt: yesterday.toISOString(),
552
+ requestsToday: 15,
553
+ lastReset: yesterdayMidnight.toISOString(),
554
+ outOfCreditsUntil: undefined,
555
+ lastErrors: ["midnight error"],
556
+ },
557
+ },
558
+ };
559
+
560
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
561
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
562
+
563
+ // Create new state manager to load the file
564
+ const newStateManager = new StateManager({ statePath: tempStatePath });
565
+ await newStateManager.initialize();
566
+
567
+ const state = (newStateManager as any).getState();
568
+ const providerState = state.providers["test-provider"];
569
+
570
+ // Counters should be reset because we crossed midnight
571
+ expect(providerState.requestsToday).toBe(0);
572
+ expect(providerState.lastErrors).toEqual(["midnight error"]); // Preserved
573
+ });
574
+
575
+ test("edge case: provider with no lastReset date", async () => {
576
+ // Create a state file with a provider that has no lastReset date
577
+ const testState: FullState = {
578
+ version: "1.0.0",
579
+ providers: {
580
+ "test-provider": {
581
+ lastUsedAt: new Date().toISOString(),
582
+ requestsToday: 7,
583
+ lastReset: undefined,
584
+ outOfCreditsUntil: undefined,
585
+ lastErrors: ["no-reset error"],
586
+ },
587
+ },
588
+ };
589
+
590
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
591
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
592
+
593
+ // Create new state manager to load the file
594
+ const newStateManager = new StateManager({ statePath: tempStatePath });
595
+ await newStateManager.initialize();
596
+
597
+ const state = (newStateManager as any).getState();
598
+ const providerState = state.providers["test-provider"];
599
+
600
+ // Counters should be reset because there was no lastReset date
601
+ expect(providerState.requestsToday).toBe(0);
602
+ expect(providerState.lastErrors).toEqual(["no-reset error"]); // Preserved
603
+ });
604
+
605
+ test("edge case: multiple providers with different reset needs", async () => {
606
+ const yesterday = new Date();
607
+ yesterday.setDate(yesterday.getDate() - 1);
608
+ const yesterdayISO = new Date(
609
+ yesterday.getFullYear(),
610
+ yesterday.getMonth(),
611
+ yesterday.getDate(),
612
+ ).toISOString();
613
+
614
+ const today = new Date();
615
+ const todayISO = new Date(
616
+ today.getFullYear(),
617
+ today.getMonth(),
618
+ today.getDate(),
619
+ ).toISOString();
620
+
621
+ const testState: FullState = {
622
+ version: "1.0.0",
623
+ providers: {
624
+ "yesterday-provider": {
625
+ lastUsedAt: yesterday.toISOString(),
626
+ requestsToday: 12,
627
+ lastReset: yesterdayISO,
628
+ outOfCreditsUntil: new Date(Date.now() + 3600000).toISOString(),
629
+ lastErrors: ["yesterday error"],
630
+ },
631
+ "today-provider": {
632
+ lastUsedAt: today.toISOString(),
633
+ requestsToday: 8,
634
+ lastReset: todayISO,
635
+ outOfCreditsUntil: undefined,
636
+ lastErrors: ["today error"],
637
+ },
638
+ "no-reset-provider": {
639
+ lastUsedAt: today.toISOString(),
640
+ requestsToday: 5,
641
+ lastReset: undefined,
642
+ outOfCreditsUntil: undefined,
643
+ lastErrors: ["no reset error"],
644
+ },
645
+ },
646
+ };
647
+
648
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
649
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
650
+
651
+ // Create new state manager to load the file
652
+ const newStateManager = new StateManager({ statePath: tempStatePath });
653
+ await newStateManager.initialize();
654
+
655
+ const state = (newStateManager as any).getState();
656
+
657
+ // yesterday-provider and no-reset-provider should be reset
658
+ const yesterdayProvider = state.providers["yesterday-provider"];
659
+ expect(yesterdayProvider.requestsToday).toBe(0);
660
+ expect(yesterdayProvider.outOfCreditsUntil).toBeUndefined();
661
+ expect(yesterdayProvider.lastErrors).toEqual(["yesterday error"]);
662
+
663
+ // today-provider should be preserved
664
+ const todayProvider = state.providers["today-provider"];
665
+ expect(todayProvider.requestsToday).toBe(8);
666
+ expect(todayProvider.lastErrors).toEqual(["today error"]);
667
+
668
+ // no-reset-provider should be reset
669
+ const noResetProvider = state.providers["no-reset-provider"];
670
+ expect(noResetProvider.requestsToday).toBe(0);
671
+ expect(noResetProvider.lastErrors).toEqual(["no reset error"]);
672
+ });
673
+ });
674
+
675
+ describe("Multiple Providers State Recording", () => {
676
+ test("tracks state for multiple providers independently", async () => {
677
+ // Record activity for multiple providers
678
+ await (stateManager as any).recordSuccess("provider-a");
679
+ await (stateManager as any).recordSuccess("provider-a");
680
+ await (stateManager as any).recordSuccess("provider-b");
681
+ await (stateManager as any).recordError("provider-a", "TRANSIENT", "error a");
682
+ await (stateManager as any).recordError("provider-b", "PERMANENT", "error b");
683
+ await (stateManager as any).recordError("provider-b", "TRANSIENT", "error b2");
684
+
685
+ // Check provider-a state
686
+ const stateA = await (stateManager as any).getProviderState("provider-a");
687
+ expect(stateA.requestsToday).toBe(2);
688
+ expect(stateA.lastErrors.length).toBe(1);
689
+ expect(stateA.lastErrors[0]).toContain("error a");
690
+
691
+ // Check provider-b state
692
+ const stateB = await (stateManager as any).getProviderState("provider-b");
693
+ expect(stateB.requestsToday).toBe(1);
694
+ expect(stateB.lastErrors.length).toBe(2);
695
+ expect(stateB.lastErrors[0]).toContain("error b");
696
+ expect(stateB.lastErrors[1]).toContain("error b2");
697
+
698
+ // Record more successes for provider-a (should clear errors)
699
+ await (stateManager as any).recordSuccess("provider-a");
700
+ const updatedStateA = await (stateManager as any).getProviderState("provider-a");
701
+ expect(updatedStateA.requestsToday).toBe(3);
702
+ expect(updatedStateA.lastErrors.length).toBe(0);
703
+ });
704
+
705
+ test("maintains separate out of credits states", async () => {
706
+ const futureDateA = new Date(Date.now() + 3600000);
707
+ const futureDateB = new Date(Date.now() + 7200000);
708
+
709
+ await (stateManager as any).markOutOfCredits("provider-a", futureDateA);
710
+ await (stateManager as any).markOutOfCredits("provider-b", futureDateB);
711
+
712
+ const stateA = await (stateManager as any).getProviderState("provider-a");
713
+ const stateB = await (stateManager as any).getProviderState("provider-b");
714
+
715
+ expect(stateA.outOfCreditsUntil).toBe(futureDateA.toISOString());
716
+ expect(stateB.outOfCreditsUntil).toBe(futureDateB.toISOString());
717
+ });
718
+
719
+ test("resets individual providers without affecting others", async () => {
720
+ // Set up state for multiple providers
721
+ await (stateManager as any).recordSuccess("provider-a");
722
+ await (stateManager as any).recordSuccess("provider-a");
723
+ await (stateManager as any).recordSuccess("provider-b");
724
+ await (stateManager as any).recordError("provider-a", "TRANSIENT", "error a");
725
+ await (stateManager as any).recordError("provider-b", "PERMANENT", "error b");
726
+
727
+ // Reset only provider-a
728
+ await (stateManager as any).resetProvider("provider-a");
729
+
730
+ // Check provider-a is reset
731
+ const stateA = await (stateManager as any).getProviderState("provider-a");
732
+ expect(stateA.requestsToday).toBe(0);
733
+ expect(stateA.lastErrors.length).toBe(0);
734
+
735
+ // Check provider-b is unaffected
736
+ const stateB = await (stateManager as any).getProviderState("provider-b");
737
+ expect(stateB.requestsToday).toBe(1);
738
+ expect(stateB.lastErrors.length).toBe(1);
739
+ expect(stateB.lastErrors[0]).toContain("error b");
740
+ });
741
+ });
742
+
743
+ describe("Credit Limit Enforcement", () => {
744
+ test("correctly tracks out of credits state", async () => {
745
+ const futureDate = new Date(Date.now() + 3600000); // 1 hour in the future
746
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
747
+
748
+ const state = await (stateManager as any).getProviderState("test-provider");
749
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
750
+ });
751
+
752
+ test("allows clearing out of credits state through reset", async () => {
753
+ const futureDate = new Date(Date.now() + 3600000);
754
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
755
+
756
+ // Verify it's set
757
+ let state = await (stateManager as any).getProviderState("test-provider");
758
+ expect(state.outOfCreditsUntil).toBeDefined();
759
+
760
+ // Reset provider
761
+ await (stateManager as any).resetProvider("test-provider");
762
+
763
+ // Verify it's cleared
764
+ state = await (stateManager as any).getProviderState("test-provider");
765
+ expect(state.outOfCreditsUntil).toBeUndefined();
766
+ });
767
+
768
+ test("persists out of credits state across sessions", async () => {
769
+ const futureDate = new Date(Date.now() + 3600000);
770
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
771
+
772
+ // Set lastReset to today to prevent daily reset from clearing outOfCreditsUntil
773
+ const providerState = await (stateManager as any).getProviderState("test-provider");
774
+ const today = new Date();
775
+ const todayISO = new Date(
776
+ today.getFullYear(),
777
+ today.getMonth(),
778
+ today.getDate(),
779
+ ).toISOString();
780
+ providerState.lastReset = todayISO;
781
+
782
+ // Force immediate save
783
+ await (stateManager as any).save();
784
+
785
+ // Create new state manager to simulate new session
786
+ const newStateManager = new StateManager({ statePath: tempStatePath });
787
+ await newStateManager.initialize();
788
+
789
+ const state = await (newStateManager as any).getProviderState("test-provider");
790
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
791
+ });
792
+ });
793
+
794
+ describe("Out of Credits Functionality", () => {
795
+ test("detects when provider is out of credits", async () => {
796
+ const futureDate = new Date(Date.now() + 3600000); // 1 hour in the future
797
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
798
+ await (stateManager as any).save(); // Force immediate save
799
+
800
+ const state = await (stateManager as any).getProviderState("test-provider");
801
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
802
+
803
+ // Verify that the out of credits state is properly detected
804
+ const isOutOfCredits =
805
+ state.outOfCreditsUntil && new Date(state.outOfCreditsUntil) > new Date();
806
+ expect(isOutOfCredits).toBe(true);
807
+ });
808
+
809
+ test("allows requests when provider is not out of credits", async () => {
810
+ const pastDate = new Date(Date.now() - 3600000); // 1 hour ago
811
+ await (stateManager as any).markOutOfCredits("test-provider", pastDate);
812
+ await (stateManager as any).save(); // Force immediate save
813
+
814
+ const state = await (stateManager as any).getProviderState("test-provider");
815
+ expect(state.outOfCreditsUntil).toBe(pastDate.toISOString());
816
+
817
+ // Verify that the out of credits state is properly detected as expired
818
+ const isOutOfCredits =
819
+ state.outOfCreditsUntil && new Date(state.outOfCreditsUntil) > new Date();
820
+ expect(isOutOfCredits).toBe(false);
821
+ });
822
+
823
+ test("enforces credit limits by marking provider as out of credits", async () => {
824
+ // Simulate reaching daily limit
825
+ for (let i = 0; i < 10; i++) {
826
+ await (stateManager as any).recordSuccess("test-provider");
827
+ }
828
+ await (stateManager as any).save(); // Force immediate save
829
+
830
+ const state = await (stateManager as any).getProviderState("test-provider");
831
+ expect(state.requestsToday).toBe(10);
832
+
833
+ // Mark as out of credits
834
+ const futureDate = new Date(Date.now() + 86400000); // 24 hours in the future
835
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
836
+ await (stateManager as any).save(); // Force immediate save
837
+
838
+ const updatedState = await (stateManager as any).getProviderState("test-provider");
839
+ expect(updatedState.outOfCreditsUntil).toBe(futureDate.toISOString());
840
+ });
841
+ });
842
+
843
+ describe("Out of Credits State Persistence", () => {
844
+ test("persists out of credits state to file", async () => {
845
+ const futureDate = new Date(Date.now() + 3600000);
846
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
847
+
848
+ // Force immediate save
849
+ await (stateManager as any).save();
850
+
851
+ // Read the file directly to verify persistence
852
+ const fileContent = await readFile(tempStatePath, "utf-8");
853
+ const savedState = JSON.parse(fileContent);
854
+
855
+ expect(savedState.providers["test-provider"]).toBeDefined();
856
+ expect(savedState.providers["test-provider"].outOfCreditsUntil).toBe(
857
+ futureDate.toISOString(),
858
+ );
859
+ });
860
+
861
+ test("loads out of credits state from file", async () => {
862
+ const futureDate = new Date(Date.now() + 3600000);
863
+ const testState = {
864
+ version: "1.0.0",
865
+ providers: {
866
+ "test-provider": {
867
+ lastUsedAt: new Date().toISOString(),
868
+ requestsToday: 5,
869
+ lastReset: new Date().toISOString(),
870
+ outOfCreditsUntil: futureDate.toISOString(),
871
+ lastErrors: [],
872
+ },
873
+ },
874
+ };
875
+
876
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
877
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
878
+
879
+ // Create new state manager to load the file
880
+ const newStateManager = new StateManager({ statePath: tempStatePath });
881
+ await newStateManager.initialize();
882
+
883
+ const state = await (newStateManager as any).getProviderState("test-provider");
884
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
885
+ });
886
+
887
+ test("preserves out of credits state across multiple sessions", async () => {
888
+ const futureDate = new Date(Date.now() + 86400000); // 24 hours in the future
889
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
890
+
891
+ // Set lastReset to today to prevent daily reset from clearing outOfCreditsUntil
892
+ const state = await (stateManager as any).getProviderState("test-provider");
893
+ const today = new Date();
894
+ const todayISO = new Date(
895
+ today.getFullYear(),
896
+ today.getMonth(),
897
+ today.getDate(),
898
+ ).toISOString();
899
+ state.lastReset = todayISO;
900
+
901
+ // Force save
902
+ await (stateManager as any).save();
903
+
904
+ // Create multiple state managers to simulate multiple sessions
905
+ for (let i = 0; i < 3; i++) {
906
+ const newStateManager = new StateManager({ statePath: tempStatePath });
907
+ await newStateManager.initialize();
908
+
909
+ const state = await (newStateManager as any).getProviderState("test-provider");
910
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
911
+ }
912
+ });
913
+ });
914
+
915
+ describe("Credit Reset Functionality", () => {
916
+ test("resets out of credits state when provider is reset", async () => {
917
+ const futureDate = new Date(Date.now() + 3600000);
918
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
919
+
920
+ // Verify state before reset
921
+ let state = await (stateManager as any).getProviderState("test-provider");
922
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
923
+ expect(state.requestsToday).toBe(0);
924
+
925
+ // Add some requests
926
+ await (stateManager as any).recordSuccess("test-provider");
927
+ await (stateManager as any).recordSuccess("test-provider");
928
+
929
+ state = await (stateManager as any).getProviderState("test-provider");
930
+ expect(state.requestsToday).toBe(2);
931
+
932
+ // Reset provider
933
+ await (stateManager as any).resetProvider("test-provider");
934
+
935
+ // Verify state after reset
936
+ state = await (stateManager as any).getProviderState("test-provider");
937
+ expect(state.outOfCreditsUntil).toBeUndefined();
938
+ expect(state.requestsToday).toBe(0);
939
+ expect(state.lastErrors).toEqual([]);
940
+ });
941
+
942
+ test("resets out of credits state when all state is reset", async () => {
943
+ const futureDate = new Date(Date.now() + 3600000);
944
+ await (stateManager as any).markOutOfCredits("test-provider", futureDate);
945
+
946
+ // Add some requests
947
+ await (stateManager as any).recordSuccess("test-provider");
948
+ await (stateManager as any).recordSuccess("test-provider");
949
+
950
+ // Verify state before reset
951
+ const state = await (stateManager as any).getProviderState("test-provider");
952
+ expect(state.outOfCreditsUntil).toBe(futureDate.toISOString());
953
+ expect(state.requestsToday).toBe(2);
954
+
955
+ // Reset all state
956
+ await (stateManager as any).resetAll();
957
+
958
+ // Verify state after reset
959
+ const fullState = (stateManager as any).getState();
960
+ expect(fullState.providers["test-provider"]).toBeUndefined();
961
+ });
962
+
963
+ test("resets out of credits state during daily counter reset", async () => {
964
+ // Create a state file with old data
965
+ const yesterday = new Date();
966
+ yesterday.setDate(yesterday.getDate() - 1);
967
+ const yesterdayISO = new Date(
968
+ yesterday.getFullYear(),
969
+ yesterday.getMonth(),
970
+ yesterday.getDate(),
971
+ ).toISOString();
972
+
973
+ const futureDate = new Date(Date.now() + 3600000);
974
+ const testState = {
975
+ version: "1.0.0",
976
+ providers: {
977
+ "test-provider": {
978
+ lastUsedAt: yesterday.toISOString(),
979
+ requestsToday: 10,
980
+ lastReset: yesterdayISO,
981
+ outOfCreditsUntil: futureDate.toISOString(),
982
+ lastErrors: ["old error"],
983
+ },
984
+ },
985
+ };
986
+
987
+ await mkdir(tempStatePath.split("/").slice(0, -1).join("/"), { recursive: true });
988
+ await writeFile(tempStatePath, JSON.stringify(testState, null, 2), "utf-8");
989
+
990
+ // Create new state manager to load the file
991
+ const newStateManager = new StateManager({ statePath: tempStatePath });
992
+ await newStateManager.initialize();
993
+
994
+ const state = (newStateManager as any).getState();
995
+ const providerState = state.providers["test-provider"];
996
+
997
+ // Daily counters and out of credits should be reset because lastReset was yesterday
998
+ expect(providerState.requestsToday).toBe(0);
999
+ expect(providerState.outOfCreditsUntil).toBeUndefined();
1000
+ // But errors should be preserved until explicitly cleared
1001
+ expect(providerState.lastErrors).toEqual(["old error"]);
1002
+ });
1003
+ });
1004
+
1005
+ describe("Provider Selection When Credits Are Exhausted", () => {
1006
+ test("skips providers that are out of credits", async () => {
1007
+ const futureDate = new Date(Date.now() + 3600000);
1008
+ await (stateManager as any).markOutOfCredits("out-of-credits-provider", futureDate);
1009
+
1010
+ // Add another provider that is not out of credits
1011
+ await (stateManager as any).getProviderState("available-provider");
1012
+
1013
+ // Check states
1014
+ const outOfCreditsState = await (stateManager as any).getProviderState(
1015
+ "out-of-credits-provider",
1016
+ );
1017
+ const availableState = await (stateManager as any).getProviderState("available-provider");
1018
+
1019
+ // Verify out of credits provider is marked as such
1020
+ const isOutOfCredits =
1021
+ outOfCreditsState.outOfCreditsUntil &&
1022
+ new Date(outOfCreditsState.outOfCreditsUntil) > new Date();
1023
+ expect(isOutOfCredits).toBe(true);
1024
+
1025
+ // Verify available provider is not marked as out of credits
1026
+ const isAvailableOutOfCredits =
1027
+ availableState.outOfCreditsUntil && new Date(availableState.outOfCreditsUntil) > new Date();
1028
+ expect(!!isAvailableOutOfCredits).toBe(false);
1029
+ });
1030
+
1031
+ test("allows selection of providers that have expired out of credits", async () => {
1032
+ const pastDate = new Date(Date.now() - 3600000); // 1 hour ago
1033
+ await (stateManager as any).markOutOfCredits("expired-provider", pastDate);
1034
+
1035
+ const state = await (stateManager as any).getProviderState("expired-provider");
1036
+
1037
+ // Verify provider is not considered out of credits anymore
1038
+ const isOutOfCredits =
1039
+ state.outOfCreditsUntil && new Date(state.outOfCreditsUntil) > new Date();
1040
+ expect(isOutOfCredits).toBe(false);
1041
+ });
1042
+
1043
+ test("properly handles multiple providers with mixed credit states", async () => {
1044
+ const futureDate = new Date(Date.now() + 3600000);
1045
+ const pastDate = new Date(Date.now() - 3600000);
1046
+
1047
+ // Mark one provider as out of credits
1048
+ await (stateManager as any).markOutOfCredits("out-of-credits-provider", futureDate);
1049
+
1050
+ // Mark another provider as having expired out of credits
1051
+ await (stateManager as any).markOutOfCredits("expired-provider", pastDate);
1052
+
1053
+ // Leave third provider with no out of credits state
1054
+
1055
+ // Check states
1056
+ const outOfCreditsState = await (stateManager as any).getProviderState(
1057
+ "out-of-credits-provider",
1058
+ );
1059
+ const expiredState = await (stateManager as any).getProviderState("expired-provider");
1060
+ const normalState = await (stateManager as any).getProviderState("normal-provider");
1061
+
1062
+ // Verify out of credits provider is marked as such
1063
+ const isOutOfCredits =
1064
+ outOfCreditsState.outOfCreditsUntil &&
1065
+ new Date(outOfCreditsState.outOfCreditsUntil) > new Date();
1066
+ expect(isOutOfCredits).toBe(true);
1067
+
1068
+ // Verify expired provider is not marked as out of credits anymore
1069
+ const isExpiredOutOfCredits =
1070
+ expiredState.outOfCreditsUntil && new Date(expiredState.outOfCreditsUntil) > new Date();
1071
+ expect(isExpiredOutOfCredits).toBe(false);
1072
+
1073
+ // Verify normal provider is not marked as out of credits
1074
+ const isNormalOutOfCredits =
1075
+ normalState.outOfCreditsUntil && new Date(normalState.outOfCreditsUntil) > new Date();
1076
+ expect(!!isNormalOutOfCredits).toBe(false);
1077
+ });
1078
+ });
1079
+ });