wraptc 1.0.3 → 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.
- package/bin/wraptc +4 -4
- package/package.json +2 -2
- package/src/cli/__tests__/cli.test.ts +337 -0
- package/src/cli/index.ts +149 -0
- package/src/core/__tests__/fixtures/configs/project-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/system-config.json +14 -0
- package/src/core/__tests__/fixtures/configs/user-config.json +15 -0
- package/src/core/__tests__/integration/integration.test.ts +241 -0
- package/src/core/__tests__/integration/mock-coder-adapter.test.ts +243 -0
- package/src/core/__tests__/test-utils.ts +136 -0
- package/src/core/__tests__/unit/adapters/runner.test.ts +302 -0
- package/src/core/__tests__/unit/basic-test.test.ts +44 -0
- package/src/core/__tests__/unit/basic.test.ts +12 -0
- package/src/core/__tests__/unit/config.test.ts +244 -0
- package/src/core/__tests__/unit/error-patterns.test.ts +181 -0
- package/src/core/__tests__/unit/memory-monitor.test.ts +354 -0
- package/src/core/__tests__/unit/plugin/registry.test.ts +356 -0
- package/src/core/__tests__/unit/providers/codex.test.ts +173 -0
- package/src/core/__tests__/unit/providers/configurable.test.ts +429 -0
- package/src/core/__tests__/unit/providers/gemini.test.ts +251 -0
- package/src/core/__tests__/unit/providers/opencode.test.ts +258 -0
- package/src/core/__tests__/unit/providers/qwen-code.test.ts +195 -0
- package/src/core/__tests__/unit/providers/simple-codex.test.ts +18 -0
- package/src/core/__tests__/unit/router.test.ts +967 -0
- package/src/core/__tests__/unit/state.test.ts +1079 -0
- package/src/core/__tests__/unit/unified/capabilities.test.ts +186 -0
- package/src/core/__tests__/unit/wrap-terminalcoder.test.ts +32 -0
- package/src/core/adapters/builtin/codex.ts +35 -0
- package/src/core/adapters/builtin/gemini.ts +34 -0
- package/src/core/adapters/builtin/index.ts +31 -0
- package/src/core/adapters/builtin/mock-coder.ts +148 -0
- package/src/core/adapters/builtin/qwen.ts +34 -0
- package/src/core/adapters/define.ts +48 -0
- package/src/core/adapters/index.ts +43 -0
- package/src/core/adapters/loader.ts +143 -0
- package/src/core/adapters/provider-bridge.ts +190 -0
- package/src/core/adapters/runner.ts +437 -0
- package/src/core/adapters/types.ts +172 -0
- package/src/core/config.ts +290 -0
- package/src/core/define-provider.ts +212 -0
- package/src/core/error-patterns.ts +147 -0
- package/src/core/index.ts +130 -0
- package/src/core/memory-monitor.ts +171 -0
- package/src/core/plugin/builtin.ts +87 -0
- package/src/core/plugin/index.ts +34 -0
- package/src/core/plugin/registry.ts +350 -0
- package/src/core/plugin/types.ts +209 -0
- package/src/core/provider-factory.ts +397 -0
- package/src/core/provider-loader.ts +171 -0
- package/src/core/providers/codex.ts +56 -0
- package/src/core/providers/configurable.ts +637 -0
- package/src/core/providers/custom.ts +261 -0
- package/src/core/providers/gemini.ts +41 -0
- package/src/core/providers/index.ts +383 -0
- package/src/core/providers/opencode.ts +168 -0
- package/src/core/providers/qwen-code.ts +41 -0
- package/src/core/router.ts +370 -0
- package/src/core/state.ts +258 -0
- package/src/core/types.ts +206 -0
- package/src/core/unified/capabilities.ts +184 -0
- package/src/core/unified/errors.ts +141 -0
- package/src/core/unified/index.ts +29 -0
- package/src/core/unified/output.ts +189 -0
- package/src/core/wrap-terminalcoder.ts +245 -0
- package/src/mcp/__tests__/server.test.ts +295 -0
- package/src/mcp/server.ts +284 -0
- package/src/test-fixtures/mock-coder.sh +194 -0
- package/dist/cli/index.js +0 -16501
- package/dist/core/index.js +0 -7531
- package/dist/mcp/server.js +0 -14568
- package/dist/wraptc-1.0.3.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
|
+
});
|