@latentminds/pi-quotas 0.1.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.
@@ -0,0 +1,256 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseAnthropicUsage } from "./providers.js";
3
+ import { parseCodexUsage } from "./providers.js";
4
+ import { parseGitHubCopilotUsage } from "./providers.js";
5
+
6
+ describe("parseAnthropicUsage", () => {
7
+ it("maps oauth usage response into quota windows", () => {
8
+ const windows = parseAnthropicUsage({
9
+ five_hour: {
10
+ utilization: 23.4,
11
+ resets_at: "2026-04-22T18:30:00Z",
12
+ },
13
+ seven_day: {
14
+ utilization: 14.1,
15
+ resets_at: "2026-04-25T08:30:00Z",
16
+ },
17
+ });
18
+
19
+ expect(windows).toHaveLength(2);
20
+ expect(windows[0]).toMatchObject({
21
+ provider: "anthropic",
22
+ label: "5h",
23
+ usedPercent: 23.4,
24
+ windowSeconds: 5 * 60 * 60,
25
+ });
26
+ expect(windows[1]).toMatchObject({
27
+ provider: "anthropic",
28
+ label: "7d",
29
+ usedPercent: 14.1,
30
+ windowSeconds: 7 * 24 * 60 * 60,
31
+ });
32
+ });
33
+
34
+ it("includes extra_usage as a currency window", () => {
35
+ const windows = parseAnthropicUsage({
36
+ five_hour: { utilization: 9, resets_at: "2026-04-22T09:00:00Z" },
37
+ seven_day: { utilization: 31, resets_at: "2026-04-23T23:00:00Z" },
38
+ extra_usage: {
39
+ is_enabled: true,
40
+ monthly_limit: 30000,
41
+ used_credits: 21548,
42
+ utilization: 71.83,
43
+ currency: "AUD",
44
+ },
45
+ });
46
+
47
+ const extra = windows.find((w) => w.label === "Extra (AUD)");
48
+ expect(extra).toBeDefined();
49
+ expect(extra).toMatchObject({
50
+ isCurrency: true,
51
+ usedPercent: 71.83,
52
+ usedValue: 215.48,
53
+ limitValue: 300,
54
+ });
55
+ });
56
+
57
+ it("includes per-model 7d windows when present", () => {
58
+ const windows = parseAnthropicUsage({
59
+ five_hour: { utilization: 9, resets_at: "2026-04-22T09:00:00Z" },
60
+ seven_day: { utilization: 31, resets_at: "2026-04-23T23:00:00Z" },
61
+ seven_day_sonnet: { utilization: 8, resets_at: "2026-04-23T23:00:00Z" },
62
+ seven_day_omelette: { utilization: 23, resets_at: "2026-04-26T23:00:00Z" },
63
+ seven_day_opus: null,
64
+ });
65
+
66
+ const sonnet = windows.find((w) => w.label === "7d Sonnet");
67
+ const opus = windows.find((w) => w.label === "7d Opus");
68
+ expect(sonnet).toMatchObject({ usedPercent: 8 });
69
+ expect(opus).toMatchObject({ usedPercent: 23 });
70
+ });
71
+
72
+ it("skips extra_usage when disabled", () => {
73
+ const windows = parseAnthropicUsage({
74
+ five_hour: { utilization: 5, resets_at: "2026-04-22T09:00:00Z" },
75
+ seven_day: { utilization: 10, resets_at: "2026-04-23T23:00:00Z" },
76
+ extra_usage: { is_enabled: false },
77
+ });
78
+ expect(windows.find((w) => w.label.startsWith("Extra"))).toBeUndefined();
79
+ });
80
+ });
81
+
82
+ describe("parseCodexUsage", () => {
83
+ it("maps primary and secondary windows from wham usage", () => {
84
+ const windows = parseCodexUsage({
85
+ plan_type: "plus",
86
+ rate_limit: {
87
+ primary_window: {
88
+ used_percent: 27,
89
+ reset_at: 1776880800,
90
+ limit_window_seconds: 18000,
91
+ },
92
+ secondary_window: {
93
+ used_percent: 11,
94
+ reset_at: 1777485600,
95
+ limit_window_seconds: 604800,
96
+ },
97
+ },
98
+ });
99
+
100
+ expect(windows).toHaveLength(2);
101
+ expect(windows[0]).toMatchObject({
102
+ provider: "openai-codex",
103
+ label: "5h",
104
+ usedPercent: 27,
105
+ windowSeconds: 18000,
106
+ });
107
+ expect(windows[1]).toMatchObject({
108
+ provider: "openai-codex",
109
+ label: "7d",
110
+ usedPercent: 11,
111
+ windowSeconds: 604800,
112
+ });
113
+ });
114
+
115
+ it("handles alternate field names", () => {
116
+ const windows = parseCodexUsage({
117
+ rate_limits: {
118
+ five_hour_limit: {
119
+ percent_left: 61,
120
+ reset_time_ms: 1776880800000,
121
+ limit_window_seconds: 18000,
122
+ },
123
+ weekly_limit: {
124
+ percent_left: 83,
125
+ reset_time_ms: 1777485600000,
126
+ limit_window_seconds: 604800,
127
+ },
128
+ },
129
+ });
130
+
131
+ expect(windows[0]).toMatchObject({ usedPercent: 39, label: "5h" });
132
+ expect(windows[1]).toMatchObject({ usedPercent: 17, label: "7d" });
133
+ });
134
+
135
+ it("includes credits window when balance is present", () => {
136
+ const windows = parseCodexUsage({
137
+ plan_type: "team",
138
+ rate_limit: {
139
+ primary_window: { used_percent: 10, reset_at: 1776880800, limit_window_seconds: 18000 },
140
+ },
141
+ credits: {
142
+ has_credits: true,
143
+ unlimited: false,
144
+ balance: 4200,
145
+ approx_local_messages: 840,
146
+ approx_cloud_messages: 168,
147
+ },
148
+ });
149
+
150
+ const credit = windows.find((w) => w.label === "Credits");
151
+ expect(credit).toBeDefined();
152
+ expect(credit).toMatchObject({ isCurrency: true, usedValue: 4200 });
153
+ });
154
+
155
+ it("includes spend control status", () => {
156
+ const windows = parseCodexUsage({
157
+ plan_type: "team",
158
+ rate_limit: {
159
+ primary_window: { used_percent: 10, reset_at: 1776880800, limit_window_seconds: 18000 },
160
+ },
161
+ spend_control: { reached: true },
162
+ });
163
+
164
+ const sc = windows.find((w) => w.label === "Spend cap");
165
+ expect(sc).toBeDefined();
166
+ expect(sc).toMatchObject({ limited: true, usedPercent: 100 });
167
+ });
168
+
169
+ it("skips credits when no balance", () => {
170
+ const windows = parseCodexUsage({
171
+ rate_limit: {
172
+ primary_window: { used_percent: 10, reset_at: 1776880800, limit_window_seconds: 18000 },
173
+ },
174
+ credits: { has_credits: false, balance: null },
175
+ });
176
+ expect(windows.find((w) => w.label === "Credits")).toBeUndefined();
177
+ });
178
+ });
179
+
180
+ describe("parseGitHubCopilotUsage", () => {
181
+ it("maps premium interaction quota snapshot", () => {
182
+ const windows = parseGitHubCopilotUsage({
183
+ copilot_plan: "pro",
184
+ quota_reset_date: "2026-05-01T00:00:00Z",
185
+ quota_snapshots: {
186
+ premium_interactions: {
187
+ entitlement: 300,
188
+ remaining: 240,
189
+ percent_remaining: 80,
190
+ quota_id: "premium",
191
+ },
192
+ chat: {
193
+ entitlement: 1000,
194
+ remaining: 950,
195
+ percent_remaining: 95,
196
+ quota_id: "chat",
197
+ },
198
+ },
199
+ });
200
+
201
+ expect(windows).toHaveLength(2);
202
+ expect(windows[0]).toMatchObject({
203
+ provider: "github-copilot",
204
+ label: "Premium / month",
205
+ usedPercent: 20,
206
+ usedValue: 60,
207
+ limitValue: 300,
208
+ });
209
+ expect(windows[1]).toMatchObject({
210
+ provider: "github-copilot",
211
+ label: "Chat / month",
212
+ usedPercent: 5,
213
+ usedValue: 50,
214
+ limitValue: 1000,
215
+ });
216
+ });
217
+
218
+ it("includes overage info on premium interactions", () => {
219
+ const windows = parseGitHubCopilotUsage({
220
+ copilot_plan: "business",
221
+ quota_reset_date: "2026-05-01T00:00:00Z",
222
+ quota_snapshots: {
223
+ premium_interactions: {
224
+ entitlement: 300,
225
+ remaining: 293,
226
+ percent_remaining: 97.8,
227
+ overage_count: 5,
228
+ overage_permitted: true,
229
+ },
230
+ },
231
+ });
232
+
233
+ const premium = windows.find((w) => w.label === "Premium / month");
234
+ expect(premium).toBeDefined();
235
+ expect(premium!.nextAmount).toBe("+5 overage");
236
+ });
237
+
238
+ it("handles free-tier completions data", () => {
239
+ const windows = parseGitHubCopilotUsage({
240
+ access_type_sku: "free_limited_copilot",
241
+ limited_user_reset_date: "2026-05-01",
242
+ monthly_quotas: {
243
+ chat: 500,
244
+ completions: 4000,
245
+ },
246
+ limited_user_quotas: {
247
+ chat: 410,
248
+ completions: 4000,
249
+ },
250
+ });
251
+
252
+ expect(windows).toHaveLength(2);
253
+ expect(windows[0]).toMatchObject({ label: "Chat / month", usedPercent: 18 });
254
+ expect(windows[1]).toMatchObject({ label: "Completions / month", usedPercent: 0 });
255
+ });
256
+ });
@@ -0,0 +1,245 @@
1
+ import type { QuotaWindow } from "../types/quotas.js";
2
+ import { safePercent } from "../utils/quotas-severity.js";
3
+
4
+ function parseDateish(value: unknown): Date {
5
+ if (typeof value === "number") {
6
+ const ms = value > 10 ** 11 ? value : value * 1000;
7
+ return new Date(ms);
8
+ }
9
+ if (typeof value === "string") return new Date(value);
10
+ return new Date(0);
11
+ }
12
+
13
+ function monthWindowSeconds(resetAt: Date): number {
14
+ const approxStart = new Date(resetAt);
15
+ approxStart.setMonth(approxStart.getMonth() - 1);
16
+ return Math.max(1, Math.round((resetAt.getTime() - approxStart.getTime()) / 1000));
17
+ }
18
+
19
+ export function parseAnthropicUsage(data: any): QuotaWindow[] {
20
+ const windows: QuotaWindow[] = [];
21
+
22
+ if (data?.five_hour) {
23
+ windows.push({
24
+ provider: "anthropic",
25
+ label: "5h",
26
+ usedPercent: Number(data.five_hour.utilization ?? 0),
27
+ resetsAt: parseDateish(data.five_hour.resets_at),
28
+ windowSeconds: 5 * 60 * 60,
29
+ usedValue: Number(data.five_hour.utilization ?? 0),
30
+ limitValue: 100,
31
+ showPace: false,
32
+ nextLabel: "Resets",
33
+ });
34
+ }
35
+
36
+ if (data?.seven_day) {
37
+ windows.push({
38
+ provider: "anthropic",
39
+ label: "7d",
40
+ usedPercent: Number(data.seven_day.utilization ?? 0),
41
+ resetsAt: parseDateish(data.seven_day.resets_at),
42
+ windowSeconds: 7 * 24 * 60 * 60,
43
+ usedValue: Number(data.seven_day.utilization ?? 0),
44
+ limitValue: 100,
45
+ showPace: false,
46
+ nextLabel: "Resets",
47
+ });
48
+ }
49
+
50
+ // Per-model 7d windows
51
+ const modelWindows: Array<[string, string]> = [
52
+ ["seven_day_sonnet", "7d Sonnet"],
53
+ ["seven_day_omelette", "7d Opus"],
54
+ ["seven_day_opus", "7d Opus (legacy)"],
55
+ ];
56
+ for (const [key, label] of modelWindows) {
57
+ const entry = data?.[key];
58
+ if (entry && typeof entry === "object" && entry.utilization != null) {
59
+ windows.push({
60
+ provider: "anthropic",
61
+ label,
62
+ usedPercent: Number(entry.utilization),
63
+ resetsAt: parseDateish(entry.resets_at),
64
+ windowSeconds: 7 * 24 * 60 * 60,
65
+ usedValue: Number(entry.utilization),
66
+ limitValue: 100,
67
+ showPace: false,
68
+ nextLabel: "Resets",
69
+ });
70
+ }
71
+ }
72
+
73
+ // Extra usage (overage budget)
74
+ const extra = data?.extra_usage;
75
+ if (extra && extra.is_enabled && extra.monthly_limit > 0) {
76
+ const limitDollars = extra.monthly_limit / 100;
77
+ const usedDollars = (extra.used_credits ?? 0) / 100;
78
+ const currency = extra.currency ?? "USD";
79
+ windows.push({
80
+ provider: "anthropic",
81
+ label: `Extra (${currency})`,
82
+ usedPercent: Number(extra.utilization ?? safePercent(usedDollars, limitDollars)),
83
+ resetsAt: new Date(new Date().getFullYear(), new Date().getMonth() + 1, 1),
84
+ windowSeconds: 30 * 24 * 60 * 60,
85
+ usedValue: usedDollars,
86
+ limitValue: limitDollars,
87
+ isCurrency: true,
88
+ showPace: true,
89
+ paceScale: 1,
90
+ nextLabel: "Resets",
91
+ });
92
+ }
93
+
94
+ return windows;
95
+ }
96
+
97
+ function percentLeftToUsedPercent(limit: any): number {
98
+ if (limit?.percent_left != null) return Math.max(0, 100 - Number(limit.percent_left));
99
+ if (limit?.remaining_percent != null) return Math.max(0, 100 - Number(limit.remaining_percent));
100
+ if (limit?.used_percent != null) return Number(limit.used_percent);
101
+ return 0;
102
+ }
103
+
104
+ export function parseCodexUsage(data: any): QuotaWindow[] {
105
+ const rateLimit = data?.rate_limit ?? data?.rate_limits ?? {};
106
+ const primary = rateLimit.primary_window ?? rateLimit.primary ?? rateLimit.five_hour_limit ?? rateLimit.five_hour;
107
+ const secondary = rateLimit.secondary_window ?? rateLimit.secondary ?? rateLimit.weekly_limit ?? rateLimit.weekly;
108
+
109
+ const windows: QuotaWindow[] = [];
110
+
111
+ if (primary) {
112
+ windows.push({
113
+ provider: "openai-codex",
114
+ label: "5h",
115
+ usedPercent: percentLeftToUsedPercent(primary),
116
+ resetsAt: parseDateish(primary.reset_at ?? primary.reset_time_ms),
117
+ windowSeconds: Number(primary.limit_window_seconds ?? 5 * 60 * 60),
118
+ usedValue: percentLeftToUsedPercent(primary),
119
+ limitValue: 100,
120
+ showPace: false,
121
+ nextLabel: "Resets",
122
+ });
123
+ }
124
+
125
+ if (secondary) {
126
+ windows.push({
127
+ provider: "openai-codex",
128
+ label: "7d",
129
+ usedPercent: percentLeftToUsedPercent(secondary),
130
+ resetsAt: parseDateish(secondary.reset_at ?? secondary.reset_time_ms),
131
+ windowSeconds: Number(secondary.limit_window_seconds ?? 7 * 24 * 60 * 60),
132
+ usedValue: percentLeftToUsedPercent(secondary),
133
+ limitValue: 100,
134
+ showPace: false,
135
+ nextLabel: "Resets",
136
+ });
137
+ }
138
+
139
+ // Credits balance
140
+ const credits = data?.credits;
141
+ if (credits && credits.has_credits && credits.balance != null) {
142
+ const balance = Number(credits.balance);
143
+ windows.push({
144
+ provider: "openai-codex",
145
+ label: "Credits",
146
+ usedPercent: 0,
147
+ resetsAt: new Date(0),
148
+ windowSeconds: 0,
149
+ usedValue: balance,
150
+ limitValue: balance,
151
+ isCurrency: true,
152
+ showPace: false,
153
+ nextLabel: credits.approx_local_messages
154
+ ? `~${credits.approx_local_messages} local msgs`
155
+ : undefined,
156
+ });
157
+ }
158
+
159
+ // Spend control
160
+ const spendControl = data?.spend_control;
161
+ if (spendControl) {
162
+ const reached = !!spendControl.reached;
163
+ windows.push({
164
+ provider: "openai-codex",
165
+ label: "Spend cap",
166
+ usedPercent: reached ? 100 : 0,
167
+ resetsAt: new Date(0),
168
+ windowSeconds: 0,
169
+ usedValue: reached ? 1 : 0,
170
+ limitValue: 1,
171
+ limited: reached,
172
+ showPace: false,
173
+ nextLabel: reached ? "Reached" : "OK",
174
+ });
175
+ }
176
+
177
+ return windows;
178
+ }
179
+
180
+ export function parseGitHubCopilotUsage(data: any): QuotaWindow[] {
181
+ const windows: QuotaWindow[] = [];
182
+
183
+ const resetAt = parseDateish(data?.quota_reset_date ?? data?.quota_reset_date_utc ?? data?.limited_user_reset_date);
184
+ const periodSeconds = monthWindowSeconds(resetAt);
185
+
186
+ const snapshots = data?.quota_snapshots;
187
+ if (snapshots && typeof snapshots === "object") {
188
+ const mappings: Array<[string, string]> = [
189
+ ["premium_interactions", "Premium / month"],
190
+ ["chat", "Chat / month"],
191
+ ["completions", "Completions / month"],
192
+ ];
193
+
194
+ for (const [key, label] of mappings) {
195
+ const snap = snapshots[key];
196
+ if (!snap || snap.unlimited) continue;
197
+ const entitlement = Number(snap.entitlement ?? 0);
198
+ const remaining = Number(snap.remaining ?? snap.quota_remaining ?? 0);
199
+ if (entitlement <= 0) continue;
200
+ const overageCount = Number(snap.overage_count ?? 0);
201
+ const overagePermitted = !!snap.overage_permitted;
202
+ windows.push({
203
+ provider: "github-copilot",
204
+ label,
205
+ usedPercent: safePercent(entitlement - remaining, entitlement),
206
+ resetsAt: resetAt,
207
+ windowSeconds: periodSeconds,
208
+ usedValue: entitlement - remaining,
209
+ limitValue: entitlement,
210
+ showPace: true,
211
+ nextLabel: "Resets",
212
+ nextAmount: overageCount > 0
213
+ ? `+${overageCount} overage`
214
+ : overagePermitted
215
+ ? "overage allowed"
216
+ : undefined,
217
+ });
218
+ }
219
+ return windows;
220
+ }
221
+
222
+ if (data?.monthly_quotas && data?.limited_user_quotas) {
223
+ for (const [key, label] of [
224
+ ["chat", "Chat / month"],
225
+ ["completions", "Completions / month"],
226
+ ] as const) {
227
+ const limitValue = Number(data.monthly_quotas[key] ?? 0);
228
+ const remaining = Number(data.limited_user_quotas[key] ?? 0);
229
+ if (limitValue <= 0) continue;
230
+ windows.push({
231
+ provider: "github-copilot",
232
+ label,
233
+ usedPercent: safePercent(limitValue - remaining, limitValue),
234
+ resetsAt: resetAt,
235
+ windowSeconds: periodSeconds,
236
+ usedValue: limitValue - remaining,
237
+ limitValue,
238
+ showPace: true,
239
+ nextLabel: "Resets",
240
+ });
241
+ }
242
+ }
243
+
244
+ return windows;
245
+ }
@@ -0,0 +1,31 @@
1
+ export type SupportedQuotaProvider =
2
+ | "anthropic"
3
+ | "openai-codex"
4
+ | "github-copilot";
5
+
6
+ export type QuotasErrorKind =
7
+ | "cancelled"
8
+ | "timeout"
9
+ | "config"
10
+ | "http"
11
+ | "network";
12
+
13
+ export type QuotasResult =
14
+ | { success: true; data: { windows: QuotaWindow[]; provider: SupportedQuotaProvider } }
15
+ | { success: false; error: { message: string; kind: QuotasErrorKind } };
16
+
17
+ export interface QuotaWindow {
18
+ provider: SupportedQuotaProvider;
19
+ label: string;
20
+ usedPercent: number;
21
+ resetsAt: Date;
22
+ windowSeconds: number;
23
+ usedValue: number;
24
+ limitValue: number;
25
+ isCurrency?: boolean;
26
+ showPace?: boolean;
27
+ paceScale?: number;
28
+ limited?: boolean;
29
+ nextAmount?: string;
30
+ nextLabel?: string;
31
+ }
@@ -0,0 +1,111 @@
1
+ import { assert, describe, expect, it } from "vitest";
2
+ import {
3
+ assessWindow,
4
+ getPacePercent,
5
+ getProjectedPercent,
6
+ getSeverityColor,
7
+ type QuotaWindow,
8
+ safePercent,
9
+ } from "./quotas-severity.js";
10
+
11
+ function makeWindow(
12
+ overrides: Partial<QuotaWindow> & Pick<QuotaWindow, "usedPercent">,
13
+ ): QuotaWindow {
14
+ const windowSeconds = overrides.windowSeconds ?? 3600;
15
+ const resetsAt =
16
+ overrides.resetsAt ?? new Date(Date.now() + windowSeconds * 500);
17
+ return {
18
+ provider: "anthropic",
19
+ label: "Test Window",
20
+ resetsAt,
21
+ windowSeconds,
22
+ usedValue: 0,
23
+ limitValue: 100,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ describe("safePercent", () => {
29
+ it("returns 0 for zero/invalid limit", () => {
30
+ expect(safePercent(50, 0)).toBe(0);
31
+ expect(safePercent(50, -1)).toBe(0);
32
+ expect(safePercent(50, NaN)).toBe(0);
33
+ expect(safePercent(NaN, 100)).toBe(0);
34
+ });
35
+
36
+ it("computes correct percentage", () => {
37
+ expect(safePercent(50, 100)).toBe(50);
38
+ expect(safePercent(75, 100)).toBe(75);
39
+ expect(safePercent(1, 3)).toBeCloseTo(33.33);
40
+ });
41
+ });
42
+
43
+ describe("getPacePercent", () => {
44
+ it("returns null for zero window", () => {
45
+ const w = makeWindow({ usedPercent: 50, windowSeconds: 0 });
46
+ expect(getPacePercent(w)).toBeNull();
47
+ });
48
+
49
+ it("returns ~50 for a window 50% elapsed", () => {
50
+ const w = makeWindow({
51
+ usedPercent: 50,
52
+ windowSeconds: 3600,
53
+ resetsAt: new Date(Date.now() + 1800 * 1000),
54
+ });
55
+ const pace = getPacePercent(w);
56
+ assert(pace, "pace should not be null");
57
+ expect(pace).toBeCloseTo(50, 0);
58
+ });
59
+ });
60
+
61
+ describe("getProjectedPercent", () => {
62
+ it("returns usedPercent when no pace", () => {
63
+ expect(getProjectedPercent(42, null)).toBe(42);
64
+ });
65
+
66
+ it("projects based on pace", () => {
67
+ expect(getProjectedPercent(50, 25)).toBe(200);
68
+ });
69
+ });
70
+
71
+ describe("assessWindow", () => {
72
+ it("returns critical for limited window regardless of usage", () => {
73
+ const w = makeWindow({ usedPercent: 5, showPace: false, limited: true });
74
+ expect(assessWindow(w).severity).toBe("critical");
75
+ });
76
+
77
+ it("returns warning when projected exceeds dynamic warn threshold", () => {
78
+ const w = makeWindow({
79
+ usedPercent: 95,
80
+ showPace: true,
81
+ paceScale: 1,
82
+ windowSeconds: 3600,
83
+ resetsAt: new Date(Date.now() + 1800 * 1000),
84
+ });
85
+ expect(assessWindow(w).severity).toBe("warning");
86
+ });
87
+
88
+ it("uses paceScale to normalize pace", () => {
89
+ const w = makeWindow({
90
+ usedPercent: 95,
91
+ showPace: true,
92
+ paceScale: 1 / 7,
93
+ windowSeconds: 7 * 24 * 3600,
94
+ resetsAt: new Date(Date.now() + 6 * 24 * 3600 * 1000),
95
+ });
96
+ const result = assessWindow(w);
97
+ assert(result.pacePercent, "pacePercent should not be null");
98
+ expect(result.pacePercent).toBeLessThan(15);
99
+ expect(result.projectedPercent).toBeGreaterThan(500);
100
+ expect(result.severity).toBe("critical");
101
+ });
102
+ });
103
+
104
+ describe("getSeverityColor", () => {
105
+ it("maps severity levels to display colors", () => {
106
+ expect(getSeverityColor("none")).toBe("success");
107
+ expect(getSeverityColor("warning")).toBe("warning");
108
+ expect(getSeverityColor("high")).toBe("error");
109
+ expect(getSeverityColor("critical")).toBe("error");
110
+ });
111
+ });