@tokenbuddy/tb-admin 1.0.15 → 1.0.27
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/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +286 -13
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +12 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +12 -8
- package/dist/src/client.js.map +1 -1
- package/dist/src/display-format.d.ts +39 -0
- package/dist/src/display-format.d.ts.map +1 -0
- package/dist/src/display-format.js +354 -0
- package/dist/src/display-format.js.map +1 -0
- package/dist/src/server-cmd.d.ts +3 -0
- package/dist/src/server-cmd.d.ts.map +1 -1
- package/dist/src/server-cmd.js +32 -9
- package/dist/src/server-cmd.js.map +1 -1
- package/dist/src/ui-actions.d.ts +2 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +123 -63
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-command.js +1 -1
- package/dist/src/ui-command.js.map +1 -1
- package/dist/src/ui-server.d.ts +0 -1
- package/dist/src/ui-server.d.ts.map +1 -1
- package/dist/src/ui-server.js +25 -9
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +7 -1
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +55 -24
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +372 -47
- package/dist/src/ui-static.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +326 -13
- package/src/client.ts +13 -8
- package/src/display-format.ts +398 -0
- package/src/server-cmd.ts +35 -9
- package/src/ui-actions.ts +129 -72
- package/src/ui-command.ts +1 -1
- package/src/ui-server.ts +24 -10
- package/src/ui-state.ts +64 -25
- package/src/ui-static.ts +375 -47
- package/tests/admin.test.ts +573 -41
package/tests/admin.test.ts
CHANGED
|
@@ -3,8 +3,27 @@ import { buildAdminCli } from "../src/cli.js";
|
|
|
3
3
|
import { FlyProvider, parseFlyMachineIds, requirePublishedDockerImage } from "../src/server-cmd.js";
|
|
4
4
|
import { startAdminUiServer } from "../src/ui-server.js";
|
|
5
5
|
import { AdminUiState } from "../src/ui-state.js";
|
|
6
|
-
import { UiActions, type UiActionResult } from "../src/ui-actions.js";
|
|
6
|
+
import { UiActions, type CreateSellerRequest, type UiActionResult } from "../src/ui-actions.js";
|
|
7
7
|
import { adminUiHtml } from "../src/ui-static.js";
|
|
8
|
+
import {
|
|
9
|
+
formatBalanceAmount,
|
|
10
|
+
formatCount,
|
|
11
|
+
formatDiscountRatio,
|
|
12
|
+
formatDuration,
|
|
13
|
+
formatMoney,
|
|
14
|
+
formatPercent,
|
|
15
|
+
formatPricePair,
|
|
16
|
+
formatSellerCapacity,
|
|
17
|
+
formatSellerStatus,
|
|
18
|
+
formatSellerId,
|
|
19
|
+
formatSpeed,
|
|
20
|
+
formatTimeCompact,
|
|
21
|
+
formatTimeFull,
|
|
22
|
+
formatTimeLedger,
|
|
23
|
+
sellerStatusTone,
|
|
24
|
+
statusTone,
|
|
25
|
+
UNKNOWN_VALUE
|
|
26
|
+
} from "../src/display-format.js";
|
|
8
27
|
import {
|
|
9
28
|
BalanceProbeCache,
|
|
10
29
|
probeUpstreamBalance
|
|
@@ -81,9 +100,30 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
81
100
|
|
|
82
101
|
expect(bootstrap).toBeDefined();
|
|
83
102
|
expect(sellers).toBeDefined();
|
|
84
|
-
expect(sellers?.commands.map((command) => command.name()).sort()).toEqual([
|
|
103
|
+
expect(sellers?.commands.map((command) => command.name()).sort()).toEqual([
|
|
104
|
+
"add",
|
|
105
|
+
"get",
|
|
106
|
+
"list",
|
|
107
|
+
"put",
|
|
108
|
+
"remove",
|
|
109
|
+
"status",
|
|
110
|
+
"update",
|
|
111
|
+
"validate"
|
|
112
|
+
]);
|
|
85
113
|
expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
|
|
114
|
+
expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--force")).toBe(true);
|
|
115
|
+
expect(sellers?.commands.find((command) => command.name() === "add")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
116
|
+
expect(sellers?.commands.find((command) => command.name() === "update")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
117
|
+
expect(sellers?.commands.find((command) => command.name() === "status")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
118
|
+
expect(sellers?.commands.find((command) => command.name() === "remove")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
86
119
|
expect(sellers?.commands.find((command) => command.name() === "validate")?.options.some((option) => option.long === "--file")).toBe(true);
|
|
120
|
+
const defaultSeller = bootstrap?.commands.find((command) => command.name() === "default-seller");
|
|
121
|
+
const registry = bootstrap?.commands.find((command) => command.name() === "registry");
|
|
122
|
+
expect(defaultSeller?.commands.map((command) => command.name()).sort()).toEqual(["set"]);
|
|
123
|
+
expect(defaultSeller?.commands.find((command) => command.name() === "set")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
124
|
+
expect(registry?.commands.map((command) => command.name()).sort()).toEqual(["diff", "import", "publish", "versions"]);
|
|
125
|
+
expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--dry-run")).toBe(true);
|
|
126
|
+
expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--force")).toBe(true);
|
|
87
127
|
expect(sellerConfig).toBeDefined();
|
|
88
128
|
expect(sellerConfig?.commands.map((command) => command.name()).sort()).toEqual(["get", "put", "validate"]);
|
|
89
129
|
expect(sellerConfig?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
|
|
@@ -170,6 +210,32 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
170
210
|
expect(commands).toEqual([]);
|
|
171
211
|
});
|
|
172
212
|
|
|
213
|
+
test("FlyProvider passes configured Fly token to flyctl commands", () => {
|
|
214
|
+
const envs: Array<string | undefined> = [];
|
|
215
|
+
const provider = new FlyProvider({ token: "fly-token-value" }, {
|
|
216
|
+
checkFlyctlInstalled: () => true,
|
|
217
|
+
imageInspector: () => ({ ok: true }),
|
|
218
|
+
execSync: (_command, options) => {
|
|
219
|
+
envs.push(options?.env?.FLY_API_TOKEN);
|
|
220
|
+
return "";
|
|
221
|
+
},
|
|
222
|
+
spawnSync: (_command, _args, options) => {
|
|
223
|
+
envs.push(options?.env?.FLY_API_TOKEN);
|
|
224
|
+
return { status: 0, signal: null, stdout: "", stderr: "", pid: 1, output: [] };
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
provider.createSeller({
|
|
229
|
+
name: "tbs-test",
|
|
230
|
+
app: "tbs-test",
|
|
231
|
+
image: "registry.fly.io/tb-seller:latest",
|
|
232
|
+
flyConfig: "deploy/fly.io/fly.tb-seller.toml",
|
|
233
|
+
operatorSecret: "operator-secret"
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(envs).toEqual(["fly-token-value", "fly-token-value", "fly-token-value"]);
|
|
237
|
+
});
|
|
238
|
+
|
|
173
239
|
test("parseFlyMachineIds reads machine ids and rejects unusable machine lists", () => {
|
|
174
240
|
expect(parseFlyMachineIds(JSON.stringify([
|
|
175
241
|
{ id: "machine-1" },
|
|
@@ -200,7 +266,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
200
266
|
expect(() => validateRegistryDocument(badDefault)).toThrow("defaultSeller `missing`");
|
|
201
267
|
});
|
|
202
268
|
|
|
203
|
-
test("tb-admin ui server binds loopback,
|
|
269
|
+
test("tb-admin ui server binds loopback, rejects cross-origin APIs, and serves bootstrap data", async () => {
|
|
204
270
|
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
205
271
|
const started = await startAdminUiServer({
|
|
206
272
|
host: "127.0.0.1",
|
|
@@ -222,10 +288,14 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
222
288
|
})
|
|
223
289
|
});
|
|
224
290
|
try {
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
headers: {
|
|
291
|
+
expect(started.url).not.toContain("session=");
|
|
292
|
+
const blocked = await fetch(`${started.url}api/bootstrap`, {
|
|
293
|
+
headers: { Origin: "http://malicious.example" }
|
|
228
294
|
});
|
|
295
|
+
expect(blocked.status).toBe(403);
|
|
296
|
+
await expect(blocked.json()).resolves.toMatchObject({ error: "invalid UI origin" });
|
|
297
|
+
|
|
298
|
+
const response = await fetch(`${started.url}api/bootstrap`);
|
|
229
299
|
await expect(response.json()).resolves.toMatchObject({
|
|
230
300
|
status: "available",
|
|
231
301
|
registryVersion: 7,
|
|
@@ -256,22 +326,19 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
256
326
|
url: "https://bootstrap.example.test",
|
|
257
327
|
fetchJson: async (url) => createSellerFetchJson(url),
|
|
258
328
|
commandRunner: async (args): Promise<UiActionResult> => {
|
|
259
|
-
if (
|
|
329
|
+
if (isBootstrapSellersAdd(args)) {
|
|
260
330
|
const filePath = args[args.indexOf("--file") + 1];
|
|
261
|
-
const
|
|
262
|
-
expect(
|
|
331
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
|
|
332
|
+
expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
|
|
263
333
|
}
|
|
264
334
|
calls.push(args);
|
|
265
335
|
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
266
336
|
}
|
|
267
337
|
});
|
|
268
338
|
try {
|
|
269
|
-
const response = await fetch(`${started.url
|
|
339
|
+
const response = await fetch(`${started.url}api/sellers`, {
|
|
270
340
|
method: "POST",
|
|
271
|
-
headers: {
|
|
272
|
-
"Content-Type": "application/json",
|
|
273
|
-
"X-TokenBuddy-Ui-Session": started.sessionToken
|
|
274
|
-
},
|
|
341
|
+
headers: { "Content-Type": "application/json" },
|
|
275
342
|
body: JSON.stringify({
|
|
276
343
|
sellerName: "tbs-nrt-07",
|
|
277
344
|
app: "tbs-nrt-07",
|
|
@@ -305,9 +372,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
305
372
|
|
|
306
373
|
let job: any;
|
|
307
374
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
308
|
-
const poll = await fetch(`${started.url
|
|
309
|
-
headers: { "X-TokenBuddy-Ui-Session": started.sessionToken }
|
|
310
|
-
});
|
|
375
|
+
const poll = await fetch(`${started.url}api/jobs/${created.jobId}`);
|
|
311
376
|
job = await poll.json();
|
|
312
377
|
if (job.status !== "running") {
|
|
313
378
|
break;
|
|
@@ -336,9 +401,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
336
401
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value status"),
|
|
337
402
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value seller-config put --file"),
|
|
338
403
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value upstreams refresh --auto-models"),
|
|
339
|
-
expect.stringContaining("bootstrap sellers
|
|
340
|
-
expect.stringContaining("bootstrap sellers put --file")
|
|
404
|
+
expect.stringContaining("bootstrap sellers add --file"),
|
|
341
405
|
]);
|
|
406
|
+
expect(commandLines[5]).toContain("--expect-version 7");
|
|
342
407
|
expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
|
|
343
408
|
} finally {
|
|
344
409
|
await new Promise<void>((resolve) => started.server.close(() => resolve()));
|
|
@@ -373,7 +438,8 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
373
438
|
lastInferenceMs: 700,
|
|
374
439
|
latencySamples: 2,
|
|
375
440
|
upstreamStatus: "healthy",
|
|
376
|
-
|
|
441
|
+
upstreamBalanceUsdMicros: 47_950_000,
|
|
442
|
+
upstreamBalanceCurrency: "USD",
|
|
377
443
|
upstreamBalanceSource: "openrouter"
|
|
378
444
|
});
|
|
379
445
|
|
|
@@ -394,6 +460,130 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
394
460
|
}
|
|
395
461
|
});
|
|
396
462
|
|
|
463
|
+
test("AdminUiState uses Fly provider operator secret for registry sellers without local profiles", async () => {
|
|
464
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
465
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
466
|
+
const state = new AdminUiState({
|
|
467
|
+
configManager: mgr,
|
|
468
|
+
url: "https://bootstrap.example.test",
|
|
469
|
+
fetchJson: async (url, init) => {
|
|
470
|
+
const pathName = new URL(url).pathname;
|
|
471
|
+
if (pathName === "/registry/sellers") {
|
|
472
|
+
return {
|
|
473
|
+
version: 9,
|
|
474
|
+
sellers: [{
|
|
475
|
+
id: "tbs-openrouter-ai-qecae",
|
|
476
|
+
name: "tbs-openrouter-ai-qecae",
|
|
477
|
+
app: "tbs-openrouter-ai-qecae",
|
|
478
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
479
|
+
status: "active",
|
|
480
|
+
region: "sin",
|
|
481
|
+
modelsCount: 2,
|
|
482
|
+
supportedProtocols: ["chat_completions"],
|
|
483
|
+
paymentMethods: ["clawtip"]
|
|
484
|
+
}]
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
|
|
488
|
+
if (pathName === "/operator/status") {
|
|
489
|
+
return { status: "healthy", upstream: { status: "healthy" }, capacity: { activeConnections: 1, maxConnections: 8 } };
|
|
490
|
+
}
|
|
491
|
+
if (pathName === "/operator/admin/service") {
|
|
492
|
+
return { sellerId: "node-seller-core", modelsCount: 2, capacity: { maxConnections: 8, maxQueueDepth: 4 } };
|
|
493
|
+
}
|
|
494
|
+
if (pathName === "/operator/admin/upstreams") {
|
|
495
|
+
return {
|
|
496
|
+
upstreams: [{
|
|
497
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
498
|
+
upstreamApiKey: "****abcd",
|
|
499
|
+
models: [{ id: "openai/gpt-5.4" }, { id: "anthropic/claude-opus-4.7" }]
|
|
500
|
+
}]
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
if (pathName === "/operator/admin/config") {
|
|
504
|
+
return {
|
|
505
|
+
config: {
|
|
506
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
507
|
+
upstreamApiKey: "live-secret-abcd",
|
|
508
|
+
upstreamBalanceProbe: { template: "none" },
|
|
509
|
+
markupRatio: 1.2,
|
|
510
|
+
discountRatio: 1,
|
|
511
|
+
maxConnections: 8,
|
|
512
|
+
maxQueueDepth: 4
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
throw new Error(`unexpected fetch url ${url}`);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const sellers = await state.sellers();
|
|
521
|
+
expect(sellers[0]).toMatchObject({
|
|
522
|
+
id: "tbs-openrouter-ai-qecae",
|
|
523
|
+
name: "tbs-openrouter-ai-qecae",
|
|
524
|
+
nodeStatus: "active",
|
|
525
|
+
upstreamDomain: "openrouter.ai",
|
|
526
|
+
profile: "tbs-openrouter-ai-qecae",
|
|
527
|
+
modelsCount: 2
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const detail = await state.sellerDetail("tbs-openrouter-ai-qecae");
|
|
531
|
+
expect(detail.configuration.upstreamUrl).toBe("https://openrouter.ai/api");
|
|
532
|
+
expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** abcd");
|
|
533
|
+
expect(detail.models.map((model) => model.upstreamModel)).toEqual(["openai/gpt-5.4", "anthropic/claude-opus-4.7"]);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("UiActions updates new registry seller config without a local profile", async () => {
|
|
537
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
538
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
539
|
+
const calls: string[][] = [];
|
|
540
|
+
const actions = new UiActions({
|
|
541
|
+
configManager: mgr,
|
|
542
|
+
url: "https://bootstrap.example.test",
|
|
543
|
+
fetchJson: async (url, init) => {
|
|
544
|
+
const pathName = new URL(url).pathname;
|
|
545
|
+
if (pathName === "/registry/sellers") {
|
|
546
|
+
return {
|
|
547
|
+
version: 10,
|
|
548
|
+
sellers: [{
|
|
549
|
+
id: "tbs-openrouter-ai-qecae",
|
|
550
|
+
name: "tbs-openrouter-ai-qecae",
|
|
551
|
+
app: "tbs-openrouter-ai-qecae",
|
|
552
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
553
|
+
status: "active",
|
|
554
|
+
supportedProtocols: ["chat_completions"],
|
|
555
|
+
paymentMethods: ["clawtip"]
|
|
556
|
+
}]
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
|
|
560
|
+
if (pathName === "/operator/admin/config") {
|
|
561
|
+
return {
|
|
562
|
+
config: {
|
|
563
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
564
|
+
upstreamApiKey: "live-secret-abcd",
|
|
565
|
+
maxConnections: 8,
|
|
566
|
+
maxQueueDepth: 4
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
throw new Error(`unexpected fetch url ${url}`);
|
|
571
|
+
},
|
|
572
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
573
|
+
calls.push(args);
|
|
574
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const result = await actions.updateSellerConfig("tbs-openrouter-ai-qecae", { maxConnections: 12 });
|
|
579
|
+
|
|
580
|
+
expect(result.ok).toBe(true);
|
|
581
|
+
expect(calls.map((args) => args.join(" "))).toEqual([
|
|
582
|
+
expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config validate --file"),
|
|
583
|
+
expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config put --file")
|
|
584
|
+
]);
|
|
585
|
+
});
|
|
586
|
+
|
|
397
587
|
test("admin UI seller rows render missing telemetry without unknown data labels", () => {
|
|
398
588
|
const html = adminUiHtml();
|
|
399
589
|
expect(html).toContain("class=\"spinner\"");
|
|
@@ -407,6 +597,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
407
597
|
expect(html).toContain("currentCreateJob");
|
|
408
598
|
expect(html).toContain("setCreateFormDisabled(true)");
|
|
409
599
|
expect(html).toContain("setCreateFormDisabled(false)");
|
|
600
|
+
expect(html).toContain("Retry create");
|
|
410
601
|
expect(html).toContain("data-progress-step");
|
|
411
602
|
expect(html).toContain("aria-expanded");
|
|
412
603
|
expect(html).toContain("progress-title");
|
|
@@ -414,25 +605,26 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
414
605
|
expect(html).toContain("Hide details");
|
|
415
606
|
expect(html).toContain("/api/jobs/");
|
|
416
607
|
expect(html).toContain("function uiErrorMessage");
|
|
417
|
-
expect(html).toContain("Admin UI connection lost.
|
|
418
|
-
expect(html).toContain("Admin
|
|
608
|
+
expect(html).toContain("Admin UI connection lost. Restart tb-admin ui and reload this page.");
|
|
609
|
+
expect(html).toContain("Admin profile authentication failed. Check the configured operator token.");
|
|
419
610
|
expect(html).toContain("renderCreateJob");
|
|
420
611
|
expect(html).toContain("pollCreateJob");
|
|
421
|
-
expect(html).toContain("Created and
|
|
612
|
+
expect(html).toContain("Created and added to bootstrap registry.");
|
|
422
613
|
expect(html).toContain("upstreamUrl:\"https://openrouter.ai/api/v1\"");
|
|
423
614
|
expect(html).toContain("loadingSpinner(\"Loading sellers\")");
|
|
424
615
|
expect(html).toContain("loadingSpinner(\"Loading configuration\")");
|
|
425
616
|
expect(html).toContain("loadingSpinner(\"Loading models\")");
|
|
426
617
|
expect(html).toContain("sellerRefreshIntervalMs = 30000");
|
|
427
|
-
expect(html).toContain("Last updated: never");
|
|
428
618
|
expect(html).toContain("function scheduleSellerRefresh()");
|
|
429
619
|
expect(html).toContain("sellerNextRefreshAt = new Date(Date.now() + sellerRefreshIntervalMs)");
|
|
430
620
|
expect(html).toContain("sellerRefreshTimer = setTimeout(() => loadSellers(), sellerRefreshIntervalMs)");
|
|
431
621
|
expect(html).toContain("Next refresh: ");
|
|
432
|
-
expect(html).toContain("
|
|
622
|
+
expect(html).not.toContain("sellerLastUpdated");
|
|
623
|
+
expect(html).not.toContain("Last updated:");
|
|
624
|
+
expect(html).not.toContain("s ago");
|
|
433
625
|
expect(html).toContain("sellerClockTimer = setInterval(() => updateSellerRefreshMeta(sellerRefreshInFlight), 1000)");
|
|
434
626
|
expect(html).toContain("function renderSellerRows(rows)");
|
|
435
|
-
expect(html).toContain("
|
|
627
|
+
expect(html).toContain("sellerRefreshLoaded = true");
|
|
436
628
|
expect(html).toContain("readonly-value");
|
|
437
629
|
expect(html).toContain("--seller-grid");
|
|
438
630
|
expect(html).toContain("grid-template-columns:var(--seller-grid)");
|
|
@@ -440,10 +632,25 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
440
632
|
expect(html).toContain("gap:12px;width:100%;min-width:0");
|
|
441
633
|
expect(html).toContain("detailFieldsHtml");
|
|
442
634
|
expect(html).toContain("data-original");
|
|
443
|
-
|
|
444
|
-
expect(html).toContain("
|
|
445
|
-
expect(html).toContain("
|
|
635
|
+
// Design-spec header labels (TTFT not Latency, Disc not Discount)
|
|
636
|
+
expect(html).toContain(">TTFT</span>");
|
|
637
|
+
expect(html).toContain(">Disc</span>");
|
|
638
|
+
expect(html).not.toContain(">Latency</span>");
|
|
639
|
+
// Spec-compliant token formats
|
|
640
|
+
expect(html).toContain("tok/s");
|
|
641
|
+
expect(html).toContain("AVG speed");
|
|
446
642
|
expect(html).toContain("Samples");
|
|
643
|
+
// Spec-compliant unknown-value char (em dash) lives in formatter
|
|
644
|
+
expect(html).toContain("UNKNOWN_VALUE");
|
|
645
|
+
expect(html).toContain("formatDuration");
|
|
646
|
+
expect(html).toContain("formatSellerStatus");
|
|
647
|
+
expect(html).toContain("formatDiscountRatio");
|
|
648
|
+
expect(html).toContain("formatBalanceAmount");
|
|
649
|
+
// No glassmorphism per spec
|
|
650
|
+
expect(html).not.toContain("backdrop-filter");
|
|
651
|
+
// Detail field references
|
|
652
|
+
expect(html).toContain("avgTokensPerSecond");
|
|
653
|
+
expect(html).toContain("lastTokensPerSecond");
|
|
447
654
|
expect(html).toContain("lastInferenceMs");
|
|
448
655
|
expect(html).toContain("upstreamBalanceSource");
|
|
449
656
|
expect(html).toContain("upstreamBalanceFetchedAt");
|
|
@@ -452,6 +659,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
452
659
|
expect(html).toContain("upstreamBalanceProbeUrl");
|
|
453
660
|
expect(html).toContain("upstreamBalanceProbeUserId");
|
|
454
661
|
expect(html).toContain("upstreamBalanceProbeRechargeUrl");
|
|
662
|
+
// Payment tabs
|
|
455
663
|
expect(html).toContain("paymentMethods");
|
|
456
664
|
expect(html).toContain("payment-tabs");
|
|
457
665
|
expect(html).toContain("payment-tab");
|
|
@@ -489,7 +697,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
489
697
|
expect(html).toContain("clawtipResourceUrl");
|
|
490
698
|
expect(html).toContain("clawtipActivationFeeFen");
|
|
491
699
|
expect(html).toContain("clawtipMicrosPerFen");
|
|
492
|
-
|
|
700
|
+
// Spec uses — (em dash) for unknown, not "not reported" or "n/a"
|
|
701
|
+
expect(html).not.toContain("not reported");
|
|
702
|
+
expect(html).not.toContain("\"n/a\"");
|
|
493
703
|
expect(html).toContain("aria-label=\"Seller specs\"");
|
|
494
704
|
expect(html).toContain("<svg viewBox=\"0 0 24 24\"");
|
|
495
705
|
expect(html).not.toContain(">i</span>");
|
|
@@ -500,7 +710,6 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
500
710
|
expect(html).not.toContain("Loading models...");
|
|
501
711
|
expect(html).not.toContain("<div id=\"createStatus\" class=\"status-line\">Ready</div>");
|
|
502
712
|
expect(html).not.toContain("not set");
|
|
503
|
-
expect(html).not.toContain("tok/s");
|
|
504
713
|
expect(html).not.toContain(" multiple");
|
|
505
714
|
});
|
|
506
715
|
|
|
@@ -729,10 +938,10 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
729
938
|
profile: "bootstrap",
|
|
730
939
|
fetchJson: async (url) => createSellerFetchJson(url),
|
|
731
940
|
commandRunner: async (args): Promise<UiActionResult> => {
|
|
732
|
-
if (
|
|
941
|
+
if (isBootstrapSellersAdd(args)) {
|
|
733
942
|
const filePath = args[args.indexOf("--file") + 1];
|
|
734
|
-
const
|
|
735
|
-
expect(
|
|
943
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
|
|
944
|
+
expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
|
|
736
945
|
}
|
|
737
946
|
calls.push(args);
|
|
738
947
|
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
@@ -772,6 +981,8 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
772
981
|
expect(response.registryPublish?.ok).toBe(true);
|
|
773
982
|
expect(response.publishRegistry).toBe("completed");
|
|
774
983
|
expect(response.configPreview.upstreamUrl).toBe("https://openrouter.ai/api");
|
|
984
|
+
expect(response.configPreview.sellerId).toBe("tbs-nrt-07");
|
|
985
|
+
expect(response.configPreview.manifestVersion).toBe("manifest.v1");
|
|
775
986
|
expect(response.configPreview.upstreamBalanceProbe).toEqual({
|
|
776
987
|
template: "usage_generic",
|
|
777
988
|
url: "https://code.shoestravel.xin/v1/usage",
|
|
@@ -798,13 +1009,153 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
798
1009
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret status"),
|
|
799
1010
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret seller-config put --file"),
|
|
800
1011
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret upstreams refresh --auto-models"),
|
|
801
|
-
|
|
802
|
-
expect.stringContaining("bootstrap sellers put --file")
|
|
1012
|
+
expect.stringContaining("bootstrap sellers add --file")
|
|
803
1013
|
]);
|
|
1014
|
+
expect(commandLines[5]).toContain("--expect-version 7");
|
|
804
1015
|
expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
|
|
1016
|
+
expect(mgr.getProfile("tbs-nrt-07")).toEqual({
|
|
1017
|
+
url: "https://tbs-nrt-07.fly.dev",
|
|
1018
|
+
token: "operator-secret"
|
|
1019
|
+
});
|
|
1020
|
+
} finally {
|
|
1021
|
+
await fixture.close();
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test("UiActions create retries transient upstream model refresh failures", async () => {
|
|
1026
|
+
const fixture = await startFixtureAdminServer();
|
|
1027
|
+
try {
|
|
1028
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
1029
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
1030
|
+
const registry = await createSellerFetchJson(`${fixture.baseUrl}/registry/sellers`);
|
|
1031
|
+
let refreshAttempts = 0;
|
|
1032
|
+
const calls: string[][] = [];
|
|
1033
|
+
const actions = new UiActions({
|
|
1034
|
+
configManager: mgr,
|
|
1035
|
+
url: fixture.baseUrl,
|
|
1036
|
+
fetchJson: async (url) => {
|
|
1037
|
+
const pathName = new URL(url).pathname;
|
|
1038
|
+
if (pathName === "/registry/sellers") {
|
|
1039
|
+
return registry;
|
|
1040
|
+
}
|
|
1041
|
+
return createSellerFetchJson(url);
|
|
1042
|
+
},
|
|
1043
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
1044
|
+
calls.push(args);
|
|
1045
|
+
if (args.includes("upstreams") && args.includes("refresh")) {
|
|
1046
|
+
refreshAttempts += 1;
|
|
1047
|
+
if (refreshAttempts < 3) {
|
|
1048
|
+
return { ok: false, stdout: "", stderr: "Error: Connection failed: fetch failed", command: ["node", "tb-admin", ...args] };
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
|
|
1052
|
+
const filePath = args[args.indexOf("--file") + 1];
|
|
1053
|
+
const doc = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1054
|
+
expect(doc).toMatchObject({ id: "tbs-nrt-07" });
|
|
1055
|
+
expect(doc.models).toHaveLength(2);
|
|
1056
|
+
}
|
|
1057
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
const response = await actions.createSeller({
|
|
1062
|
+
sellerName: "tbs-nrt-07",
|
|
1063
|
+
app: "tbs-nrt-07",
|
|
1064
|
+
region: "nrt",
|
|
1065
|
+
image: "registry.fly.io/tb-seller:latest",
|
|
1066
|
+
upstreamWebsite: "https://openrouter.ai",
|
|
1067
|
+
upstreamUrl: "https://openrouter.ai/api/v1",
|
|
1068
|
+
upstreamApiKey: "fixture-upstream-key",
|
|
1069
|
+
upstreamBalanceProbeTemplate: "none",
|
|
1070
|
+
maxConnections: 8,
|
|
1071
|
+
maxQueueDepth: 4,
|
|
1072
|
+
markupRatio: 1.2,
|
|
1073
|
+
discountRatio: 1,
|
|
1074
|
+
paymentMethods: ["mock"],
|
|
1075
|
+
flyConfig: "deploy/fly.io/fly.tb-seller.toml"
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
expect(response.modelsRefresh?.ok).toBe(true);
|
|
1079
|
+
expect(response.registryPublish?.ok).toBe(true);
|
|
1080
|
+
expect(refreshAttempts).toBe(3);
|
|
1081
|
+
expect(calls.filter((args) => args.includes("upstreams") && args.includes("refresh"))).toHaveLength(3);
|
|
805
1082
|
} finally {
|
|
806
1083
|
await fixture.close();
|
|
807
1084
|
}
|
|
1085
|
+
}, 30000);
|
|
1086
|
+
|
|
1087
|
+
test("UiActions create publishes registry from wrapped upstream metadata", async () => {
|
|
1088
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
1089
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
1090
|
+
let published: any;
|
|
1091
|
+
const actions = new UiActions({
|
|
1092
|
+
configManager: mgr,
|
|
1093
|
+
url: "https://bootstrap.example.test",
|
|
1094
|
+
fetchJson: async (url) => createSellerFetchJson(url, "wrappedArray"),
|
|
1095
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
1096
|
+
if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
|
|
1097
|
+
const filePath = args[args.indexOf("--file") + 1];
|
|
1098
|
+
published = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1099
|
+
}
|
|
1100
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
const response = await actions.createSeller(createSellerRequestFixture({
|
|
1105
|
+
sellerName: "openrouter-ai",
|
|
1106
|
+
app: "tbs-openrouter-ai-qecae"
|
|
1107
|
+
}));
|
|
1108
|
+
|
|
1109
|
+
expect(response.registryPublish?.ok).toBe(true);
|
|
1110
|
+
expect(published).toMatchObject({
|
|
1111
|
+
id: "tbs-openrouter-ai-qecae",
|
|
1112
|
+
name: "tbs-openrouter-ai-qecae",
|
|
1113
|
+
app: "tbs-openrouter-ai-qecae",
|
|
1114
|
+
status: "active",
|
|
1115
|
+
models: ["openai/gpt-5.4", "openai/gpt-5.4-mini"],
|
|
1116
|
+
supportedProtocols: ["chat_completions"]
|
|
1117
|
+
});
|
|
1118
|
+
expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
|
|
1119
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
1120
|
+
token: "operator-secret"
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("UiActions create falls back to public manifest models for registry publish", async () => {
|
|
1125
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
1126
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
1127
|
+
let published: any;
|
|
1128
|
+
const actions = new UiActions({
|
|
1129
|
+
configManager: mgr,
|
|
1130
|
+
url: "https://bootstrap.example.test",
|
|
1131
|
+
fetchJson: async (url) => createSellerFetchJson(url, "manifest"),
|
|
1132
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
1133
|
+
if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
|
|
1134
|
+
const filePath = args[args.indexOf("--file") + 1];
|
|
1135
|
+
published = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1136
|
+
}
|
|
1137
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
1138
|
+
}
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const response = await actions.createSeller(createSellerRequestFixture({
|
|
1142
|
+
sellerName: "openrouter-ai",
|
|
1143
|
+
app: "tbs-openrouter-ai-qecae"
|
|
1144
|
+
}));
|
|
1145
|
+
|
|
1146
|
+
expect(response.registryPublish?.ok).toBe(true);
|
|
1147
|
+
expect(published).toMatchObject({
|
|
1148
|
+
id: "tbs-openrouter-ai-qecae",
|
|
1149
|
+
name: "tbs-openrouter-ai-qecae",
|
|
1150
|
+
app: "tbs-openrouter-ai-qecae",
|
|
1151
|
+
status: "active",
|
|
1152
|
+
models: ["anthropic/claude-opus-4.7"],
|
|
1153
|
+
supportedProtocols: ["chat_completions"]
|
|
1154
|
+
});
|
|
1155
|
+
expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
|
|
1156
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
1157
|
+
token: "operator-secret"
|
|
1158
|
+
});
|
|
808
1159
|
});
|
|
809
1160
|
|
|
810
1161
|
test("UiActions create supports mock-only payment without ClawTip parameters", async () => {
|
|
@@ -879,12 +1230,13 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
879
1230
|
|
|
880
1231
|
test("UiActions subprocess runner uses the tb-admin bin entrypoint", async () => {
|
|
881
1232
|
const previousArgv = process.argv[1];
|
|
882
|
-
process.argv[1] = path.resolve(
|
|
1233
|
+
process.argv[1] = path.resolve(process.env.HOME || "/tmp", "packages/admin-cli/bin/tb-admin.js");
|
|
883
1234
|
const { runTbAdmin } = await import("../src/ui-actions.js");
|
|
884
1235
|
try {
|
|
885
1236
|
const result = await runTbAdmin(["--help"], 30000);
|
|
886
1237
|
expect(result.ok).toBe(true);
|
|
887
1238
|
expect(result.command[1]).toMatch(/packages\/admin-cli\/bin\/tb-admin\.js$/);
|
|
1239
|
+
expect(fs.existsSync(result.command[1])).toBe(true);
|
|
888
1240
|
expect(result.stdout).toContain("Remote admin CLI");
|
|
889
1241
|
} finally {
|
|
890
1242
|
process.argv[1] = previousArgv;
|
|
@@ -892,6 +1244,127 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
892
1244
|
});
|
|
893
1245
|
});
|
|
894
1246
|
|
|
1247
|
+
describe("Admin CLI Display Format Spec Compliance", () => {
|
|
1248
|
+
test("formatDuration uses ms under 1s and 2-decimal seconds above 1s", () => {
|
|
1249
|
+
expect(formatDuration(undefined)).toBe(UNKNOWN_VALUE);
|
|
1250
|
+
expect(formatDuration(0)).toBe("0ms");
|
|
1251
|
+
expect(formatDuration(272)).toBe("272ms");
|
|
1252
|
+
expect(formatDuration(999)).toBe("999ms");
|
|
1253
|
+
expect(formatDuration(1000)).toBe("1.00s");
|
|
1254
|
+
expect(formatDuration(3542)).toBe("3.54s");
|
|
1255
|
+
expect(formatDuration(11540)).toBe("11.54s");
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
test("formatMoney uses 4 decimals by default and 6 for tiny ledger amounts", () => {
|
|
1259
|
+
expect(formatMoney(undefined)).toBe(UNKNOWN_VALUE);
|
|
1260
|
+
expect(formatMoney(0)).toBe("$0.0000");
|
|
1261
|
+
expect(formatMoney(138600)).toBe("$0.1386");
|
|
1262
|
+
expect(formatMoney(2_772_200)).toBe("$2.7722");
|
|
1263
|
+
expect(formatMoney(138600, { ledger: true })).toBe("$0.1386");
|
|
1264
|
+
expect(formatMoney(5782, { ledger: true })).toBe("$0.005782");
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test("formatDiscountRatio converts ratio to percentage per the design spec", () => {
|
|
1268
|
+
expect(formatDiscountRatio(undefined)).toBe(UNKNOWN_VALUE);
|
|
1269
|
+
expect(formatDiscountRatio(0.5)).toBe("50%");
|
|
1270
|
+
expect(formatDiscountRatio(0.01)).toBe("99%");
|
|
1271
|
+
expect(formatDiscountRatio(0.99)).toBe("1%");
|
|
1272
|
+
expect(formatDiscountRatio(1)).toBe("0%");
|
|
1273
|
+
expect(formatDiscountRatio(0)).toBe("100%");
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
test("formatPercent rounds to whole percent", () => {
|
|
1277
|
+
expect(formatPercent(0.123)).toBe("12%");
|
|
1278
|
+
expect(formatPercent(0.987)).toBe("99%");
|
|
1279
|
+
expect(formatPercent(undefined)).toBe(UNKNOWN_VALUE);
|
|
1280
|
+
});
|
|
1281
|
+
|
|
1282
|
+
test("formatPricePair renders $/1M with 4 decimals per spec", () => {
|
|
1283
|
+
expect(formatPricePair(undefined, undefined)).toBe(UNKNOWN_VALUE);
|
|
1284
|
+
expect(formatPricePair(2_500_000, 3_000_000)).toBe("$2.5000 / $3.0000");
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
test("formatCount uses en-US grouping", () => {
|
|
1288
|
+
expect(formatCount(undefined)).toBe(UNKNOWN_VALUE);
|
|
1289
|
+
expect(formatCount(7_529)).toBe("7,529");
|
|
1290
|
+
expect(formatCount(218_700)).toBe("218,700");
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
test("formatTimeCompact emits HH:mm for today and MM/DD HH:mm otherwise", () => {
|
|
1294
|
+
const now = new Date();
|
|
1295
|
+
const todayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 51).toISOString();
|
|
1296
|
+
const yesterdayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 9, 5).toISOString();
|
|
1297
|
+
expect(formatTimeCompact(undefined)).toBe(UNKNOWN_VALUE);
|
|
1298
|
+
expect(formatTimeCompact(todayIso)).toBe("14:51");
|
|
1299
|
+
expect(formatTimeCompact(yesterdayIso)).toMatch(/^\d{2}\/\d{2} 09:05$/);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
test("formatTimeFull uses YYYY-MM-DD HH:mm:ss for detail panels", () => {
|
|
1303
|
+
expect(formatTimeFull(undefined)).toBe(UNKNOWN_VALUE);
|
|
1304
|
+
expect(formatTimeFull("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
test("formatTimeLedger uses YYYY/MM/DD HH:mm:ss for audit tables", () => {
|
|
1308
|
+
expect(formatTimeLedger(undefined)).toBe(UNKNOWN_VALUE);
|
|
1309
|
+
expect(formatTimeLedger("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
test("formatSellerId trims long tbs ids to 10 chars", () => {
|
|
1313
|
+
expect(formatSellerId(undefined)).toBe(UNKNOWN_VALUE);
|
|
1314
|
+
expect(formatSellerId("tbs-825edb")).toBe("tbs-825edb");
|
|
1315
|
+
expect(formatSellerId("tbs-openrouter-ai-k7p9x")).toBe("tbs-openro");
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
test("formatSpeed renders to one decimal with tok/s suffix", () => {
|
|
1319
|
+
expect(formatSpeed(undefined)).toBe(UNKNOWN_VALUE);
|
|
1320
|
+
expect(formatSpeed(0)).toBe("0.0 tok/s");
|
|
1321
|
+
expect(formatSpeed(11.54)).toBe("11.5 tok/s");
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
test("formatSellerCapacity renders used / limit with en-US grouping", () => {
|
|
1325
|
+
expect(formatSellerCapacity(undefined, undefined)).toBe(UNKNOWN_VALUE);
|
|
1326
|
+
expect(formatSellerCapacity(8, 16)).toBe("8 / 16");
|
|
1327
|
+
expect(formatSellerCapacity(undefined, 16)).toBe(`${UNKNOWN_VALUE} / 16`);
|
|
1328
|
+
expect(formatSellerCapacity(8, undefined)).toBe(`8 / ${UNKNOWN_VALUE}`);
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
test("formatBalanceAmount uses 0 decimals for >=100 and 2 decimals otherwise", () => {
|
|
1332
|
+
expect(formatBalanceAmount(undefined, "USD")).toBe(UNKNOWN_VALUE);
|
|
1333
|
+
expect(formatBalanceAmount(47_950_000, "USD")).toBe("USD 47.95");
|
|
1334
|
+
expect(formatBalanceAmount(479_500_000, "USD")).toBe("USD 480");
|
|
1335
|
+
expect(formatBalanceAmount(50_000_000, "EUR")).toBe("EUR 50.00");
|
|
1336
|
+
});
|
|
1337
|
+
|
|
1338
|
+
test("formatSellerStatus maps all internal node statuses to the 7 canonical labels", () => {
|
|
1339
|
+
const canonical = new Set(["ok", "online", "configured", "pending", "degraded", "error", "unknown"]);
|
|
1340
|
+
for (const value of ["active", "healthy", "online", "configured", "pending", "draining", "degraded", "busy_capacity", "offline", "unhealthy", "error", "auth_unknown", "unknown", undefined, "weird-state"]) {
|
|
1341
|
+
expect(canonical.has(formatSellerStatus(value))).toBe(true);
|
|
1342
|
+
}
|
|
1343
|
+
expect(formatSellerStatus("active")).toBe("ok");
|
|
1344
|
+
expect(formatSellerStatus("draining")).toBe("degraded");
|
|
1345
|
+
expect(formatSellerStatus("busy_capacity")).toBe("degraded");
|
|
1346
|
+
expect(formatSellerStatus("offline")).toBe("error");
|
|
1347
|
+
expect(formatSellerStatus("auth_unknown")).toBe("unknown");
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
test("statusTone / sellerStatusTone keep green/amber/red/blue/gray buckets stable", () => {
|
|
1351
|
+
expect(statusTone("ok")).toBe("green");
|
|
1352
|
+
expect(statusTone("online")).toBe("green");
|
|
1353
|
+
expect(statusTone("configured")).toBe("green");
|
|
1354
|
+
expect(statusTone("pending")).toBe("amber");
|
|
1355
|
+
expect(statusTone("degraded")).toBe("amber");
|
|
1356
|
+
expect(statusTone("error")).toBe("red");
|
|
1357
|
+
expect(statusTone("offline")).toBe("red");
|
|
1358
|
+
expect(statusTone("unknown")).toBe("gray");
|
|
1359
|
+
expect(statusTone("running")).toBe("blue");
|
|
1360
|
+
expect(sellerStatusTone("active")).toBe("green");
|
|
1361
|
+
expect(sellerStatusTone("draining")).toBe("amber");
|
|
1362
|
+
expect(sellerStatusTone("busy_capacity")).toBe("amber");
|
|
1363
|
+
expect(sellerStatusTone("offline")).toBe("red");
|
|
1364
|
+
expect(sellerStatusTone("auth_unknown")).toBe("gray");
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
|
|
895
1368
|
async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
|
|
896
1369
|
const server = http.createServer((req, res) => {
|
|
897
1370
|
const pathName = new URL(req.url || "/", "http://127.0.0.1").pathname;
|
|
@@ -990,7 +1463,27 @@ function sendJson(res: http.ServerResponse, body: unknown): void {
|
|
|
990
1463
|
res.end(JSON.stringify(body));
|
|
991
1464
|
}
|
|
992
1465
|
|
|
993
|
-
|
|
1466
|
+
function createSellerRequestFixture(overrides: Partial<CreateSellerRequest> = {}): CreateSellerRequest {
|
|
1467
|
+
return {
|
|
1468
|
+
sellerName: "tbs-nrt-07",
|
|
1469
|
+
app: "tbs-nrt-07",
|
|
1470
|
+
region: "nrt",
|
|
1471
|
+
image: "registry.fly.io/tb-seller:latest",
|
|
1472
|
+
upstreamWebsite: "https://openrouter.ai",
|
|
1473
|
+
upstreamUrl: "https://openrouter.ai/api/v1",
|
|
1474
|
+
upstreamApiKey: "fixture-upstream-key",
|
|
1475
|
+
upstreamBalanceProbeTemplate: "none",
|
|
1476
|
+
maxConnections: 8,
|
|
1477
|
+
maxQueueDepth: 4,
|
|
1478
|
+
markupRatio: 1.2,
|
|
1479
|
+
discountRatio: 1,
|
|
1480
|
+
paymentMethods: ["mock"],
|
|
1481
|
+
flyConfig: "deploy/fly.io/fly.tb-seller.toml",
|
|
1482
|
+
...overrides
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async function createSellerFetchJson(url: string, mode: "default" | "wrapped" | "wrappedArray" | "manifest" = "default"): Promise<unknown> {
|
|
994
1487
|
const pathName = new URL(url).pathname;
|
|
995
1488
|
if (pathName === "/registry/sellers") {
|
|
996
1489
|
return {
|
|
@@ -999,6 +1492,38 @@ async function createSellerFetchJson(url: string): Promise<unknown> {
|
|
|
999
1492
|
};
|
|
1000
1493
|
}
|
|
1001
1494
|
if (pathName === "/operator/admin/upstreams") {
|
|
1495
|
+
if (mode === "wrapped") {
|
|
1496
|
+
return {
|
|
1497
|
+
upstreams: {
|
|
1498
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
1499
|
+
models: [
|
|
1500
|
+
{ id: "openai/gpt-5.4" },
|
|
1501
|
+
{ id: "openai/gpt-5.4-mini" }
|
|
1502
|
+
],
|
|
1503
|
+
supportedProtocols: ["chat_completions"]
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
if (mode === "wrappedArray") {
|
|
1508
|
+
return {
|
|
1509
|
+
upstreams: [{
|
|
1510
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
1511
|
+
models: [
|
|
1512
|
+
{ id: "openai/gpt-5.4" },
|
|
1513
|
+
{ id: "openai/gpt-5.4-mini" }
|
|
1514
|
+
],
|
|
1515
|
+
supportedProtocols: ["chat_completions"]
|
|
1516
|
+
}]
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
if (mode === "manifest") {
|
|
1520
|
+
return {
|
|
1521
|
+
upstreams: {
|
|
1522
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
1523
|
+
modelsCount: 1
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1002
1527
|
return {
|
|
1003
1528
|
upstreamUrl: "https://openrouter.ai/api",
|
|
1004
1529
|
models: [
|
|
@@ -1015,12 +1540,19 @@ async function createSellerFetchJson(url: string): Promise<unknown> {
|
|
|
1015
1540
|
supportedProtocols: ["chat_completions"]
|
|
1016
1541
|
};
|
|
1017
1542
|
}
|
|
1543
|
+
if (pathName === "/manifest" && mode === "manifest") {
|
|
1544
|
+
return {
|
|
1545
|
+
sellerId: "tbs-nrt-07",
|
|
1546
|
+
models: ["anthropic/claude-opus-4.7"],
|
|
1547
|
+
supportedProtocols: ["chat_completions"]
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1018
1550
|
throw new Error(`unexpected fetch url ${url}`);
|
|
1019
1551
|
}
|
|
1020
1552
|
|
|
1021
|
-
function
|
|
1022
|
-
const
|
|
1023
|
-
return
|
|
1553
|
+
function isBootstrapSellersAdd(args: string[]): boolean {
|
|
1554
|
+
const addIndex = args.indexOf("add");
|
|
1555
|
+
return addIndex >= 2 && args[addIndex - 2] === "bootstrap" && args[addIndex - 1] === "sellers";
|
|
1024
1556
|
}
|
|
1025
1557
|
|
|
1026
1558
|
function jsonResponse(body: unknown, status = 200): Response {
|