@aliou/pi-synthetic 0.17.3 → 0.18.0

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.
@@ -1,211 +0,0 @@
1
- import {
2
- afterEach,
3
- assert,
4
- beforeEach,
5
- describe,
6
- expect,
7
- it,
8
- vi,
9
- } from "vitest";
10
- import type { QuotasResponse } from "../types/quotas";
11
- import { QuotaStore } from "./quota-store";
12
-
13
- beforeEach(() => {
14
- vi.useFakeTimers();
15
- });
16
-
17
- afterEach(() => {
18
- vi.useRealTimers();
19
- });
20
-
21
- describe("QuotaStore", () => {
22
- const sampleQuotas: QuotasResponse = {
23
- subscription: { limit: 100, requests: 5, renewsAt: "2026-01-01T00:00:00Z" },
24
- };
25
-
26
- describe("ingest", () => {
27
- it("stores and emits API-sourced data", () => {
28
- const store = new QuotaStore();
29
- const received: QuotasResponse[] = [];
30
- store.subscribe((snap) => received.push(snap.quotas));
31
-
32
- const result = store.ingest(sampleQuotas, "api");
33
-
34
- expect(result).toBe(true);
35
- expect(store.getSnapshot()?.quotas).toBe(sampleQuotas);
36
- expect(store.getSnapshot()?.source).toBe("api");
37
- expect(received).toHaveLength(1);
38
- });
39
-
40
- it("stores and emits header-sourced data", () => {
41
- const store = new QuotaStore();
42
- const result = store.ingest(sampleQuotas, "header");
43
-
44
- expect(result).toBe(true);
45
- expect(store.getSnapshot()?.source).toBe("header");
46
- });
47
-
48
- it("throttles header ingestion within throttle window", () => {
49
- const store = new QuotaStore();
50
- store.ingest(sampleQuotas, "header");
51
-
52
- // Within throttle window — should be dropped
53
- const result = store.ingest(sampleQuotas, "header");
54
- expect(result).toBe(false);
55
-
56
- // Advance past throttle
57
- vi.advanceTimersByTime(store.headerThrottleMs + 1);
58
- const result2 = store.ingest(sampleQuotas, "header");
59
- expect(result2).toBe(true);
60
- });
61
-
62
- it("does NOT throttle API ingestion", () => {
63
- const store = new QuotaStore();
64
- store.ingest(sampleQuotas, "api");
65
-
66
- // API is never throttled
67
- const result = store.ingest(sampleQuotas, "api");
68
- expect(result).toBe(true);
69
- });
70
-
71
- it("header after API emit always goes through", () => {
72
- const store = new QuotaStore();
73
- store.ingest(sampleQuotas, "api");
74
-
75
- // Header 1ms after API should not be blocked
76
- vi.advanceTimersByTime(1);
77
- const result = store.ingest(sampleQuotas, "header");
78
- expect(result).toBe(true);
79
- });
80
-
81
- it("updates timestamp on each successful ingest", () => {
82
- const store = new QuotaStore();
83
- store.ingest(sampleQuotas, "api");
84
- const snap1 = store.getSnapshot();
85
- assert(snap1);
86
- const t1 = snap1.updatedAt;
87
-
88
- vi.advanceTimersByTime(10_000);
89
- store.ingest(sampleQuotas, "api");
90
- const snap2 = store.getSnapshot();
91
- assert(snap2);
92
- const t2 = snap2.updatedAt;
93
-
94
- expect(t2).toBeGreaterThan(t1);
95
- });
96
- });
97
-
98
- describe("subscribe", () => {
99
- it("notifies subscribers on ingest", () => {
100
- const store = new QuotaStore();
101
- const calls: QuotasResponse[] = [];
102
- store.subscribe((snap) => calls.push(snap.quotas));
103
-
104
- store.ingest(sampleQuotas, "api");
105
- expect(calls).toHaveLength(1);
106
- expect(calls[0]).toBe(sampleQuotas);
107
- });
108
-
109
- it("does not notify on throttled ingest", () => {
110
- const store = new QuotaStore();
111
- const calls: QuotasResponse[] = [];
112
- store.subscribe((snap) => calls.push(snap.quotas));
113
-
114
- store.ingest(sampleQuotas, "header");
115
- store.ingest(sampleQuotas, "header"); // throttled
116
-
117
- expect(calls).toHaveLength(1);
118
- });
119
-
120
- it("unsubscribes when unsubscribe function is called", () => {
121
- const store = new QuotaStore();
122
- const calls: QuotasResponse[] = [];
123
- const unsub = store.subscribe((snap) => calls.push(snap.quotas));
124
-
125
- unsub();
126
- store.ingest(sampleQuotas, "api");
127
-
128
- expect(calls).toHaveLength(0);
129
- });
130
-
131
- it("supports multiple subscribers", () => {
132
- const store = new QuotaStore();
133
- const calls1: QuotasResponse[] = [];
134
- const calls2: QuotasResponse[] = [];
135
- store.subscribe((snap) => calls1.push(snap.quotas));
136
- store.subscribe((snap) => calls2.push(snap.quotas));
137
-
138
- store.ingest(sampleQuotas, "api");
139
-
140
- expect(calls1).toHaveLength(1);
141
- expect(calls2).toHaveLength(1);
142
- });
143
- });
144
-
145
- describe("refreshFromApi", () => {
146
- it("calls the fetcher and ingests the result", async () => {
147
- const store = new QuotaStore();
148
- const fetcher = vi.fn().mockResolvedValue(sampleQuotas);
149
-
150
- const result = await store.refreshFromApi(fetcher);
151
-
152
- assert(result);
153
- expect(result.quotas).toBe(sampleQuotas);
154
- expect(result.source).toBe("api");
155
- expect(fetcher).toHaveBeenCalledOnce();
156
- });
157
-
158
- it("deduplicates concurrent calls", async () => {
159
- const store = new QuotaStore();
160
- let resolveFirst!: (v: QuotasResponse) => void;
161
- const first = new Promise<QuotasResponse>((r) => (resolveFirst = r));
162
- const fetcher = vi.fn().mockImplementation(() => first);
163
-
164
- // Start two concurrent refreshes
165
- const p1 = store.refreshFromApi(fetcher);
166
- const p2 = store.refreshFromApi(fetcher);
167
-
168
- // Only one fetcher call
169
- expect(fetcher).toHaveBeenCalledOnce();
170
- expect(store.isRefreshing).toBe(true);
171
-
172
- // Resolve the fetch
173
- resolveFirst(sampleQuotas);
174
- await p1;
175
- await p2;
176
-
177
- expect(store.isRefreshing).toBe(false);
178
- });
179
-
180
- it("handles fetcher returning undefined", async () => {
181
- const store = new QuotaStore();
182
- const fetcher = vi.fn().mockResolvedValue(undefined);
183
-
184
- const result = await store.refreshFromApi(fetcher);
185
-
186
- expect(result).toBeUndefined();
187
- expect(store.getSnapshot()).toBeUndefined();
188
- });
189
- });
190
-
191
- describe("clear", () => {
192
- it("resets all state", () => {
193
- const store = new QuotaStore();
194
- store.ingest(sampleQuotas, "api");
195
-
196
- store.clear();
197
-
198
- expect(store.getSnapshot()).toBeUndefined();
199
- });
200
-
201
- it("resets header throttle after clear", () => {
202
- const store = new QuotaStore();
203
- store.ingest(sampleQuotas, "header");
204
- expect(store.ingest(sampleQuotas, "header")).toBe(false);
205
-
206
- store.clear();
207
-
208
- expect(store.ingest(sampleQuotas, "header")).toBe(true);
209
- });
210
- });
211
- });
@@ -1,393 +0,0 @@
1
- import {
2
- afterEach,
3
- assert,
4
- beforeEach,
5
- describe,
6
- expect,
7
- it,
8
- vi,
9
- } from "vitest";
10
- import type { QuotasResponse } from "../types/quotas";
11
- import { assessWindow, type QuotaWindow } from "../utils/quotas-severity";
12
- import { type NotifyFn, QuotaWarningNotifier } from "./quota-warnings";
13
-
14
- beforeEach(() => {
15
- vi.useFakeTimers();
16
- });
17
-
18
- afterEach(() => {
19
- vi.useRealTimers();
20
- });
21
-
22
- describe("QuotaWarningNotifier", () => {
23
- const baseQuotas: QuotasResponse = {
24
- weeklyTokenLimit: {
25
- nextRegenAt: new Date(Date.now() + 6 * 24 * 3600 * 1000).toISOString(),
26
- percentRemaining: 90,
27
- maxCredits: "$10.00",
28
- remainingCredits: "$9.00",
29
- nextRegenCredits: "$0.50",
30
- },
31
- rollingFiveHourLimit: {
32
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
33
- tickPercent: 10,
34
- remaining: 90,
35
- max: 100,
36
- limited: false,
37
- },
38
- search: {
39
- hourly: {
40
- limit: 100,
41
- requests: 10,
42
- renewsAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
43
- },
44
- },
45
- freeToolCalls: {
46
- limit: 100,
47
- requests: 5,
48
- renewsAt: new Date(Date.now() + 12 * 3600 * 1000).toISOString(),
49
- },
50
- };
51
-
52
- describe("shouldNotify", () => {
53
- it("notifies on first time seeing a window at risk", () => {
54
- const notifier = new QuotaWarningNotifier();
55
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(true);
56
- expect(notifier.shouldNotify("Requests / 5h", "high")).toBe(true);
57
- expect(notifier.shouldNotify("Search / hour", "critical")).toBe(true);
58
- });
59
-
60
- it("notifies on severity escalation", () => {
61
- const notifier = new QuotaWarningNotifier();
62
- notifier.markNotified("Credits / week", "warning");
63
- expect(notifier.shouldNotify("Credits / week", "high")).toBe(true);
64
-
65
- notifier.markNotified("Requests / 5h", "high");
66
- expect(notifier.shouldNotify("Requests / 5h", "critical")).toBe(true);
67
- });
68
-
69
- it("notifies on skip from none to any risk level", () => {
70
- const notifier = new QuotaWarningNotifier();
71
- notifier.markNotified("Test", "none");
72
- expect(notifier.shouldNotify("Test", "warning")).toBe(true);
73
- });
74
-
75
- it("does not notify on same severity for warning within cooldown", () => {
76
- const notifier = new QuotaWarningNotifier();
77
- notifier.markNotified("Credits / week", "warning");
78
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
79
- });
80
-
81
- it("does notify on warning after cooldown elapsed", () => {
82
- const notifier = new QuotaWarningNotifier();
83
- notifier.markNotified("Credits / week", "warning");
84
-
85
- vi.advanceTimersByTime(60 * 60 * 1000 + 1);
86
-
87
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(true);
88
- });
89
-
90
- it("does not notify on downgrade to warning", () => {
91
- const notifier = new QuotaWarningNotifier();
92
- notifier.markNotified("Credits / week", "high");
93
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
94
- });
95
-
96
- it("does notify on downgrade to high (no cooldown)", () => {
97
- const notifier = new QuotaWarningNotifier();
98
- notifier.markNotified("Requests / 5h", "critical");
99
- expect(notifier.shouldNotify("Requests / 5h", "high")).toBe(true);
100
- });
101
-
102
- it("always notifies for high severity (no cooldown)", () => {
103
- const notifier = new QuotaWarningNotifier();
104
- notifier.markNotified("Credits / week", "high");
105
- expect(notifier.shouldNotify("Credits / week", "high")).toBe(true);
106
- });
107
-
108
- it("always notifies for critical severity (no cooldown)", () => {
109
- const notifier = new QuotaWarningNotifier();
110
- notifier.markNotified("Credits / week", "critical");
111
- expect(notifier.shouldNotify("Credits / week", "critical")).toBe(true);
112
- });
113
- });
114
-
115
- describe("markNotified", () => {
116
- it("tracks severity per window key", () => {
117
- const notifier = new QuotaWarningNotifier();
118
- notifier.markNotified("Credits / week", "warning");
119
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
120
-
121
- // Different key is independent
122
- expect(notifier.shouldNotify("Requests / 5h", "warning")).toBe(true);
123
- });
124
-
125
- it("allows re-notification after escalation then downgrade then re-escalation", () => {
126
- const notifier = new QuotaWarningNotifier();
127
- notifier.markNotified("Test", "high");
128
- expect(notifier.shouldNotify("Test", "warning")).toBe(false);
129
- expect(notifier.shouldNotify("Test", "high")).toBe(true);
130
- });
131
- });
132
-
133
- describe("clearAlertState", () => {
134
- it("resets all alert state so windows notify again", () => {
135
- const notifier = new QuotaWarningNotifier();
136
- notifier.markNotified("Credits / week", "warning");
137
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
138
-
139
- notifier.clearAlertState();
140
-
141
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(true);
142
- });
143
- });
144
-
145
- describe("findHighRiskWindows", () => {
146
- it("returns empty for low-usage quotas", () => {
147
- const notifier = new QuotaWarningNotifier();
148
- const risks = notifier.findHighRiskWindows(baseQuotas);
149
- expect(risks).toHaveLength(0);
150
- });
151
-
152
- it("finds windows with high usage", () => {
153
- const notifier = new QuotaWarningNotifier();
154
- const quotas: QuotasResponse = {
155
- ...baseQuotas,
156
- rollingFiveHourLimit: {
157
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
158
- tickPercent: 10,
159
- remaining: 5,
160
- max: 100,
161
- limited: false,
162
- },
163
- };
164
- const risks = notifier.findHighRiskWindows(quotas);
165
- const fiveHourRisk = risks.find(
166
- (r) => r.window.label === "Requests / 5h",
167
- );
168
- assert(fiveHourRisk, "fiveHourRisk should exist");
169
- expect(fiveHourRisk.assessment.severity).toBe("high");
170
- });
171
-
172
- it("finds limited windows even with low usage", () => {
173
- const notifier = new QuotaWarningNotifier();
174
- const quotas: QuotasResponse = {
175
- ...baseQuotas,
176
- rollingFiveHourLimit: {
177
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
178
- tickPercent: 10,
179
- remaining: 95,
180
- max: 100,
181
- limited: true,
182
- },
183
- };
184
- const risks = notifier.findHighRiskWindows(quotas);
185
- const fiveHourRisk = risks.find(
186
- (r) => r.window.label === "Requests / 5h",
187
- );
188
- assert(fiveHourRisk, "fiveHourRisk should exist");
189
- expect(fiveHourRisk.assessment.severity).toBe("critical");
190
- });
191
-
192
- it("returns empty for quotas with no windows", () => {
193
- const notifier = new QuotaWarningNotifier();
194
- expect(notifier.findHighRiskWindows({})).toHaveLength(0);
195
- });
196
- });
197
-
198
- describe("formatWarningMessage", () => {
199
- it("formats single window warning", () => {
200
- const notifier = new QuotaWarningNotifier();
201
- const w: QuotaWindow = {
202
- label: "Requests / 5h",
203
- usedPercent: 92,
204
- resetsAt: new Date(Date.now() + 2 * 3600 * 1000),
205
- windowSeconds: 5 * 3600,
206
- usedValue: 92,
207
- limitValue: 100,
208
- showPace: false,
209
- };
210
- const assessment = assessWindow(w);
211
- const msg = notifier.formatWarningMessage([{ window: w, assessment }]);
212
- expect(msg).toContain("Synthetic quota warning:");
213
- expect(msg).toContain("Requests / 5h");
214
- expect(msg).toContain("92% used");
215
- expect(msg).toContain("projected");
216
- });
217
-
218
- it("formats multiple windows", () => {
219
- const notifier = new QuotaWarningNotifier();
220
- const w1: QuotaWindow = {
221
- label: "Credits / week",
222
- usedPercent: 85,
223
- resetsAt: new Date(Date.now() + 6 * 24 * 3600 * 1000),
224
- windowSeconds: 7 * 24 * 3600,
225
- usedValue: 85,
226
- limitValue: 100,
227
- showPace: true,
228
- paceScale: 1 / 7,
229
- };
230
- const w2: QuotaWindow = {
231
- label: "Requests / 5h",
232
- usedPercent: 92,
233
- resetsAt: new Date(Date.now() + 2 * 3600 * 1000),
234
- windowSeconds: 5 * 3600,
235
- usedValue: 92,
236
- limitValue: 100,
237
- showPace: false,
238
- };
239
- const msg = notifier.formatWarningMessage([
240
- { window: w1, assessment: assessWindow(w1) },
241
- { window: w2, assessment: assessWindow(w2) },
242
- ]);
243
- expect(msg).toContain("Credits / week");
244
- expect(msg).toContain("Requests / 5h");
245
- const lines = msg.split("\n");
246
- expect(lines).toHaveLength(3); // header + 2 windows
247
- });
248
-
249
- it("includes severity label for non-none severities", () => {
250
- const notifier = new QuotaWarningNotifier();
251
- const w: QuotaWindow = {
252
- label: "Requests / 5h",
253
- usedPercent: 92,
254
- resetsAt: new Date(Date.now() + 2 * 3600 * 1000),
255
- windowSeconds: 5 * 3600,
256
- usedValue: 92,
257
- limitValue: 100,
258
- showPace: false,
259
- };
260
- const msg = notifier.formatWarningMessage([
261
- { window: w, assessment: assessWindow(w) },
262
- ]);
263
- expect(msg).toMatch(/\(high\)/);
264
- });
265
- });
266
-
267
- describe("evaluate", () => {
268
- it("does not notify for low-usage quotas", () => {
269
- const notifier = new QuotaWarningNotifier();
270
- const calls: Array<[string, string]> = [];
271
- const notify: NotifyFn = (msg, lvl) => calls.push([msg, lvl]);
272
-
273
- notifier.evaluate(baseQuotas, false, notify);
274
- expect(calls).toHaveLength(0);
275
- });
276
-
277
- it("notifies for high-usage quotas with skipAlreadyWarned=false", () => {
278
- const notifier = new QuotaWarningNotifier();
279
- const calls: Array<[string, string]> = [];
280
- const notify: NotifyFn = (msg, lvl) => calls.push([msg, lvl]);
281
-
282
- const highUsageQuotas: QuotasResponse = {
283
- ...baseQuotas,
284
- rollingFiveHourLimit: {
285
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
286
- tickPercent: 10,
287
- remaining: 5,
288
- max: 100,
289
- limited: false,
290
- },
291
- };
292
-
293
- notifier.evaluate(highUsageQuotas, false, notify);
294
- expect(calls).toHaveLength(1);
295
- expect(calls[0][0]).toContain("Synthetic quota warning");
296
- });
297
-
298
- it("does not re-notify on same severity with skipAlreadyWarned=true", () => {
299
- const notifier = new QuotaWarningNotifier();
300
- const calls: Array<[string, string]> = [];
301
- const notify: NotifyFn = (msg, lvl) => calls.push([msg, lvl]);
302
-
303
- // 85% used (no pace) → warning severity, which has cooldown
304
- const warningQuotas: QuotasResponse = {
305
- ...baseQuotas,
306
- rollingFiveHourLimit: {
307
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
308
- tickPercent: 10,
309
- remaining: 15,
310
- max: 100,
311
- limited: false,
312
- },
313
- };
314
-
315
- notifier.evaluate(warningQuotas, true, notify);
316
- expect(calls).toHaveLength(1);
317
-
318
- // Same severity, same data — should not re-notify (warning has cooldown)
319
- notifier.evaluate(warningQuotas, true, notify);
320
- expect(calls).toHaveLength(1);
321
- });
322
-
323
- it("notifies on severity escalation even with skipAlreadyWarned=true", () => {
324
- const notifier = new QuotaWarningNotifier();
325
- const calls: Array<[string, string]> = [];
326
- const notify: NotifyFn = (msg, lvl) => calls.push([msg, lvl]);
327
-
328
- // 92% used (no pace) → high severity
329
- const highQuotas: QuotasResponse = {
330
- ...baseQuotas,
331
- rollingFiveHourLimit: {
332
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
333
- tickPercent: 10,
334
- remaining: 8,
335
- max: 100,
336
- limited: false,
337
- },
338
- };
339
-
340
- notifier.evaluate(highQuotas, true, notify);
341
- expect(calls).toHaveLength(1);
342
-
343
- // Escalate to critical (limited)
344
- const criticalQuotas: QuotasResponse = {
345
- ...baseQuotas,
346
- rollingFiveHourLimit: {
347
- nextTickAt: new Date(Date.now() + 2.5 * 3600 * 1000).toISOString(),
348
- tickPercent: 10,
349
- remaining: 2,
350
- max: 100,
351
- limited: true,
352
- },
353
- };
354
-
355
- notifier.evaluate(criticalQuotas, true, notify);
356
- expect(calls).toHaveLength(2);
357
- expect(calls[1][1]).toBe("error");
358
- });
359
- });
360
-
361
- describe("notification flow (shouldNotify + markNotified integration)", () => {
362
- it("notifies once on first warning, blocks repeat, notifies on escalation", () => {
363
- const notifier = new QuotaWarningNotifier();
364
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(true);
365
- notifier.markNotified("Credits / week", "warning");
366
-
367
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
368
-
369
- expect(notifier.shouldNotify("Credits / week", "high")).toBe(true);
370
- notifier.markNotified("Credits / week", "high");
371
-
372
- expect(notifier.shouldNotify("Credits / week", "high")).toBe(true);
373
- });
374
-
375
- it("allows re-notification after clear", () => {
376
- const notifier = new QuotaWarningNotifier();
377
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(true);
378
- notifier.markNotified("Credits / week", "warning");
379
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
380
-
381
- notifier.clearAlertState();
382
-
383
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(true);
384
- });
385
-
386
- it("tracks windows independently", () => {
387
- const notifier = new QuotaWarningNotifier();
388
- notifier.markNotified("Credits / week", "warning");
389
- expect(notifier.shouldNotify("Credits / week", "warning")).toBe(false);
390
- expect(notifier.shouldNotify("Search / hour", "warning")).toBe(true);
391
- });
392
- });
393
- });