@getrouter/getrouter-cli 0.1.1 โ 0.1.3
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/.serena/project.yml +84 -0
- package/CLAUDE.md +52 -0
- package/biome.json +1 -1
- package/bun.lock +10 -10
- package/dist/bin.mjs +245 -94
- package/package.json +2 -2
- package/src/cli.ts +2 -1
- package/src/cmd/codex.ts +17 -7
- package/src/cmd/env.ts +1 -1
- package/src/cmd/keys.ts +46 -28
- package/src/cmd/models.ts +2 -1
- package/src/core/api/pagination.ts +25 -0
- package/src/core/api/providerModels.ts +32 -0
- package/src/core/auth/refresh.ts +68 -0
- package/src/core/config/fs.ts +33 -2
- package/src/core/config/index.ts +2 -8
- package/src/core/config/paths.ts +6 -3
- package/src/core/http/request.ts +71 -15
- package/src/core/http/retry.ts +68 -0
- package/src/core/interactive/codex.ts +21 -0
- package/src/core/interactive/keys.ts +19 -10
- package/src/core/output/usages.ts +11 -30
- package/src/core/setup/codex.ts +4 -0
- package/src/core/setup/env.ts +14 -6
- package/tests/auth/refresh.test.ts +149 -0
- package/tests/cmd/codex.test.ts +87 -1
- package/tests/cmd/keys.test.ts +48 -14
- package/tests/cmd/models.test.ts +5 -2
- package/tests/cmd/usages.test.ts +5 -5
- package/tests/config/fs.test.ts +22 -1
- package/tests/config/index.test.ts +16 -1
- package/tests/config/paths.test.ts +23 -0
- package/tests/core/api/pagination.test.ts +87 -0
- package/tests/core/interactive/codex.test.ts +25 -1
- package/tests/core/setup/env.test.ts +18 -4
- package/tests/http/request.test.ts +157 -0
- package/tests/http/retry.test.ts +152 -0
- package/tests/output/usages.test.ts +11 -12
- package/tsconfig.json +3 -2
- package/src/core/paths.ts +0 -4
- package/tests/paths.test.ts +0 -9
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
import {
|
|
3
|
+
getCodexModelChoices,
|
|
3
4
|
MODEL_CHOICES,
|
|
4
5
|
mapReasoningValue,
|
|
5
6
|
REASONING_CHOICES,
|
|
6
7
|
} from "../../../src/core/interactive/codex";
|
|
7
8
|
|
|
8
9
|
describe("codex interactive helpers", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.unstubAllGlobals();
|
|
12
|
+
});
|
|
13
|
+
|
|
9
14
|
it("maps extra high to xhigh", () => {
|
|
10
15
|
expect(mapReasoningValue("extra_high")).toBe("xhigh");
|
|
11
16
|
});
|
|
@@ -14,4 +19,23 @@ describe("codex interactive helpers", () => {
|
|
|
14
19
|
expect(MODEL_CHOICES.length).toBeGreaterThan(0);
|
|
15
20
|
expect(REASONING_CHOICES.length).toBeGreaterThan(0);
|
|
16
21
|
});
|
|
22
|
+
|
|
23
|
+
it("fetches codex models from ListProviderModels(tag=codex)", async () => {
|
|
24
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
25
|
+
ok: true,
|
|
26
|
+
status: 200,
|
|
27
|
+
json: vi.fn().mockResolvedValue({
|
|
28
|
+
models: ["new-codex-model-xyz"],
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
32
|
+
|
|
33
|
+
const choices = await getCodexModelChoices();
|
|
34
|
+
expect(
|
|
35
|
+
choices.some((choice) => choice.value === "new-codex-model-xyz"),
|
|
36
|
+
).toBe(true);
|
|
37
|
+
expect(String(fetchMock.mock.calls[0]?.[0] ?? "")).toContain(
|
|
38
|
+
"/v1/dashboard/providers/models?tag=codex",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
17
41
|
});
|
|
@@ -23,17 +23,31 @@ describe("setup env helpers", () => {
|
|
|
23
23
|
it("renders sh env", () => {
|
|
24
24
|
const output = renderEnv("sh", vars);
|
|
25
25
|
expect(output).toContain(
|
|
26
|
-
"export OPENAI_BASE_URL=https://api.getrouter.dev/codex",
|
|
26
|
+
"export OPENAI_BASE_URL='https://api.getrouter.dev/codex'",
|
|
27
27
|
);
|
|
28
|
-
expect(output).toContain("export ANTHROPIC_API_KEY=key-123");
|
|
28
|
+
expect(output).toContain("export ANTHROPIC_API_KEY='key-123'");
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
it("renders ps1 env", () => {
|
|
32
32
|
const output = renderEnv("ps1", vars);
|
|
33
33
|
expect(output).toContain(
|
|
34
|
-
|
|
34
|
+
"$env:OPENAI_BASE_URL='https://api.getrouter.dev/codex'",
|
|
35
35
|
);
|
|
36
|
-
expect(output).toContain(
|
|
36
|
+
expect(output).toContain("$env:ANTHROPIC_API_KEY='key-123'");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("escapes values safely", () => {
|
|
40
|
+
expect(
|
|
41
|
+
renderEnv("sh", {
|
|
42
|
+
openaiApiKey: "a'b",
|
|
43
|
+
}),
|
|
44
|
+
).toContain("export OPENAI_API_KEY='a'\\''b'");
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
renderEnv("ps1", {
|
|
48
|
+
openaiApiKey: "a'b",
|
|
49
|
+
}),
|
|
50
|
+
).toContain("$env:OPENAI_API_KEY='a''b'");
|
|
37
51
|
});
|
|
38
52
|
|
|
39
53
|
it("writes env file", () => {
|
|
@@ -79,4 +79,161 @@ describe("requestJson", () => {
|
|
|
79
79
|
const headers = (init?.headers ?? {}) as Record<string, string>;
|
|
80
80
|
expect(headers.Cookie).toBe("router_auth=t2");
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
it("retries with refreshed token on 401", async () => {
|
|
84
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
85
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
86
|
+
fs.writeFileSync(
|
|
87
|
+
path.join(dir, "auth.json"),
|
|
88
|
+
JSON.stringify({
|
|
89
|
+
accessToken: "expired",
|
|
90
|
+
refreshToken: "refresh-token",
|
|
91
|
+
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
92
|
+
tokenType: "Bearer",
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
let callCount = 0;
|
|
97
|
+
const fetchSpy = vi.fn(
|
|
98
|
+
async (input: RequestInfo | URL, _init?: RequestInit) => {
|
|
99
|
+
callCount++;
|
|
100
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
101
|
+
|
|
102
|
+
// Refresh token endpoint
|
|
103
|
+
if (url.includes("auth/token")) {
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
json: async () => ({
|
|
107
|
+
accessToken: "new-access",
|
|
108
|
+
refreshToken: "new-refresh",
|
|
109
|
+
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
110
|
+
}),
|
|
111
|
+
} as Response;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// First call returns 401, second succeeds
|
|
115
|
+
if (callCount === 1) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
status: 401,
|
|
119
|
+
statusText: "Unauthorized",
|
|
120
|
+
json: async () => ({ message: "Token expired" }),
|
|
121
|
+
} as Response;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
json: async () => ({ ok: true }),
|
|
127
|
+
} as Response;
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const res = await requestJson<{ ok: boolean }>({
|
|
132
|
+
path: "/v1/test",
|
|
133
|
+
method: "GET",
|
|
134
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(res.ok).toBe(true);
|
|
138
|
+
expect(fetchSpy).toHaveBeenCalledTimes(3); // initial + refresh + retry
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("does not retry when no refresh token", async () => {
|
|
142
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
143
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
144
|
+
fs.writeFileSync(
|
|
145
|
+
path.join(dir, "auth.json"),
|
|
146
|
+
JSON.stringify({ accessToken: "expired", refreshToken: "" }),
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const fetchSpy = vi.fn(
|
|
150
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
151
|
+
({
|
|
152
|
+
ok: false,
|
|
153
|
+
status: 401,
|
|
154
|
+
statusText: "Unauthorized",
|
|
155
|
+
json: async () => ({ message: "Token expired" }),
|
|
156
|
+
}) as Response,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await expect(
|
|
160
|
+
requestJson({
|
|
161
|
+
path: "/v1/test",
|
|
162
|
+
method: "GET",
|
|
163
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
164
|
+
}),
|
|
165
|
+
).rejects.toThrow();
|
|
166
|
+
|
|
167
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1); // no retry
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("retries on 5xx server errors", async () => {
|
|
171
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
172
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
173
|
+
fs.writeFileSync(
|
|
174
|
+
path.join(dir, "auth.json"),
|
|
175
|
+
JSON.stringify({ accessToken: "token", refreshToken: "" }),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
let callCount = 0;
|
|
179
|
+
const fetchSpy = vi.fn(
|
|
180
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) => {
|
|
181
|
+
callCount++;
|
|
182
|
+
if (callCount === 1) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
status: 503,
|
|
186
|
+
statusText: "Service Unavailable",
|
|
187
|
+
json: async () => ({ message: "Server overloaded" }),
|
|
188
|
+
} as Response;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
json: async () => ({ ok: true }),
|
|
193
|
+
} as Response;
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const noopSleep = async () => {};
|
|
198
|
+
const res = await requestJson<{ ok: boolean }>({
|
|
199
|
+
path: "/v1/test",
|
|
200
|
+
method: "GET",
|
|
201
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
202
|
+
maxRetries: 2,
|
|
203
|
+
_retrySleep: noopSleep,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(res.ok).toBe(true);
|
|
207
|
+
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not retry on 4xx client errors", async () => {
|
|
211
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "getrouter-"));
|
|
212
|
+
process.env.GETROUTER_CONFIG_DIR = dir;
|
|
213
|
+
fs.writeFileSync(
|
|
214
|
+
path.join(dir, "auth.json"),
|
|
215
|
+
JSON.stringify({ accessToken: "token", refreshToken: "" }),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const fetchSpy = vi.fn(
|
|
219
|
+
async (_input: RequestInfo | URL, _init?: RequestInit) =>
|
|
220
|
+
({
|
|
221
|
+
ok: false,
|
|
222
|
+
status: 404,
|
|
223
|
+
statusText: "Not Found",
|
|
224
|
+
json: async () => ({ message: "Not found" }),
|
|
225
|
+
}) as Response,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
await expect(
|
|
229
|
+
requestJson({
|
|
230
|
+
path: "/v1/test",
|
|
231
|
+
method: "GET",
|
|
232
|
+
fetchImpl: fetchSpy as unknown as typeof fetch,
|
|
233
|
+
maxRetries: 2,
|
|
234
|
+
}),
|
|
235
|
+
).rejects.toThrow();
|
|
236
|
+
|
|
237
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1); // no retry on 404
|
|
238
|
+
});
|
|
82
239
|
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { isServerError, withRetry } from "../../src/core/http/retry";
|
|
3
|
+
|
|
4
|
+
describe("isServerError", () => {
|
|
5
|
+
it("returns true for 5xx errors", () => {
|
|
6
|
+
expect(isServerError(500)).toBe(true);
|
|
7
|
+
expect(isServerError(502)).toBe(true);
|
|
8
|
+
expect(isServerError(503)).toBe(true);
|
|
9
|
+
expect(isServerError(599)).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns true for 408 and 429", () => {
|
|
13
|
+
expect(isServerError(408)).toBe(true);
|
|
14
|
+
expect(isServerError(429)).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns false for 4xx client errors", () => {
|
|
18
|
+
expect(isServerError(400)).toBe(false);
|
|
19
|
+
expect(isServerError(401)).toBe(false);
|
|
20
|
+
expect(isServerError(403)).toBe(false);
|
|
21
|
+
expect(isServerError(404)).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns false for 2xx success", () => {
|
|
25
|
+
expect(isServerError(200)).toBe(false);
|
|
26
|
+
expect(isServerError(201)).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("withRetry", () => {
|
|
31
|
+
it("returns result on first success", async () => {
|
|
32
|
+
const fn = vi.fn().mockResolvedValue("success");
|
|
33
|
+
const result = await withRetry(fn);
|
|
34
|
+
expect(result).toBe("success");
|
|
35
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("retries on failure and succeeds", async () => {
|
|
39
|
+
const fn = vi
|
|
40
|
+
.fn()
|
|
41
|
+
.mockRejectedValueOnce(new TypeError("network error"))
|
|
42
|
+
.mockResolvedValueOnce("success");
|
|
43
|
+
|
|
44
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
45
|
+
const result = await withRetry(fn, { sleep });
|
|
46
|
+
|
|
47
|
+
expect(result).toBe("success");
|
|
48
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
49
|
+
expect(sleep).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("respects maxRetries", async () => {
|
|
53
|
+
const fn = vi.fn().mockRejectedValue(new TypeError("network error"));
|
|
54
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
55
|
+
|
|
56
|
+
await expect(withRetry(fn, { maxRetries: 2, sleep })).rejects.toThrow(
|
|
57
|
+
"network error",
|
|
58
|
+
);
|
|
59
|
+
expect(fn).toHaveBeenCalledTimes(3); // initial + 2 retries
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("calls onRetry callback", async () => {
|
|
63
|
+
const fn = vi
|
|
64
|
+
.fn()
|
|
65
|
+
.mockRejectedValueOnce(new TypeError("fail"))
|
|
66
|
+
.mockResolvedValueOnce("ok");
|
|
67
|
+
|
|
68
|
+
const onRetry = vi.fn();
|
|
69
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
70
|
+
|
|
71
|
+
await withRetry(fn, { onRetry, sleep });
|
|
72
|
+
|
|
73
|
+
expect(onRetry).toHaveBeenCalledWith(expect.any(TypeError), 1, 1000);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("uses exponential backoff", async () => {
|
|
77
|
+
const fn = vi
|
|
78
|
+
.fn()
|
|
79
|
+
.mockRejectedValueOnce(new TypeError("fail1"))
|
|
80
|
+
.mockRejectedValueOnce(new TypeError("fail2"))
|
|
81
|
+
.mockResolvedValueOnce("ok");
|
|
82
|
+
|
|
83
|
+
const onRetry = vi.fn();
|
|
84
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
85
|
+
|
|
86
|
+
await withRetry(fn, {
|
|
87
|
+
onRetry,
|
|
88
|
+
sleep,
|
|
89
|
+
initialDelayMs: 100,
|
|
90
|
+
maxDelayMs: 500,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(onRetry).toHaveBeenCalledTimes(2);
|
|
94
|
+
expect(onRetry).toHaveBeenNthCalledWith(1, expect.any(TypeError), 1, 100);
|
|
95
|
+
expect(onRetry).toHaveBeenNthCalledWith(2, expect.any(TypeError), 2, 200);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("respects maxDelayMs", async () => {
|
|
99
|
+
const fn = vi
|
|
100
|
+
.fn()
|
|
101
|
+
.mockRejectedValueOnce(new TypeError("1"))
|
|
102
|
+
.mockRejectedValueOnce(new TypeError("2"))
|
|
103
|
+
.mockRejectedValueOnce(new TypeError("3"))
|
|
104
|
+
.mockResolvedValueOnce("ok");
|
|
105
|
+
|
|
106
|
+
const onRetry = vi.fn();
|
|
107
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
108
|
+
|
|
109
|
+
await withRetry(fn, {
|
|
110
|
+
onRetry,
|
|
111
|
+
sleep,
|
|
112
|
+
initialDelayMs: 100,
|
|
113
|
+
maxDelayMs: 150,
|
|
114
|
+
maxRetries: 5,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(onRetry).toHaveBeenNthCalledWith(3, expect.any(TypeError), 3, 150);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not retry when shouldRetry returns false", async () => {
|
|
121
|
+
const error = new Error("non-retryable");
|
|
122
|
+
const fn = vi.fn().mockRejectedValue(error);
|
|
123
|
+
const shouldRetry = vi.fn().mockReturnValue(false);
|
|
124
|
+
|
|
125
|
+
await expect(withRetry(fn, { shouldRetry })).rejects.toThrow(
|
|
126
|
+
"non-retryable",
|
|
127
|
+
);
|
|
128
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("retries on 500 status errors", async () => {
|
|
132
|
+
const error = { status: 500, message: "server error" };
|
|
133
|
+
const fn = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce("ok");
|
|
134
|
+
|
|
135
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
136
|
+
const result = await withRetry(fn, { sleep });
|
|
137
|
+
|
|
138
|
+
expect(result).toBe("ok");
|
|
139
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("retries on 429 rate limit", async () => {
|
|
143
|
+
const error = { status: 429, message: "rate limited" };
|
|
144
|
+
const fn = vi.fn().mockRejectedValueOnce(error).mockResolvedValueOnce("ok");
|
|
145
|
+
|
|
146
|
+
const sleep = vi.fn().mockResolvedValue(undefined);
|
|
147
|
+
const result = await withRetry(fn, { sleep });
|
|
148
|
+
|
|
149
|
+
expect(result).toBe("ok");
|
|
150
|
+
expect(fn).toHaveBeenCalledTimes(2);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { renderUsageChart } from "../../src/core/output/usages";
|
|
3
3
|
|
|
4
4
|
describe("renderUsageChart", () => {
|
|
5
|
-
it("renders
|
|
5
|
+
it("renders bars with total tokens", () => {
|
|
6
6
|
const output = renderUsageChart([
|
|
7
7
|
{
|
|
8
8
|
day: "2026-01-03",
|
|
@@ -29,16 +29,14 @@ describe("renderUsageChart", () => {
|
|
|
29
29
|
expect(output.startsWith("๐ Usage (last 7 days)")).toBe(true);
|
|
30
30
|
expect(output).toContain("Tokens");
|
|
31
31
|
expect(output).toContain("โ");
|
|
32
|
-
expect(output).toContain("
|
|
33
|
-
expect(output).toContain("I:1K");
|
|
34
|
-
expect(output).toContain("O:1K");
|
|
32
|
+
expect(output).toContain("2K");
|
|
35
33
|
expect(output).toContain("๐ Usage (last 7 days) ยท Tokens\n\n2026-01-03");
|
|
36
|
-
expect(output).toContain("
|
|
37
|
-
expect(output).toContain("
|
|
38
|
-
expect(output).not.toContain("O:
|
|
34
|
+
expect(output).not.toContain("Legend");
|
|
35
|
+
expect(output).not.toContain("I:");
|
|
36
|
+
expect(output).not.toContain("O:");
|
|
39
37
|
});
|
|
40
38
|
|
|
41
|
-
it("prints
|
|
39
|
+
it("prints total tokens only", () => {
|
|
42
40
|
const output = renderUsageChart([
|
|
43
41
|
{
|
|
44
42
|
day: "2026-01-03",
|
|
@@ -48,9 +46,9 @@ describe("renderUsageChart", () => {
|
|
|
48
46
|
requests: 2,
|
|
49
47
|
},
|
|
50
48
|
]);
|
|
51
|
-
expect(output).toContain("
|
|
52
|
-
expect(output).toContain("
|
|
53
|
-
expect(output).not.toContain("
|
|
49
|
+
expect(output).toContain("4.6K");
|
|
50
|
+
expect(output).not.toContain("I:");
|
|
51
|
+
expect(output).not.toContain("O:");
|
|
54
52
|
});
|
|
55
53
|
|
|
56
54
|
it("handles numeric strings without skewing bars", () => {
|
|
@@ -66,6 +64,7 @@ describe("renderUsageChart", () => {
|
|
|
66
64
|
],
|
|
67
65
|
10,
|
|
68
66
|
);
|
|
69
|
-
expect(output).toContain("
|
|
67
|
+
expect(output).toContain("โโโโโโโโโโ");
|
|
68
|
+
expect(output).toContain("2K");
|
|
70
69
|
});
|
|
71
70
|
});
|
package/tsconfig.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
|
-
"target": "
|
|
3
|
+
"target": "ES2021",
|
|
4
4
|
"module": "CommonJS",
|
|
5
5
|
"rootDir": ".",
|
|
6
6
|
"outDir": "dist",
|
|
7
7
|
"strict": true,
|
|
8
8
|
"esModuleInterop": true,
|
|
9
9
|
"moduleResolution": "node",
|
|
10
|
-
"skipLibCheck": true
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true
|
|
11
12
|
},
|
|
12
13
|
"include": ["src", "tests"]
|
|
13
14
|
}
|
package/src/core/paths.ts
DELETED
package/tests/paths.test.ts
DELETED