routstrd 0.4.8 → 0.4.9

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/src/cli.test.ts DELETED
@@ -1,192 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdtempSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import {
6
- collectRecentRequestsFromLines,
7
- initializeWallet,
8
- parseStructuredLogLine,
9
- } from "./cli";
10
-
11
- const tempDirs: string[] = [];
12
-
13
- function makeTempDir(): string {
14
- const dir = mkdtempSync(join(tmpdir(), "routstrd-wallet-test-"));
15
- tempDirs.push(dir);
16
- return dir;
17
- }
18
-
19
- function permissions(path: string): number {
20
- return statSync(path).mode & 0o777;
21
- }
22
-
23
- afterEach(() => {
24
- for (const dir of tempDirs.splice(0)) {
25
- rmSync(dir, { recursive: true, force: true });
26
- }
27
- });
28
-
29
- describe("parseStructuredLogLine", () => {
30
- test("extracts timestamp, level, request id, model, and routing flag", () => {
31
- const line =
32
- "[2026-08-27T13:00:00.000Z] [INFO] [req:a1b2c3d4] [model:gpt-5] Routing request with path: /v1/chat/completions";
33
-
34
- expect(parseStructuredLogLine(line)).toEqual({
35
- timestamp: "2026-08-27T13:00:00.000Z",
36
- level: "INFO",
37
- requestId: "a1b2c3d4",
38
- modelId: "gpt-5",
39
- isRouting: true,
40
- });
41
- });
42
-
43
- test("parses SDK child lines without marking them as routing lines", () => {
44
- const line =
45
- "[2026-08-27T13:00:01.000Z] [DEBUG] [req:a1b2c3d4] [model:gpt-5] [BalanceManager] token stuff";
46
-
47
- expect(parseStructuredLogLine(line)).toEqual({
48
- timestamp: "2026-08-27T13:00:01.000Z",
49
- level: "DEBUG",
50
- requestId: "a1b2c3d4",
51
- modelId: "gpt-5",
52
- isRouting: false,
53
- });
54
- });
55
-
56
- test("handles legacy lines that have a request id but no model tag", () => {
57
- const line =
58
- "[2026-08-27T13:00:02.000Z] [INFO] [req:deadbeef] Routing request with path: /v1/responses";
59
-
60
- expect(parseStructuredLogLine(line)).toEqual({
61
- timestamp: "2026-08-27T13:00:02.000Z",
62
- level: "INFO",
63
- requestId: "deadbeef",
64
- modelId: undefined,
65
- isRouting: true,
66
- });
67
- });
68
-
69
- test("extracts the bare request id from colon-joined legacy child tags", () => {
70
- const line =
71
- "[2026-08-27T13:00:04.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-flash-0731\"}";
72
-
73
- const parsed = parseStructuredLogLine(line);
74
- expect(parsed.requestId).toBe("deadbeef");
75
- expect(parsed.modelId).toBe("deepseek-v4-flash-0731");
76
- expect(parsed.isRouting).toBe(false);
77
- });
78
-
79
- test("falls back to the JSON modelId when no model tag is present", () => {
80
- const line =
81
- "[2026-08-27T13:00:05.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-pro-0813\",\"messageCount\":121}";
82
-
83
- expect(parseStructuredLogLine(line).modelId).toBe("deepseek-v4-pro-0813");
84
- });
85
-
86
- test("returns an empty record for plain non-request lines", () => {
87
- const line = "[2026-08-27T13:00:03.000Z] [WARN] Some startup message";
88
-
89
- expect(parseStructuredLogLine(line)).toEqual({
90
- timestamp: "2026-08-27T13:00:03.000Z",
91
- level: "WARN",
92
- requestId: undefined,
93
- modelId: undefined,
94
- isRouting: false,
95
- });
96
- });
97
- });
98
-
99
- describe("collectRecentRequestsFromLines", () => {
100
- const lines = [
101
- "[2026-08-27T13:00:00.000Z] [INFO] [req:11111111] [model:gpt-5] Routing request with path: /a",
102
- "[2026-08-27T13:00:01.000Z] [DEBUG] [req:11111111] [model:gpt-5] [BalanceManager] token",
103
- "[2026-08-27T13:00:02.000Z] [INFO] [req:22222222] [model:claude-sonnet-4.5] Routing request with path: /b",
104
- "[2026-08-27T13:00:03.000Z] [DEBUG] [req:22222222] [model:claude-sonnet-4.5] [ModelManager] refresh",
105
- ];
106
-
107
- test("returns one record per request, newest first", () => {
108
- expect(collectRecentRequestsFromLines(lines, 10)).toEqual([
109
- {
110
- requestId: "22222222",
111
- modelId: "claude-sonnet-4.5",
112
- timestamp: "2026-08-27T13:00:02.000Z",
113
- },
114
- {
115
- requestId: "11111111",
116
- modelId: "gpt-5",
117
- timestamp: "2026-08-27T13:00:00.000Z",
118
- },
119
- ]);
120
- });
121
-
122
- test("respects the limit", () => {
123
- expect(collectRecentRequestsFromLines(lines, 1)).toEqual([
124
- {
125
- requestId: "22222222",
126
- modelId: "claude-sonnet-4.5",
127
- timestamp: "2026-08-27T13:00:02.000Z",
128
- },
129
- ]);
130
- });
131
-
132
- test("defaults the model to unknown for legacy routing lines", () => {
133
- const legacy = [
134
- "[2026-08-27T13:00:00.000Z] [INFO] [req:deadbeef] Routing request with path: /a",
135
- ];
136
-
137
- expect(collectRecentRequestsFromLines(legacy, 10)).toEqual([
138
- {
139
- requestId: "deadbeef",
140
- modelId: "unknown",
141
- timestamp: "2026-08-27T13:00:00.000Z",
142
- },
143
- ]);
144
- });
145
-
146
- test("enriches the model from a sibling pricing line in legacy logs", () => {
147
- const legacy = [
148
- "[2026-08-27T13:00:00.000Z] [INFO] [req:deadbeef] Routing request with path: /a",
149
- "[2026-08-27T13:00:01.000Z] [INFO] [req:deadbeef:RoutstrClient] [RoutstrClient] generic request pricing input {\"modelId\":\"deepseek-v4-flash-0731\"}",
150
- ];
151
-
152
- expect(collectRecentRequestsFromLines(legacy, 10)).toEqual([
153
- {
154
- requestId: "deadbeef",
155
- modelId: "deepseek-v4-flash-0731",
156
- timestamp: "2026-08-27T13:00:00.000Z",
157
- },
158
- ]);
159
- });
160
- });
161
-
162
- describe("initializeWallet", () => {
163
- test("creates the wallet directory and config with restrictive permissions", () => {
164
- const walletDir = join(makeTempDir(), "wallet");
165
-
166
- initializeWallet(walletDir);
167
-
168
- const walletConfig = join(walletDir, "config.json");
169
- expect(permissions(walletDir)).toBe(0o700);
170
- expect(permissions(walletConfig)).toBe(0o600);
171
-
172
- const config = JSON.parse(readFileSync(walletConfig, "utf8"));
173
- expect(config.encrypted).toBe(false);
174
- expect(typeof config.mnemonic).toBe("string");
175
- expect(config.mnemonic.length).toBeGreaterThan(0);
176
- });
177
-
178
- test("repairs permissions without replacing an existing wallet", () => {
179
- const walletDir = join(makeTempDir(), "wallet");
180
- const walletConfig = join(walletDir, "config.json");
181
- const existingConfig = JSON.stringify({ mnemonic: "existing seed" });
182
-
183
- mkdirSync(walletDir, { mode: 0o755 });
184
- writeFileSync(walletConfig, existingConfig, { mode: 0o644 });
185
-
186
- initializeWallet(walletDir);
187
-
188
- expect(readFileSync(walletConfig, "utf8")).toBe(existingConfig);
189
- expect(permissions(walletDir)).toBe(0o700);
190
- expect(permissions(walletConfig)).toBe(0o600);
191
- });
192
- });
@@ -1,104 +0,0 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { unlinkSync } from "fs";
3
- import { tmpdir } from "os";
4
- import { join } from "path";
5
- import { exitOnUncaughtException } from "./fatal-error";
6
-
7
- const fatalErrorModule = JSON.stringify(join(import.meta.dir, "fatal-error.ts"));
8
-
9
- const fixturePaths: string[] = [];
10
-
11
- afterEach(() => {
12
- while (fixturePaths.length > 0) {
13
- try {
14
- unlinkSync(fixturePaths.pop()!);
15
- } catch {
16
- // Best effort cleanup of temp fixtures.
17
- }
18
- }
19
- });
20
-
21
- /** Run a fixture script in a real subprocess so process.exit is for real. */
22
- async function runFixture(source: string): Promise<{
23
- exitCode: number;
24
- stdout: string;
25
- stderr: string;
26
- }> {
27
- const scriptPath = join(
28
- tmpdir(),
29
- `routstrd-fatal-error-fixture-${crypto.randomUUID()}.ts`,
30
- );
31
- fixturePaths.push(scriptPath);
32
- await Bun.write(scriptPath, source);
33
- const proc = Bun.spawn(["bun", scriptPath], {
34
- stdout: "pipe",
35
- stderr: "pipe",
36
- });
37
- const [stdout, stderr] = await Promise.all([
38
- new Response(proc.stdout).text(),
39
- new Response(proc.stderr).text(),
40
- ]);
41
- const exitCode = await proc.exited;
42
- return { exitCode, stdout, stderr };
43
- }
44
-
45
- describe("exitOnUncaughtException", () => {
46
- test("reports the error and exits with code 1", () => {
47
- let exitCode: number | undefined;
48
- const stderrLines: string[] = [];
49
- const originalConsoleError = console.error;
50
- console.error = (...args: unknown[]) => {
51
- stderrLines.push(args.map(String).join(" "));
52
- };
53
- try {
54
- exitOnUncaughtException(new Error("boom"), (code) => {
55
- exitCode = code;
56
- });
57
- } finally {
58
- console.error = originalConsoleError;
59
- }
60
- expect(exitCode).toBe(1);
61
- expect(stderrLines.join("\n")).toContain("boom");
62
- });
63
-
64
- test("handles non-Error thrown values", () => {
65
- let exitCode: number | undefined;
66
- const originalConsoleError = console.error;
67
- console.error = () => {};
68
- try {
69
- exitOnUncaughtException("string failure", (code) => {
70
- exitCode = code;
71
- });
72
- } finally {
73
- console.error = originalConsoleError;
74
- }
75
- expect(exitCode).toBe(1);
76
- });
77
- });
78
-
79
- describe("installGlobalErrorHandlers (subprocess)", () => {
80
- test("an uncaught exception exits the process with code 1", async () => {
81
- const { exitCode, stderr } = await runFixture(`
82
- import { installGlobalErrorHandlers } from ${fatalErrorModule};
83
- installGlobalErrorHandlers();
84
- setTimeout(() => {
85
- throw new Error("boom-uncaught");
86
- }, 10);
87
- `);
88
- expect(exitCode).toBe(1);
89
- expect(stderr).toContain("boom-uncaught");
90
- });
91
-
92
- test("an unhandled rejection is logged without exiting", async () => {
93
- const { exitCode, stdout } = await runFixture(`
94
- import { installGlobalErrorHandlers } from ${fatalErrorModule};
95
- installGlobalErrorHandlers();
96
- Promise.reject(new Error("boom-rejection"));
97
- setTimeout(() => {
98
- console.log("STILL-ALIVE");
99
- }, 50);
100
- `);
101
- expect(exitCode).toBe(0);
102
- expect(stdout).toContain("STILL-ALIVE");
103
- });
104
- });
@@ -1,272 +0,0 @@
1
- import { describe, it, expect, beforeEach } from "bun:test";
2
- import { createMemoryUsageTrackingDriver } from "@routstr/sdk/storage";
3
- import type { UsageTrackingEntry } from "@routstr/sdk/storage";
4
- import type { ClientEntry } from "../../utils/clients";
5
- import { getUsageSummary, __resetUsageSummaryCacheForTest } from "./usage-summary";
6
-
7
- // ─── Test fixtures ────────────────────────────────────────────────────────────
8
- //
9
- // All timestamps are computed relative to "now" so the fixtures never age
10
- // out of getUsageSummary's 30-day rolling window. We anchor to a recent UTC
11
- // midnight ~10 days ago and keep the same hour-of-day structure the
12
- // tz-bucketing assertions depend on.
13
- //
14
- // tz = 300 (UTC-5 / EST). After shifting by -300min, UTC dates become:
15
- // d1: <anchor> 10:00Z → local <anchor> 05:00 → local day "<anchor>"
16
- // d2: <anchor+1d> 03:00Z → local <anchor> 22:00 → local day "<anchor>"
17
- // d3: <anchor+1d> 06:00Z → local <anchor+1d> 01:00 → local day "<anchor+1d>"
18
-
19
- const _BASE_UTC_MIDNIGHT = Math.floor(Date.now() / 86_400_000) * 86_400_000;
20
- const _ANCHOR_MS = _BASE_UTC_MIDNIGHT - 10 * 86_400_000; // 10 days ago UTC
21
- const D1 = _ANCHOR_MS + 10 * 3_600_000; // 10:00Z → local day <anchor>
22
- const D2 = _ANCHOR_MS + 1 * 86_400_000 + 3 * 3_600_000; // 03:00Z next day → local day <anchor>
23
- const D3 = _ANCHOR_MS + 1 * 86_400_000 + 6 * 3_600_000; // 06:00Z next day → local day <anchor+1d>
24
-
25
- /** Compute the local-day date string the same way the SDK's day grouping does. */
26
- function localDayDateStr(ts: number, tzOffsetMinutes: number): string {
27
- const d = new Date(ts - tzOffsetMinutes * 60_000);
28
- return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
29
- }
30
-
31
- const BASE_ENTRY: Omit<UsageTrackingEntry, "id" | "timestamp" | "modelId" | "client" | "totalTokens"> = {
32
- baseUrl: "https://api.openai.com/",
33
- requestId: "req-1",
34
- cost: 0.01,
35
- satsCost: 10,
36
- promptTokens: 100,
37
- completionTokens: 50,
38
- };
39
-
40
- function makeEntry(
41
- id: string,
42
- overrides: Partial<UsageTrackingEntry> & Pick<UsageTrackingEntry, "id" | "timestamp" | "modelId" | "totalTokens">,
43
- ): UsageTrackingEntry {
44
- return {
45
- ...BASE_ENTRY,
46
- ...overrides,
47
- id,
48
- };
49
- }
50
-
51
- // Seed entries:
52
- // - 2 entries for model-a, client-1 (owned by npub1)
53
- // - 1 entry for model-b, client-2 (no npub)
54
- // - 1 entry for model-a, no client (null client)
55
- // - totalTokens spread across tiny (< 1000) and small (1000-10000) buckets
56
-
57
- const ENTRIES: UsageTrackingEntry[] = [
58
- // model-a, client-1, day 2026-05-20 (D1), totalTokens=500 (tiny)
59
- makeEntry("e1", {
60
- id: "e1",
61
- timestamp: D1,
62
- modelId: "model-a",
63
- client: "client-1",
64
- totalTokens: 500,
65
- promptTokens: 350,
66
- completionTokens: 150,
67
- satsCost: 10,
68
- cost: 0.01,
69
- }),
70
- // model-a, client-1, day 2026-05-20 (D2), totalTokens=2000 (small)
71
- makeEntry("e2", {
72
- id: "e2",
73
- timestamp: D2,
74
- modelId: "model-a",
75
- client: "client-1",
76
- totalTokens: 2000,
77
- promptTokens: 1400,
78
- completionTokens: 600,
79
- satsCost: 20,
80
- cost: 0.02,
81
- }),
82
- // model-b, client-2, day 2026-05-21 (D3), totalTokens=800 (tiny)
83
- makeEntry("e3", {
84
- id: "e3",
85
- timestamp: D3,
86
- modelId: "model-b",
87
- client: "client-2",
88
- totalTokens: 800,
89
- promptTokens: 600,
90
- completionTokens: 200,
91
- satsCost: 8,
92
- cost: 0.008,
93
- }),
94
- // model-a, no client, day 2026-05-21 (D3), totalTokens=5000 (small)
95
- makeEntry("e4", {
96
- id: "e4",
97
- timestamp: D3 + 1000,
98
- modelId: "model-a",
99
- client: undefined,
100
- totalTokens: 5000,
101
- promptTokens: 3500,
102
- completionTokens: 1500,
103
- satsCost: 50,
104
- cost: 0.05,
105
- }),
106
- ];
107
-
108
- const CLIENTS: ClientEntry[] = [
109
- {
110
- clientId: "client-1",
111
- name: "Client One",
112
- apiKey: "sk-client1",
113
- createdAt: D1 - 86400000,
114
- ownerNpub: "npub1abc",
115
- },
116
- {
117
- clientId: "client-2",
118
- name: "Client Two",
119
- apiKey: "sk-client2",
120
- createdAt: D1 - 86400000,
121
- // no ownerNpub
122
- },
123
- ];
124
-
125
- const TZ = 300; // UTC-5 / EST
126
-
127
- describe("getUsageSummary", () => {
128
- let driver: ReturnType<typeof createMemoryUsageTrackingDriver>;
129
-
130
- beforeEach(async () => {
131
- // Reset the module-level memo cache explicitly so each test starts cold.
132
- // Without this, tests 2-10 would share the cached object from test 1
133
- // (all use the same fixtures → identical cache key → same TTL window).
134
- __resetUsageSummaryCacheForTest();
135
- driver = createMemoryUsageTrackingDriver(ENTRIES);
136
- });
137
-
138
- it("returns correct totals", async () => {
139
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
140
-
141
- expect(summary.totals.requests).toBe(4);
142
- expect(summary.totals.totalTokens).toBe(500 + 2000 + 800 + 5000);
143
- expect(summary.totals.promptTokens).toBe(350 + 1400 + 600 + 3500);
144
- expect(summary.totals.completionTokens).toBe(150 + 600 + 200 + 1500);
145
- expect(summary.totals.satsCost).toBe(10 + 20 + 8 + 50);
146
- expect(summary.totals.cost).toBeCloseTo(0.01 + 0.02 + 0.008 + 0.05, 5);
147
- });
148
-
149
- it("returns models sorted desc by satsCost", async () => {
150
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
151
- const { models } = summary;
152
-
153
- // model-a: 3 entries (e1, e2, e4) total satsCost = 10+20+50 = 80
154
- // model-b: 1 entry (e3) satsCost = 8
155
- expect(models).toHaveLength(2);
156
- expect(models[0]!.modelId).toBe("model-a");
157
- expect(models[0]!.satsCost).toBe(80);
158
- expect(models[0]!.requests).toBe(3);
159
- expect(models[1]!.modelId).toBe("model-b");
160
- expect(models[1]!.satsCost).toBe(8);
161
- });
162
-
163
- it("returns days most-recent-first with correct tz-bucketing", async () => {
164
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
165
- const { days } = summary;
166
-
167
- // D1 (10:00Z) and D2 (next day 03:00Z) both bucket to local day <anchor>;
168
- // D3 (next day 06:00Z) buckets to local day <anchor+1d>.
169
- // So: day <anchor> has e1+e2, day <anchor+1d> has e3+e4
170
- expect(days.length).toBeGreaterThanOrEqual(2);
171
-
172
- // Most-recent-first: <anchor+1d> first
173
- expect(days[0]!.date).toBe(localDayDateStr(D3, TZ));
174
- expect(days[0]!.requests).toBe(2); // e3, e4
175
- expect(days[0]!.satsCost).toBe(8 + 50);
176
-
177
- expect(days[1]!.date).toBe(localDayDateStr(D1, TZ));
178
- expect(days[1]!.requests).toBe(2); // e1, e2
179
- expect(days[1]!.satsCost).toBe(10 + 20);
180
- });
181
-
182
- it("returns empty hoursToday (test entries are not today)", async () => {
183
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
184
- // Test entries are ~10 days ago, not today
185
- expect(summary.hoursToday).toHaveLength(0);
186
- });
187
-
188
- it("returns correct sizeBuckets", async () => {
189
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
190
- const { sizeBuckets } = summary;
191
-
192
- // tiny [0, 1000): e1 (500) + e3 (800) = 2 entries
193
- expect(sizeBuckets.tiny.count).toBe(2);
194
- expect(sizeBuckets.tiny.cost).toBe(10 + 8);
195
-
196
- // small [1000, 10000): e2 (2000) + e4 (5000) = 2 entries
197
- expect(sizeBuckets.small.count).toBe(2);
198
- expect(sizeBuckets.small.cost).toBe(20 + 50);
199
-
200
- // medium, large, huge should be 0
201
- expect(sizeBuckets.medium.count).toBe(0);
202
- expect(sizeBuckets.large.count).toBe(0);
203
- expect(sizeBuckets.huge.count).toBe(0);
204
- });
205
-
206
- it("returns per-client topModels for top non-null clients", async () => {
207
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
208
- const { clients } = summary;
209
-
210
- // client-1: e1 + e2 (model-a), satsCost = 30
211
- // client-2: e3 (model-b), satsCost = 8
212
- // unknown (null client): e4 (model-a), satsCost = 50
213
- // Sorted desc by satsCost: unknown (50), client-1 (30), client-2 (8)
214
-
215
- const c1 = clients.find((c) => c.client === "client-1");
216
- expect(c1).toBeDefined();
217
- expect(c1!.requests).toBe(2);
218
- expect(c1!.satsCost).toBe(30);
219
- // client-1 is in top 3 non-null clients, should have topModels
220
- expect(c1!.topModels.length).toBeGreaterThan(0);
221
- expect(c1!.topModels[0]!.modelId).toBe("model-a");
222
-
223
- const c2 = clients.find((c) => c.client === "client-2");
224
- expect(c2).toBeDefined();
225
- expect(c2!.requests).toBe(1);
226
- expect(c2!.satsCost).toBe(8);
227
-
228
- // unknown client (null group) should have empty topModels
229
- const unknown = clients.find((c) => c.client === "unknown");
230
- expect(unknown).toBeDefined();
231
- expect(unknown!.topModels).toHaveLength(0);
232
- });
233
-
234
- it("folds client rows into npubs correctly", async () => {
235
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
236
- const { npubs } = summary;
237
-
238
- // Only client-1 has ownerNpub = "npub1abc"
239
- // npub1abc: e1 + e2 = requests 2, satsCost 30
240
- expect(npubs).toHaveLength(1);
241
- expect(npubs[0]!.npub).toBe("npub1abc");
242
- expect(npubs[0]!.requests).toBe(2);
243
- expect(npubs[0]!.satsCost).toBe(30);
244
- // Top models for npub1abc (clients: ["client-1"]): only model-a
245
- expect(npubs[0]!.topModels.length).toBeGreaterThan(0);
246
- expect(npubs[0]!.topModels[0]!.modelId).toBe("model-a");
247
- });
248
-
249
- it("returns recent entries (up to 50)", async () => {
250
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
251
- expect(summary.recent).toHaveLength(4); // only 4 entries in driver
252
- // Should be sorted desc by timestamp
253
- expect(summary.recent[0]!.timestamp).toBeGreaterThanOrEqual(summary.recent[1]!.timestamp);
254
- });
255
-
256
- it("returns providers correctly", async () => {
257
- const summary = await getUsageSummary(driver, CLIENTS, TZ);
258
- const { providers } = summary;
259
-
260
- // All entries use the same baseUrl
261
- expect(providers).toHaveLength(1);
262
- expect(providers[0]!.baseUrl).toBe("https://api.openai.com/");
263
- expect(providers[0]!.requests).toBe(4);
264
- });
265
-
266
- it("caches results for same key within TTL", async () => {
267
- const summary1 = await getUsageSummary(driver, CLIENTS, TZ);
268
- const summary2 = await getUsageSummary(driver, CLIENTS, TZ);
269
- // Same object reference means it was served from cache
270
- expect(summary1).toBe(summary2);
271
- });
272
- });
@@ -1,81 +0,0 @@
1
- import { describe, it, expect } from "bun:test";
2
- import type { SdkStore } from "@routstr/sdk";
3
- import { createModelService } from "./models";
4
-
5
- /**
6
- * `ensureProvidersBootstrapped` must mirror the review-disabled provider set
7
- * (kind 38425) into the SdkStore. The SDK applies review disables to the
8
- * discovery adapter during bootstrap, but `providers list` and per-model
9
- * provider views read the store — if the store is not kept in sync, a fresh
10
- * install reports "0 disabled" while routing silently excludes them.
11
- */
12
- describe("createModelService.ensureProvidersBootstrapped", () => {
13
- function makeStore(initialBaseUrls: string[] = []) {
14
- const disabledCalls: string[][] = [];
15
- const state: Record<string, unknown> = {
16
- baseUrlsList: initialBaseUrls,
17
- setBaseUrlsList: (urls: string[]) => {
18
- state.baseUrlsList = urls;
19
- },
20
- disabledProviders: [],
21
- setDisabledProviders: (urls: string[]) => {
22
- state.disabledProviders = urls;
23
- disabledCalls.push(urls);
24
- },
25
- manuallyDisabledProviders: [],
26
- manuallyEnabledProviders: [],
27
- };
28
-
29
- return {
30
- store: { getState: () => state } as unknown as SdkStore,
31
- state,
32
- disabledCalls,
33
- };
34
- }
35
-
36
- function makeModelManager(
37
- providers: string[],
38
- reviewedDisabled: string[] | null,
39
- ) {
40
- return {
41
- bootstrapProviders: async () => providers,
42
- syncReviewedProvidersFromNostr: async () => reviewedDisabled,
43
- } as never;
44
- }
45
-
46
- it("mirrors review-disabled providers into the store after bootstrap", async () => {
47
- const { store, state, disabledCalls } = makeStore();
48
- const modelManager = makeModelManager(
49
- ["https://ok.example/", "https://bad.example/"],
50
- ["https://bad.example/"],
51
- );
52
- const service = createModelService(
53
- modelManager as never,
54
- {} as never,
55
- store,
56
- );
57
-
58
- await service.ensureProvidersBootstrapped();
59
-
60
- expect(disabledCalls).toEqual([["https://bad.example/"]]);
61
- expect(state.disabledProviders).toEqual(["https://bad.example/"]);
62
- });
63
-
64
- it("leaves the store unchanged when review sync reports no change (null)", async () => {
65
- const { store, state, disabledCalls } = makeStore(["https://ok.example/"]);
66
- const modelManager = makeModelManager(
67
- ["https://ok.example/", "https://new.example/"],
68
- null,
69
- );
70
- const service = createModelService(
71
- modelManager as never,
72
- {} as never,
73
- store,
74
- );
75
-
76
- await service.ensureProvidersBootstrapped();
77
-
78
- expect(disabledCalls).toEqual([]);
79
- expect(state.disabledProviders).toEqual([]);
80
- });
81
- });