1import { describe, expect, it } from "vite-plus/test";
2
3import { claudeRateLimitEventToUpdate, claudeUsageResponseToLimits } from "./claudeUsageLimits.ts";
4
5const checkedAt = "2026-07-18T10:00:00.000Z";
6const noNames = { overageIncluded: undefined } as const;
7
8describe("claudeUsageResponseToLimits", () => {
9 it("maps the session, weekly, and model-scoped weekly windows", () => {
10 expect(
11 claudeUsageResponseToLimits({
12 checkedAt,
13 response: {
14 rate_limits_available: true,
15 rate_limits: {
16 five_hour: { utilization: 54, resets_at: "2026-07-18T14:39:00Z" },
17 seven_day: { utilization: 18.4, resets_at: "2026-07-24T08:59:00+00:00" },
18 seven_day_opus: { utilization: 3, resets_at: null },
19 // Newer CLIs add this on top of the typed keys; the pinned SDK
20 // typings do not know it yet.
21 ...({
22 model_scoped: [
23 { display_name: "Fable", utilization: 73, resets_at: "2026-07-24T08:59:00Z" },
24 { display_name: "Ghost", utilization: null, resets_at: null },
25 ],
26 } as object),
27 extra_usage: {
28 is_enabled: false,
29 monthly_limit: null,
30 used_credits: null,
31 utilization: null,
32 },
33 },
34 },
35 }),
36 ).toEqual({
37 names: { overageIncluded: "Fable" },
38 limits: {
39 checkedAt,
40 windows: [
41 {
42 id: "five_hour",
43 kind: "session",
44 label: "Session",
45 usedPercent: 54,
46 windowDurationMins: 300,
47 resetsAt: "2026-07-18T14:39:00.000Z",
48 },
49 {
50 id: "seven_day",
51 kind: "weekly",
52 label: "Weekly",
53 usedPercent: 18.4,
54 windowDurationMins: 10080,
55 resetsAt: "2026-07-24T08:59:00.000Z",
56 },
57 {
58 id: "seven_day_fable",
59 kind: "weekly",
60 label: "Weekly · Fable",
61 usedPercent: 73,
62 windowDurationMins: 10080,
63 resetsAt: "2026-07-24T08:59:00.000Z",
64 },
65 ],
66 },
67 });
68 });
69
70 it("names the overage-included bucket only from a scoped entry that drew a row", () => {
71 expect(
72 claudeUsageResponseToLimits({
73 checkedAt,
74 response: {
75 rate_limits_available: true,
76 rate_limits: {
77 ...({
78 model_scoped: [
79 { display_name: "Ghost", utilization: null, resets_at: null },
80 { display_name: "Fable", utilization: 5, resets_at: null },
81 ],
82 } as object),
83 },
84 },
85 }).names,
86 ).toEqual({ overageIncluded: "Fable" });
87 });
88
89 it("reports API key and Bedrock accounts as unsupported", () => {
90 expect(
91 claudeUsageResponseToLimits({
92 checkedAt,
93 response: { rate_limits_available: false, rate_limits: null },
94 }).limits,
95 ).toEqual({ checkedAt, windows: [], unavailable: { reason: "unsupported" } });
96 });
97
98 it("skips a window the endpoint reports without a utilization", () => {
99 expect(
100 claudeUsageResponseToLimits({
101 checkedAt,
102 response: {
103 rate_limits_available: true,
104 rate_limits: {
105 five_hour: { utilization: null, resets_at: null },
106 seven_day: { utilization: 250, resets_at: null },
107 },
108 },
109 }).limits.windows,
110 ).toEqual([
111 {
112 id: "seven_day",
113 kind: "weekly",
114 label: "Weekly",
115 usedPercent: 100,
116 windowDurationMins: 10080,
117 },
118 ]);
119 });
120});
121
122describe("claudeRateLimitEventToUpdate", () => {
123 it("scales the 0–1 utilization and epoch-second reset onto the probe's window id", () => {
124 expect(
125 claudeRateLimitEventToUpdate(
126 {
127 status: "allowed_warning",
128 rateLimitType: "seven_day",
129 utilization: 0.85,
130 resetsAt: 1_784_000_000,
131 },
132 noNames,
133 ),
134 ).toEqual({
135 windows: [
136 {
137 id: "seven_day",
138 kind: "weekly",
139 label: "Weekly",
140 usedPercent: 85,
141 windowDurationMins: 10080,
142 resetsAt: "2026-07-14T03:33:20.000Z",
143 },
144 ],
145 });
146 });
147
148 it("lands the streamed overage-included bucket on the row the probe named", () => {
149 const event = {
150 status: "allowed",
151 rateLimitType: "seven_day_overage_included" as never,
152 utilization: 0.4,
153 } as const;
154 // No probe has named the bucket yet: guessing would open a stray row.
155 expect(claudeRateLimitEventToUpdate(event, noNames)).toBeUndefined();
156 expect(claudeRateLimitEventToUpdate(event, { overageIncluded: "Fable" })).toEqual({
157 windows: [
158 {
159 id: "seven_day_fable",
160 kind: "weekly",
161 label: "Weekly · Fable",
162 usedPercent: 40,
163 windowDurationMins: 10080,
164 },
165 ],
166 });
167 });
168
169 it("ignores windows the page does not render and events without a utilization", () => {
170 expect(
171 claudeRateLimitEventToUpdate(
172 { status: "allowed", rateLimitType: "seven_day_opus", utilization: 0.1 },
173 noNames,
174 ),
175 ).toBeUndefined();
176 expect(
177 claudeRateLimitEventToUpdate({ status: "rejected", rateLimitType: "five_hour" }, noNames),
178 ).toBeUndefined();
179 });
180});