@tokenbuddy/tb-admin 1.0.15 → 1.0.28
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 +323 -14
- 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 +371 -46
- package/dist/src/ui-static.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +367 -14
- 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 +374 -46
- package/tests/admin.test.ts +590 -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
|
|
@@ -15,8 +34,10 @@ import {
|
|
|
15
34
|
import * as fs from "fs";
|
|
16
35
|
import * as http from "http";
|
|
17
36
|
import * as path from "path";
|
|
37
|
+
import * as vm from "vm";
|
|
18
38
|
|
|
19
39
|
const TEMP_CONF_PATH = path.resolve(__dirname, "../../data-test/admin-config.json");
|
|
40
|
+
const PACKAGE_JSON = path.resolve(__dirname, "../package.json");
|
|
20
41
|
|
|
21
42
|
describe("Admin CLI Config Profile Management Tests", () => {
|
|
22
43
|
beforeEach(() => {
|
|
@@ -54,6 +75,21 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
54
75
|
expect(defaultP?.url).toBe("http://127.0.0.1:8000");
|
|
55
76
|
});
|
|
56
77
|
|
|
78
|
+
test("tb-admin version follows package version", () => {
|
|
79
|
+
const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
|
|
80
|
+
const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
|
|
81
|
+
|
|
82
|
+
expect(program.version()).toBe(packageJson.version);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("admin UI inline script is valid JavaScript", () => {
|
|
86
|
+
const html = adminUiHtml();
|
|
87
|
+
const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((match) => match[1]);
|
|
88
|
+
|
|
89
|
+
expect(scripts.length).toBe(1);
|
|
90
|
+
expect(() => new vm.Script(scripts[0])).not.toThrow();
|
|
91
|
+
});
|
|
92
|
+
|
|
57
93
|
test("Switch default profiles", () => {
|
|
58
94
|
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
59
95
|
mgr.setProfile("prod", { url: "http://127.0.0.1:8000", token: "secret-op" });
|
|
@@ -81,9 +117,30 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
81
117
|
|
|
82
118
|
expect(bootstrap).toBeDefined();
|
|
83
119
|
expect(sellers).toBeDefined();
|
|
84
|
-
expect(sellers?.commands.map((command) => command.name()).sort()).toEqual([
|
|
120
|
+
expect(sellers?.commands.map((command) => command.name()).sort()).toEqual([
|
|
121
|
+
"add",
|
|
122
|
+
"get",
|
|
123
|
+
"list",
|
|
124
|
+
"put",
|
|
125
|
+
"remove",
|
|
126
|
+
"status",
|
|
127
|
+
"update",
|
|
128
|
+
"validate"
|
|
129
|
+
]);
|
|
85
130
|
expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
|
|
131
|
+
expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--force")).toBe(true);
|
|
132
|
+
expect(sellers?.commands.find((command) => command.name() === "add")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
133
|
+
expect(sellers?.commands.find((command) => command.name() === "update")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
134
|
+
expect(sellers?.commands.find((command) => command.name() === "status")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
135
|
+
expect(sellers?.commands.find((command) => command.name() === "remove")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
86
136
|
expect(sellers?.commands.find((command) => command.name() === "validate")?.options.some((option) => option.long === "--file")).toBe(true);
|
|
137
|
+
const defaultSeller = bootstrap?.commands.find((command) => command.name() === "default-seller");
|
|
138
|
+
const registry = bootstrap?.commands.find((command) => command.name() === "registry");
|
|
139
|
+
expect(defaultSeller?.commands.map((command) => command.name()).sort()).toEqual(["set"]);
|
|
140
|
+
expect(defaultSeller?.commands.find((command) => command.name() === "set")?.options.some((option) => option.long === "--expect-version")).toBe(true);
|
|
141
|
+
expect(registry?.commands.map((command) => command.name()).sort()).toEqual(["diff", "import", "publish", "versions"]);
|
|
142
|
+
expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--dry-run")).toBe(true);
|
|
143
|
+
expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--force")).toBe(true);
|
|
87
144
|
expect(sellerConfig).toBeDefined();
|
|
88
145
|
expect(sellerConfig?.commands.map((command) => command.name()).sort()).toEqual(["get", "put", "validate"]);
|
|
89
146
|
expect(sellerConfig?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
|
|
@@ -170,6 +227,32 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
170
227
|
expect(commands).toEqual([]);
|
|
171
228
|
});
|
|
172
229
|
|
|
230
|
+
test("FlyProvider passes configured Fly token to flyctl commands", () => {
|
|
231
|
+
const envs: Array<string | undefined> = [];
|
|
232
|
+
const provider = new FlyProvider({ token: "fly-token-value" }, {
|
|
233
|
+
checkFlyctlInstalled: () => true,
|
|
234
|
+
imageInspector: () => ({ ok: true }),
|
|
235
|
+
execSync: (_command, options) => {
|
|
236
|
+
envs.push(options?.env?.FLY_API_TOKEN);
|
|
237
|
+
return "";
|
|
238
|
+
},
|
|
239
|
+
spawnSync: (_command, _args, options) => {
|
|
240
|
+
envs.push(options?.env?.FLY_API_TOKEN);
|
|
241
|
+
return { status: 0, signal: null, stdout: "", stderr: "", pid: 1, output: [] };
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
provider.createSeller({
|
|
246
|
+
name: "tbs-test",
|
|
247
|
+
app: "tbs-test",
|
|
248
|
+
image: "registry.fly.io/tb-seller:latest",
|
|
249
|
+
flyConfig: "deploy/fly.io/fly.tb-seller.toml",
|
|
250
|
+
operatorSecret: "operator-secret"
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(envs).toEqual(["fly-token-value", "fly-token-value", "fly-token-value"]);
|
|
254
|
+
});
|
|
255
|
+
|
|
173
256
|
test("parseFlyMachineIds reads machine ids and rejects unusable machine lists", () => {
|
|
174
257
|
expect(parseFlyMachineIds(JSON.stringify([
|
|
175
258
|
{ id: "machine-1" },
|
|
@@ -200,7 +283,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
200
283
|
expect(() => validateRegistryDocument(badDefault)).toThrow("defaultSeller `missing`");
|
|
201
284
|
});
|
|
202
285
|
|
|
203
|
-
test("tb-admin ui server binds loopback,
|
|
286
|
+
test("tb-admin ui server binds loopback, rejects cross-origin APIs, and serves bootstrap data", async () => {
|
|
204
287
|
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
205
288
|
const started = await startAdminUiServer({
|
|
206
289
|
host: "127.0.0.1",
|
|
@@ -222,10 +305,14 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
222
305
|
})
|
|
223
306
|
});
|
|
224
307
|
try {
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
headers: {
|
|
308
|
+
expect(started.url).not.toContain("session=");
|
|
309
|
+
const blocked = await fetch(`${started.url}api/bootstrap`, {
|
|
310
|
+
headers: { Origin: "http://malicious.example" }
|
|
228
311
|
});
|
|
312
|
+
expect(blocked.status).toBe(403);
|
|
313
|
+
await expect(blocked.json()).resolves.toMatchObject({ error: "invalid UI origin" });
|
|
314
|
+
|
|
315
|
+
const response = await fetch(`${started.url}api/bootstrap`);
|
|
229
316
|
await expect(response.json()).resolves.toMatchObject({
|
|
230
317
|
status: "available",
|
|
231
318
|
registryVersion: 7,
|
|
@@ -256,22 +343,19 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
256
343
|
url: "https://bootstrap.example.test",
|
|
257
344
|
fetchJson: async (url) => createSellerFetchJson(url),
|
|
258
345
|
commandRunner: async (args): Promise<UiActionResult> => {
|
|
259
|
-
if (
|
|
346
|
+
if (isBootstrapSellersAdd(args)) {
|
|
260
347
|
const filePath = args[args.indexOf("--file") + 1];
|
|
261
|
-
const
|
|
262
|
-
expect(
|
|
348
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
|
|
349
|
+
expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
|
|
263
350
|
}
|
|
264
351
|
calls.push(args);
|
|
265
352
|
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
266
353
|
}
|
|
267
354
|
});
|
|
268
355
|
try {
|
|
269
|
-
const response = await fetch(`${started.url
|
|
356
|
+
const response = await fetch(`${started.url}api/sellers`, {
|
|
270
357
|
method: "POST",
|
|
271
|
-
headers: {
|
|
272
|
-
"Content-Type": "application/json",
|
|
273
|
-
"X-TokenBuddy-Ui-Session": started.sessionToken
|
|
274
|
-
},
|
|
358
|
+
headers: { "Content-Type": "application/json" },
|
|
275
359
|
body: JSON.stringify({
|
|
276
360
|
sellerName: "tbs-nrt-07",
|
|
277
361
|
app: "tbs-nrt-07",
|
|
@@ -305,9 +389,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
305
389
|
|
|
306
390
|
let job: any;
|
|
307
391
|
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
308
|
-
const poll = await fetch(`${started.url
|
|
309
|
-
headers: { "X-TokenBuddy-Ui-Session": started.sessionToken }
|
|
310
|
-
});
|
|
392
|
+
const poll = await fetch(`${started.url}api/jobs/${created.jobId}`);
|
|
311
393
|
job = await poll.json();
|
|
312
394
|
if (job.status !== "running") {
|
|
313
395
|
break;
|
|
@@ -336,9 +418,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
336
418
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value status"),
|
|
337
419
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value seller-config put --file"),
|
|
338
420
|
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")
|
|
421
|
+
expect.stringContaining("bootstrap sellers add --file"),
|
|
341
422
|
]);
|
|
423
|
+
expect(commandLines[5]).toContain("--expect-version 7");
|
|
342
424
|
expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
|
|
343
425
|
} finally {
|
|
344
426
|
await new Promise<void>((resolve) => started.server.close(() => resolve()));
|
|
@@ -373,7 +455,8 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
373
455
|
lastInferenceMs: 700,
|
|
374
456
|
latencySamples: 2,
|
|
375
457
|
upstreamStatus: "healthy",
|
|
376
|
-
|
|
458
|
+
upstreamBalanceUsdMicros: 47_950_000,
|
|
459
|
+
upstreamBalanceCurrency: "USD",
|
|
377
460
|
upstreamBalanceSource: "openrouter"
|
|
378
461
|
});
|
|
379
462
|
|
|
@@ -394,6 +477,130 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
394
477
|
}
|
|
395
478
|
});
|
|
396
479
|
|
|
480
|
+
test("AdminUiState uses Fly provider operator secret for registry sellers without local profiles", async () => {
|
|
481
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
482
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
483
|
+
const state = new AdminUiState({
|
|
484
|
+
configManager: mgr,
|
|
485
|
+
url: "https://bootstrap.example.test",
|
|
486
|
+
fetchJson: async (url, init) => {
|
|
487
|
+
const pathName = new URL(url).pathname;
|
|
488
|
+
if (pathName === "/registry/sellers") {
|
|
489
|
+
return {
|
|
490
|
+
version: 9,
|
|
491
|
+
sellers: [{
|
|
492
|
+
id: "tbs-openrouter-ai-qecae",
|
|
493
|
+
name: "tbs-openrouter-ai-qecae",
|
|
494
|
+
app: "tbs-openrouter-ai-qecae",
|
|
495
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
496
|
+
status: "active",
|
|
497
|
+
region: "sin",
|
|
498
|
+
modelsCount: 2,
|
|
499
|
+
supportedProtocols: ["chat_completions"],
|
|
500
|
+
paymentMethods: ["clawtip"]
|
|
501
|
+
}]
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
|
|
505
|
+
if (pathName === "/operator/status") {
|
|
506
|
+
return { status: "healthy", upstream: { status: "healthy" }, capacity: { activeConnections: 1, maxConnections: 8 } };
|
|
507
|
+
}
|
|
508
|
+
if (pathName === "/operator/admin/service") {
|
|
509
|
+
return { sellerId: "node-seller-core", modelsCount: 2, capacity: { maxConnections: 8, maxQueueDepth: 4 } };
|
|
510
|
+
}
|
|
511
|
+
if (pathName === "/operator/admin/upstreams") {
|
|
512
|
+
return {
|
|
513
|
+
upstreams: [{
|
|
514
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
515
|
+
upstreamApiKey: "****abcd",
|
|
516
|
+
models: [{ id: "openai/gpt-5.4" }, { id: "anthropic/claude-opus-4.7" }]
|
|
517
|
+
}]
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (pathName === "/operator/admin/config") {
|
|
521
|
+
return {
|
|
522
|
+
config: {
|
|
523
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
524
|
+
upstreamApiKey: "live-secret-abcd",
|
|
525
|
+
upstreamBalanceProbe: { template: "none" },
|
|
526
|
+
markupRatio: 1.2,
|
|
527
|
+
discountRatio: 1,
|
|
528
|
+
maxConnections: 8,
|
|
529
|
+
maxQueueDepth: 4
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
throw new Error(`unexpected fetch url ${url}`);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const sellers = await state.sellers();
|
|
538
|
+
expect(sellers[0]).toMatchObject({
|
|
539
|
+
id: "tbs-openrouter-ai-qecae",
|
|
540
|
+
name: "tbs-openrouter-ai-qecae",
|
|
541
|
+
nodeStatus: "active",
|
|
542
|
+
upstreamDomain: "openrouter.ai",
|
|
543
|
+
profile: "tbs-openrouter-ai-qecae",
|
|
544
|
+
modelsCount: 2
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const detail = await state.sellerDetail("tbs-openrouter-ai-qecae");
|
|
548
|
+
expect(detail.configuration.upstreamUrl).toBe("https://openrouter.ai/api");
|
|
549
|
+
expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** abcd");
|
|
550
|
+
expect(detail.models.map((model) => model.upstreamModel)).toEqual(["openai/gpt-5.4", "anthropic/claude-opus-4.7"]);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("UiActions updates new registry seller config without a local profile", async () => {
|
|
554
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
555
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
556
|
+
const calls: string[][] = [];
|
|
557
|
+
const actions = new UiActions({
|
|
558
|
+
configManager: mgr,
|
|
559
|
+
url: "https://bootstrap.example.test",
|
|
560
|
+
fetchJson: async (url, init) => {
|
|
561
|
+
const pathName = new URL(url).pathname;
|
|
562
|
+
if (pathName === "/registry/sellers") {
|
|
563
|
+
return {
|
|
564
|
+
version: 10,
|
|
565
|
+
sellers: [{
|
|
566
|
+
id: "tbs-openrouter-ai-qecae",
|
|
567
|
+
name: "tbs-openrouter-ai-qecae",
|
|
568
|
+
app: "tbs-openrouter-ai-qecae",
|
|
569
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
570
|
+
status: "active",
|
|
571
|
+
supportedProtocols: ["chat_completions"],
|
|
572
|
+
paymentMethods: ["clawtip"]
|
|
573
|
+
}]
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
|
|
577
|
+
if (pathName === "/operator/admin/config") {
|
|
578
|
+
return {
|
|
579
|
+
config: {
|
|
580
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
581
|
+
upstreamApiKey: "live-secret-abcd",
|
|
582
|
+
maxConnections: 8,
|
|
583
|
+
maxQueueDepth: 4
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
throw new Error(`unexpected fetch url ${url}`);
|
|
588
|
+
},
|
|
589
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
590
|
+
calls.push(args);
|
|
591
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const result = await actions.updateSellerConfig("tbs-openrouter-ai-qecae", { maxConnections: 12 });
|
|
596
|
+
|
|
597
|
+
expect(result.ok).toBe(true);
|
|
598
|
+
expect(calls.map((args) => args.join(" "))).toEqual([
|
|
599
|
+
expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config validate --file"),
|
|
600
|
+
expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config put --file")
|
|
601
|
+
]);
|
|
602
|
+
});
|
|
603
|
+
|
|
397
604
|
test("admin UI seller rows render missing telemetry without unknown data labels", () => {
|
|
398
605
|
const html = adminUiHtml();
|
|
399
606
|
expect(html).toContain("class=\"spinner\"");
|
|
@@ -407,6 +614,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
407
614
|
expect(html).toContain("currentCreateJob");
|
|
408
615
|
expect(html).toContain("setCreateFormDisabled(true)");
|
|
409
616
|
expect(html).toContain("setCreateFormDisabled(false)");
|
|
617
|
+
expect(html).toContain("Retry create");
|
|
410
618
|
expect(html).toContain("data-progress-step");
|
|
411
619
|
expect(html).toContain("aria-expanded");
|
|
412
620
|
expect(html).toContain("progress-title");
|
|
@@ -414,25 +622,26 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
414
622
|
expect(html).toContain("Hide details");
|
|
415
623
|
expect(html).toContain("/api/jobs/");
|
|
416
624
|
expect(html).toContain("function uiErrorMessage");
|
|
417
|
-
expect(html).toContain("Admin UI connection lost.
|
|
418
|
-
expect(html).toContain("Admin
|
|
625
|
+
expect(html).toContain("Admin UI connection lost. Restart tb-admin ui and reload this page.");
|
|
626
|
+
expect(html).toContain("Admin profile authentication failed. Check the configured operator token.");
|
|
419
627
|
expect(html).toContain("renderCreateJob");
|
|
420
628
|
expect(html).toContain("pollCreateJob");
|
|
421
|
-
expect(html).toContain("Created and
|
|
629
|
+
expect(html).toContain("Created and added to bootstrap registry.");
|
|
422
630
|
expect(html).toContain("upstreamUrl:\"https://openrouter.ai/api/v1\"");
|
|
423
631
|
expect(html).toContain("loadingSpinner(\"Loading sellers\")");
|
|
424
632
|
expect(html).toContain("loadingSpinner(\"Loading configuration\")");
|
|
425
633
|
expect(html).toContain("loadingSpinner(\"Loading models\")");
|
|
426
634
|
expect(html).toContain("sellerRefreshIntervalMs = 30000");
|
|
427
|
-
expect(html).toContain("Last updated: never");
|
|
428
635
|
expect(html).toContain("function scheduleSellerRefresh()");
|
|
429
636
|
expect(html).toContain("sellerNextRefreshAt = new Date(Date.now() + sellerRefreshIntervalMs)");
|
|
430
637
|
expect(html).toContain("sellerRefreshTimer = setTimeout(() => loadSellers(), sellerRefreshIntervalMs)");
|
|
431
638
|
expect(html).toContain("Next refresh: ");
|
|
432
|
-
expect(html).toContain("
|
|
639
|
+
expect(html).not.toContain("sellerLastUpdated");
|
|
640
|
+
expect(html).not.toContain("Last updated:");
|
|
641
|
+
expect(html).not.toContain("s ago");
|
|
433
642
|
expect(html).toContain("sellerClockTimer = setInterval(() => updateSellerRefreshMeta(sellerRefreshInFlight), 1000)");
|
|
434
643
|
expect(html).toContain("function renderSellerRows(rows)");
|
|
435
|
-
expect(html).toContain("
|
|
644
|
+
expect(html).toContain("sellerRefreshLoaded = true");
|
|
436
645
|
expect(html).toContain("readonly-value");
|
|
437
646
|
expect(html).toContain("--seller-grid");
|
|
438
647
|
expect(html).toContain("grid-template-columns:var(--seller-grid)");
|
|
@@ -440,10 +649,25 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
440
649
|
expect(html).toContain("gap:12px;width:100%;min-width:0");
|
|
441
650
|
expect(html).toContain("detailFieldsHtml");
|
|
442
651
|
expect(html).toContain("data-original");
|
|
443
|
-
|
|
444
|
-
expect(html).toContain("
|
|
445
|
-
expect(html).toContain("
|
|
652
|
+
// Design-spec header labels (TTFT not Latency, Disc not Discount)
|
|
653
|
+
expect(html).toContain(">TTFT</span>");
|
|
654
|
+
expect(html).toContain(">Disc</span>");
|
|
655
|
+
expect(html).not.toContain(">Latency</span>");
|
|
656
|
+
// Spec-compliant token formats
|
|
657
|
+
expect(html).toContain("tok/s");
|
|
658
|
+
expect(html).toContain("AVG speed");
|
|
446
659
|
expect(html).toContain("Samples");
|
|
660
|
+
// Spec-compliant unknown-value char (em dash) lives in formatter
|
|
661
|
+
expect(html).toContain("UNKNOWN_VALUE");
|
|
662
|
+
expect(html).toContain("formatDuration");
|
|
663
|
+
expect(html).toContain("formatSellerStatus");
|
|
664
|
+
expect(html).toContain("formatDiscountRatio");
|
|
665
|
+
expect(html).toContain("formatBalanceAmount");
|
|
666
|
+
// No glassmorphism per spec
|
|
667
|
+
expect(html).not.toContain("backdrop-filter");
|
|
668
|
+
// Detail field references
|
|
669
|
+
expect(html).toContain("avgTokensPerSecond");
|
|
670
|
+
expect(html).toContain("lastTokensPerSecond");
|
|
447
671
|
expect(html).toContain("lastInferenceMs");
|
|
448
672
|
expect(html).toContain("upstreamBalanceSource");
|
|
449
673
|
expect(html).toContain("upstreamBalanceFetchedAt");
|
|
@@ -452,6 +676,7 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
452
676
|
expect(html).toContain("upstreamBalanceProbeUrl");
|
|
453
677
|
expect(html).toContain("upstreamBalanceProbeUserId");
|
|
454
678
|
expect(html).toContain("upstreamBalanceProbeRechargeUrl");
|
|
679
|
+
// Payment tabs
|
|
455
680
|
expect(html).toContain("paymentMethods");
|
|
456
681
|
expect(html).toContain("payment-tabs");
|
|
457
682
|
expect(html).toContain("payment-tab");
|
|
@@ -489,7 +714,9 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
489
714
|
expect(html).toContain("clawtipResourceUrl");
|
|
490
715
|
expect(html).toContain("clawtipActivationFeeFen");
|
|
491
716
|
expect(html).toContain("clawtipMicrosPerFen");
|
|
492
|
-
|
|
717
|
+
// Spec uses — (em dash) for unknown, not "not reported" or "n/a"
|
|
718
|
+
expect(html).not.toContain("not reported");
|
|
719
|
+
expect(html).not.toContain("\"n/a\"");
|
|
493
720
|
expect(html).toContain("aria-label=\"Seller specs\"");
|
|
494
721
|
expect(html).toContain("<svg viewBox=\"0 0 24 24\"");
|
|
495
722
|
expect(html).not.toContain(">i</span>");
|
|
@@ -500,7 +727,6 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
500
727
|
expect(html).not.toContain("Loading models...");
|
|
501
728
|
expect(html).not.toContain("<div id=\"createStatus\" class=\"status-line\">Ready</div>");
|
|
502
729
|
expect(html).not.toContain("not set");
|
|
503
|
-
expect(html).not.toContain("tok/s");
|
|
504
730
|
expect(html).not.toContain(" multiple");
|
|
505
731
|
});
|
|
506
732
|
|
|
@@ -729,10 +955,10 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
729
955
|
profile: "bootstrap",
|
|
730
956
|
fetchJson: async (url) => createSellerFetchJson(url),
|
|
731
957
|
commandRunner: async (args): Promise<UiActionResult> => {
|
|
732
|
-
if (
|
|
958
|
+
if (isBootstrapSellersAdd(args)) {
|
|
733
959
|
const filePath = args[args.indexOf("--file") + 1];
|
|
734
|
-
const
|
|
735
|
-
expect(
|
|
960
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
|
|
961
|
+
expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
|
|
736
962
|
}
|
|
737
963
|
calls.push(args);
|
|
738
964
|
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
@@ -772,6 +998,8 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
772
998
|
expect(response.registryPublish?.ok).toBe(true);
|
|
773
999
|
expect(response.publishRegistry).toBe("completed");
|
|
774
1000
|
expect(response.configPreview.upstreamUrl).toBe("https://openrouter.ai/api");
|
|
1001
|
+
expect(response.configPreview.sellerId).toBe("tbs-nrt-07");
|
|
1002
|
+
expect(response.configPreview.manifestVersion).toBe("manifest.v1");
|
|
775
1003
|
expect(response.configPreview.upstreamBalanceProbe).toEqual({
|
|
776
1004
|
template: "usage_generic",
|
|
777
1005
|
url: "https://code.shoestravel.xin/v1/usage",
|
|
@@ -798,13 +1026,153 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
798
1026
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret status"),
|
|
799
1027
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret seller-config put --file"),
|
|
800
1028
|
expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret upstreams refresh --auto-models"),
|
|
801
|
-
|
|
802
|
-
expect.stringContaining("bootstrap sellers put --file")
|
|
1029
|
+
expect.stringContaining("bootstrap sellers add --file")
|
|
803
1030
|
]);
|
|
1031
|
+
expect(commandLines[5]).toContain("--expect-version 7");
|
|
804
1032
|
expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
|
|
1033
|
+
expect(mgr.getProfile("tbs-nrt-07")).toEqual({
|
|
1034
|
+
url: "https://tbs-nrt-07.fly.dev",
|
|
1035
|
+
token: "operator-secret"
|
|
1036
|
+
});
|
|
1037
|
+
} finally {
|
|
1038
|
+
await fixture.close();
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
test("UiActions create retries transient upstream model refresh failures", async () => {
|
|
1043
|
+
const fixture = await startFixtureAdminServer();
|
|
1044
|
+
try {
|
|
1045
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
1046
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
1047
|
+
const registry = await createSellerFetchJson(`${fixture.baseUrl}/registry/sellers`);
|
|
1048
|
+
let refreshAttempts = 0;
|
|
1049
|
+
const calls: string[][] = [];
|
|
1050
|
+
const actions = new UiActions({
|
|
1051
|
+
configManager: mgr,
|
|
1052
|
+
url: fixture.baseUrl,
|
|
1053
|
+
fetchJson: async (url) => {
|
|
1054
|
+
const pathName = new URL(url).pathname;
|
|
1055
|
+
if (pathName === "/registry/sellers") {
|
|
1056
|
+
return registry;
|
|
1057
|
+
}
|
|
1058
|
+
return createSellerFetchJson(url);
|
|
1059
|
+
},
|
|
1060
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
1061
|
+
calls.push(args);
|
|
1062
|
+
if (args.includes("upstreams") && args.includes("refresh")) {
|
|
1063
|
+
refreshAttempts += 1;
|
|
1064
|
+
if (refreshAttempts < 3) {
|
|
1065
|
+
return { ok: false, stdout: "", stderr: "Error: Connection failed: fetch failed", command: ["node", "tb-admin", ...args] };
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
|
|
1069
|
+
const filePath = args[args.indexOf("--file") + 1];
|
|
1070
|
+
const doc = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1071
|
+
expect(doc).toMatchObject({ id: "tbs-nrt-07" });
|
|
1072
|
+
expect(doc.models).toHaveLength(2);
|
|
1073
|
+
}
|
|
1074
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
const response = await actions.createSeller({
|
|
1079
|
+
sellerName: "tbs-nrt-07",
|
|
1080
|
+
app: "tbs-nrt-07",
|
|
1081
|
+
region: "nrt",
|
|
1082
|
+
image: "registry.fly.io/tb-seller:latest",
|
|
1083
|
+
upstreamWebsite: "https://openrouter.ai",
|
|
1084
|
+
upstreamUrl: "https://openrouter.ai/api/v1",
|
|
1085
|
+
upstreamApiKey: "fixture-upstream-key",
|
|
1086
|
+
upstreamBalanceProbeTemplate: "none",
|
|
1087
|
+
maxConnections: 8,
|
|
1088
|
+
maxQueueDepth: 4,
|
|
1089
|
+
markupRatio: 1.2,
|
|
1090
|
+
discountRatio: 1,
|
|
1091
|
+
paymentMethods: ["mock"],
|
|
1092
|
+
flyConfig: "deploy/fly.io/fly.tb-seller.toml"
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
expect(response.modelsRefresh?.ok).toBe(true);
|
|
1096
|
+
expect(response.registryPublish?.ok).toBe(true);
|
|
1097
|
+
expect(refreshAttempts).toBe(3);
|
|
1098
|
+
expect(calls.filter((args) => args.includes("upstreams") && args.includes("refresh"))).toHaveLength(3);
|
|
805
1099
|
} finally {
|
|
806
1100
|
await fixture.close();
|
|
807
1101
|
}
|
|
1102
|
+
}, 30000);
|
|
1103
|
+
|
|
1104
|
+
test("UiActions create publishes registry from wrapped upstream metadata", async () => {
|
|
1105
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
1106
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
1107
|
+
let published: any;
|
|
1108
|
+
const actions = new UiActions({
|
|
1109
|
+
configManager: mgr,
|
|
1110
|
+
url: "https://bootstrap.example.test",
|
|
1111
|
+
fetchJson: async (url) => createSellerFetchJson(url, "wrappedArray"),
|
|
1112
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
1113
|
+
if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
|
|
1114
|
+
const filePath = args[args.indexOf("--file") + 1];
|
|
1115
|
+
published = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1116
|
+
}
|
|
1117
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const response = await actions.createSeller(createSellerRequestFixture({
|
|
1122
|
+
sellerName: "openrouter-ai",
|
|
1123
|
+
app: "tbs-openrouter-ai-qecae"
|
|
1124
|
+
}));
|
|
1125
|
+
|
|
1126
|
+
expect(response.registryPublish?.ok).toBe(true);
|
|
1127
|
+
expect(published).toMatchObject({
|
|
1128
|
+
id: "tbs-openrouter-ai-qecae",
|
|
1129
|
+
name: "tbs-openrouter-ai-qecae",
|
|
1130
|
+
app: "tbs-openrouter-ai-qecae",
|
|
1131
|
+
status: "active",
|
|
1132
|
+
models: ["openai/gpt-5.4", "openai/gpt-5.4-mini"],
|
|
1133
|
+
supportedProtocols: ["chat_completions"]
|
|
1134
|
+
});
|
|
1135
|
+
expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
|
|
1136
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
1137
|
+
token: "operator-secret"
|
|
1138
|
+
});
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("UiActions create falls back to public manifest models for registry publish", async () => {
|
|
1142
|
+
const mgr = new ConfigManager(TEMP_CONF_PATH);
|
|
1143
|
+
mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
|
|
1144
|
+
let published: any;
|
|
1145
|
+
const actions = new UiActions({
|
|
1146
|
+
configManager: mgr,
|
|
1147
|
+
url: "https://bootstrap.example.test",
|
|
1148
|
+
fetchJson: async (url) => createSellerFetchJson(url, "manifest"),
|
|
1149
|
+
commandRunner: async (args): Promise<UiActionResult> => {
|
|
1150
|
+
if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
|
|
1151
|
+
const filePath = args[args.indexOf("--file") + 1];
|
|
1152
|
+
published = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
1153
|
+
}
|
|
1154
|
+
return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
const response = await actions.createSeller(createSellerRequestFixture({
|
|
1159
|
+
sellerName: "openrouter-ai",
|
|
1160
|
+
app: "tbs-openrouter-ai-qecae"
|
|
1161
|
+
}));
|
|
1162
|
+
|
|
1163
|
+
expect(response.registryPublish?.ok).toBe(true);
|
|
1164
|
+
expect(published).toMatchObject({
|
|
1165
|
+
id: "tbs-openrouter-ai-qecae",
|
|
1166
|
+
name: "tbs-openrouter-ai-qecae",
|
|
1167
|
+
app: "tbs-openrouter-ai-qecae",
|
|
1168
|
+
status: "active",
|
|
1169
|
+
models: ["anthropic/claude-opus-4.7"],
|
|
1170
|
+
supportedProtocols: ["chat_completions"]
|
|
1171
|
+
});
|
|
1172
|
+
expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
|
|
1173
|
+
url: "https://tbs-openrouter-ai-qecae.fly.dev",
|
|
1174
|
+
token: "operator-secret"
|
|
1175
|
+
});
|
|
808
1176
|
});
|
|
809
1177
|
|
|
810
1178
|
test("UiActions create supports mock-only payment without ClawTip parameters", async () => {
|
|
@@ -879,12 +1247,13 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
879
1247
|
|
|
880
1248
|
test("UiActions subprocess runner uses the tb-admin bin entrypoint", async () => {
|
|
881
1249
|
const previousArgv = process.argv[1];
|
|
882
|
-
process.argv[1] = path.resolve(
|
|
1250
|
+
process.argv[1] = path.resolve(process.env.HOME || "/tmp", "packages/admin-cli/bin/tb-admin.js");
|
|
883
1251
|
const { runTbAdmin } = await import("../src/ui-actions.js");
|
|
884
1252
|
try {
|
|
885
1253
|
const result = await runTbAdmin(["--help"], 30000);
|
|
886
1254
|
expect(result.ok).toBe(true);
|
|
887
1255
|
expect(result.command[1]).toMatch(/packages\/admin-cli\/bin\/tb-admin\.js$/);
|
|
1256
|
+
expect(fs.existsSync(result.command[1])).toBe(true);
|
|
888
1257
|
expect(result.stdout).toContain("Remote admin CLI");
|
|
889
1258
|
} finally {
|
|
890
1259
|
process.argv[1] = previousArgv;
|
|
@@ -892,6 +1261,127 @@ describe("Admin CLI Config Profile Management Tests", () => {
|
|
|
892
1261
|
});
|
|
893
1262
|
});
|
|
894
1263
|
|
|
1264
|
+
describe("Admin CLI Display Format Spec Compliance", () => {
|
|
1265
|
+
test("formatDuration uses ms under 1s and 2-decimal seconds above 1s", () => {
|
|
1266
|
+
expect(formatDuration(undefined)).toBe(UNKNOWN_VALUE);
|
|
1267
|
+
expect(formatDuration(0)).toBe("0ms");
|
|
1268
|
+
expect(formatDuration(272)).toBe("272ms");
|
|
1269
|
+
expect(formatDuration(999)).toBe("999ms");
|
|
1270
|
+
expect(formatDuration(1000)).toBe("1.00s");
|
|
1271
|
+
expect(formatDuration(3542)).toBe("3.54s");
|
|
1272
|
+
expect(formatDuration(11540)).toBe("11.54s");
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1275
|
+
test("formatMoney uses 4 decimals by default and 6 for tiny ledger amounts", () => {
|
|
1276
|
+
expect(formatMoney(undefined)).toBe(UNKNOWN_VALUE);
|
|
1277
|
+
expect(formatMoney(0)).toBe("$0.0000");
|
|
1278
|
+
expect(formatMoney(138600)).toBe("$0.1386");
|
|
1279
|
+
expect(formatMoney(2_772_200)).toBe("$2.7722");
|
|
1280
|
+
expect(formatMoney(138600, { ledger: true })).toBe("$0.1386");
|
|
1281
|
+
expect(formatMoney(5782, { ledger: true })).toBe("$0.005782");
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
test("formatDiscountRatio converts ratio to percentage per the design spec", () => {
|
|
1285
|
+
expect(formatDiscountRatio(undefined)).toBe(UNKNOWN_VALUE);
|
|
1286
|
+
expect(formatDiscountRatio(0.5)).toBe("50%");
|
|
1287
|
+
expect(formatDiscountRatio(0.01)).toBe("99%");
|
|
1288
|
+
expect(formatDiscountRatio(0.99)).toBe("1%");
|
|
1289
|
+
expect(formatDiscountRatio(1)).toBe("0%");
|
|
1290
|
+
expect(formatDiscountRatio(0)).toBe("100%");
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
test("formatPercent rounds to whole percent", () => {
|
|
1294
|
+
expect(formatPercent(0.123)).toBe("12%");
|
|
1295
|
+
expect(formatPercent(0.987)).toBe("99%");
|
|
1296
|
+
expect(formatPercent(undefined)).toBe(UNKNOWN_VALUE);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test("formatPricePair renders $/1M with 4 decimals per spec", () => {
|
|
1300
|
+
expect(formatPricePair(undefined, undefined)).toBe(UNKNOWN_VALUE);
|
|
1301
|
+
expect(formatPricePair(2_500_000, 3_000_000)).toBe("$2.5000 / $3.0000");
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
test("formatCount uses en-US grouping", () => {
|
|
1305
|
+
expect(formatCount(undefined)).toBe(UNKNOWN_VALUE);
|
|
1306
|
+
expect(formatCount(7_529)).toBe("7,529");
|
|
1307
|
+
expect(formatCount(218_700)).toBe("218,700");
|
|
1308
|
+
});
|
|
1309
|
+
|
|
1310
|
+
test("formatTimeCompact emits HH:mm for today and MM/DD HH:mm otherwise", () => {
|
|
1311
|
+
const now = new Date();
|
|
1312
|
+
const todayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 51).toISOString();
|
|
1313
|
+
const yesterdayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 9, 5).toISOString();
|
|
1314
|
+
expect(formatTimeCompact(undefined)).toBe(UNKNOWN_VALUE);
|
|
1315
|
+
expect(formatTimeCompact(todayIso)).toBe("14:51");
|
|
1316
|
+
expect(formatTimeCompact(yesterdayIso)).toMatch(/^\d{2}\/\d{2} 09:05$/);
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
test("formatTimeFull uses YYYY-MM-DD HH:mm:ss for detail panels", () => {
|
|
1320
|
+
expect(formatTimeFull(undefined)).toBe(UNKNOWN_VALUE);
|
|
1321
|
+
expect(formatTimeFull("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
test("formatTimeLedger uses YYYY/MM/DD HH:mm:ss for audit tables", () => {
|
|
1325
|
+
expect(formatTimeLedger(undefined)).toBe(UNKNOWN_VALUE);
|
|
1326
|
+
expect(formatTimeLedger("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
test("formatSellerId trims long tbs ids to 10 chars", () => {
|
|
1330
|
+
expect(formatSellerId(undefined)).toBe(UNKNOWN_VALUE);
|
|
1331
|
+
expect(formatSellerId("tbs-825edb")).toBe("tbs-825edb");
|
|
1332
|
+
expect(formatSellerId("tbs-openrouter-ai-k7p9x")).toBe("tbs-openro");
|
|
1333
|
+
});
|
|
1334
|
+
|
|
1335
|
+
test("formatSpeed renders to one decimal with tok/s suffix", () => {
|
|
1336
|
+
expect(formatSpeed(undefined)).toBe(UNKNOWN_VALUE);
|
|
1337
|
+
expect(formatSpeed(0)).toBe("0.0 tok/s");
|
|
1338
|
+
expect(formatSpeed(11.54)).toBe("11.5 tok/s");
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
test("formatSellerCapacity renders used / limit with en-US grouping", () => {
|
|
1342
|
+
expect(formatSellerCapacity(undefined, undefined)).toBe(UNKNOWN_VALUE);
|
|
1343
|
+
expect(formatSellerCapacity(8, 16)).toBe("8 / 16");
|
|
1344
|
+
expect(formatSellerCapacity(undefined, 16)).toBe(`${UNKNOWN_VALUE} / 16`);
|
|
1345
|
+
expect(formatSellerCapacity(8, undefined)).toBe(`8 / ${UNKNOWN_VALUE}`);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
test("formatBalanceAmount uses 0 decimals for >=100 and 2 decimals otherwise", () => {
|
|
1349
|
+
expect(formatBalanceAmount(undefined, "USD")).toBe(UNKNOWN_VALUE);
|
|
1350
|
+
expect(formatBalanceAmount(47_950_000, "USD")).toBe("USD 47.95");
|
|
1351
|
+
expect(formatBalanceAmount(479_500_000, "USD")).toBe("USD 480");
|
|
1352
|
+
expect(formatBalanceAmount(50_000_000, "EUR")).toBe("EUR 50.00");
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
test("formatSellerStatus maps all internal node statuses to the 7 canonical labels", () => {
|
|
1356
|
+
const canonical = new Set(["ok", "online", "configured", "pending", "degraded", "error", "unknown"]);
|
|
1357
|
+
for (const value of ["active", "healthy", "online", "configured", "pending", "draining", "degraded", "busy_capacity", "offline", "unhealthy", "error", "auth_unknown", "unknown", undefined, "weird-state"]) {
|
|
1358
|
+
expect(canonical.has(formatSellerStatus(value))).toBe(true);
|
|
1359
|
+
}
|
|
1360
|
+
expect(formatSellerStatus("active")).toBe("ok");
|
|
1361
|
+
expect(formatSellerStatus("draining")).toBe("degraded");
|
|
1362
|
+
expect(formatSellerStatus("busy_capacity")).toBe("degraded");
|
|
1363
|
+
expect(formatSellerStatus("offline")).toBe("error");
|
|
1364
|
+
expect(formatSellerStatus("auth_unknown")).toBe("unknown");
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
test("statusTone / sellerStatusTone keep green/amber/red/blue/gray buckets stable", () => {
|
|
1368
|
+
expect(statusTone("ok")).toBe("green");
|
|
1369
|
+
expect(statusTone("online")).toBe("green");
|
|
1370
|
+
expect(statusTone("configured")).toBe("green");
|
|
1371
|
+
expect(statusTone("pending")).toBe("amber");
|
|
1372
|
+
expect(statusTone("degraded")).toBe("amber");
|
|
1373
|
+
expect(statusTone("error")).toBe("red");
|
|
1374
|
+
expect(statusTone("offline")).toBe("red");
|
|
1375
|
+
expect(statusTone("unknown")).toBe("gray");
|
|
1376
|
+
expect(statusTone("running")).toBe("blue");
|
|
1377
|
+
expect(sellerStatusTone("active")).toBe("green");
|
|
1378
|
+
expect(sellerStatusTone("draining")).toBe("amber");
|
|
1379
|
+
expect(sellerStatusTone("busy_capacity")).toBe("amber");
|
|
1380
|
+
expect(sellerStatusTone("offline")).toBe("red");
|
|
1381
|
+
expect(sellerStatusTone("auth_unknown")).toBe("gray");
|
|
1382
|
+
});
|
|
1383
|
+
});
|
|
1384
|
+
|
|
895
1385
|
async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
|
|
896
1386
|
const server = http.createServer((req, res) => {
|
|
897
1387
|
const pathName = new URL(req.url || "/", "http://127.0.0.1").pathname;
|
|
@@ -990,7 +1480,27 @@ function sendJson(res: http.ServerResponse, body: unknown): void {
|
|
|
990
1480
|
res.end(JSON.stringify(body));
|
|
991
1481
|
}
|
|
992
1482
|
|
|
993
|
-
|
|
1483
|
+
function createSellerRequestFixture(overrides: Partial<CreateSellerRequest> = {}): CreateSellerRequest {
|
|
1484
|
+
return {
|
|
1485
|
+
sellerName: "tbs-nrt-07",
|
|
1486
|
+
app: "tbs-nrt-07",
|
|
1487
|
+
region: "nrt",
|
|
1488
|
+
image: "registry.fly.io/tb-seller:latest",
|
|
1489
|
+
upstreamWebsite: "https://openrouter.ai",
|
|
1490
|
+
upstreamUrl: "https://openrouter.ai/api/v1",
|
|
1491
|
+
upstreamApiKey: "fixture-upstream-key",
|
|
1492
|
+
upstreamBalanceProbeTemplate: "none",
|
|
1493
|
+
maxConnections: 8,
|
|
1494
|
+
maxQueueDepth: 4,
|
|
1495
|
+
markupRatio: 1.2,
|
|
1496
|
+
discountRatio: 1,
|
|
1497
|
+
paymentMethods: ["mock"],
|
|
1498
|
+
flyConfig: "deploy/fly.io/fly.tb-seller.toml",
|
|
1499
|
+
...overrides
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
async function createSellerFetchJson(url: string, mode: "default" | "wrapped" | "wrappedArray" | "manifest" = "default"): Promise<unknown> {
|
|
994
1504
|
const pathName = new URL(url).pathname;
|
|
995
1505
|
if (pathName === "/registry/sellers") {
|
|
996
1506
|
return {
|
|
@@ -999,6 +1509,38 @@ async function createSellerFetchJson(url: string): Promise<unknown> {
|
|
|
999
1509
|
};
|
|
1000
1510
|
}
|
|
1001
1511
|
if (pathName === "/operator/admin/upstreams") {
|
|
1512
|
+
if (mode === "wrapped") {
|
|
1513
|
+
return {
|
|
1514
|
+
upstreams: {
|
|
1515
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
1516
|
+
models: [
|
|
1517
|
+
{ id: "openai/gpt-5.4" },
|
|
1518
|
+
{ id: "openai/gpt-5.4-mini" }
|
|
1519
|
+
],
|
|
1520
|
+
supportedProtocols: ["chat_completions"]
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
if (mode === "wrappedArray") {
|
|
1525
|
+
return {
|
|
1526
|
+
upstreams: [{
|
|
1527
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
1528
|
+
models: [
|
|
1529
|
+
{ id: "openai/gpt-5.4" },
|
|
1530
|
+
{ id: "openai/gpt-5.4-mini" }
|
|
1531
|
+
],
|
|
1532
|
+
supportedProtocols: ["chat_completions"]
|
|
1533
|
+
}]
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (mode === "manifest") {
|
|
1537
|
+
return {
|
|
1538
|
+
upstreams: {
|
|
1539
|
+
upstreamUrl: "https://openrouter.ai/api",
|
|
1540
|
+
modelsCount: 1
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1002
1544
|
return {
|
|
1003
1545
|
upstreamUrl: "https://openrouter.ai/api",
|
|
1004
1546
|
models: [
|
|
@@ -1015,12 +1557,19 @@ async function createSellerFetchJson(url: string): Promise<unknown> {
|
|
|
1015
1557
|
supportedProtocols: ["chat_completions"]
|
|
1016
1558
|
};
|
|
1017
1559
|
}
|
|
1560
|
+
if (pathName === "/manifest" && mode === "manifest") {
|
|
1561
|
+
return {
|
|
1562
|
+
sellerId: "tbs-nrt-07",
|
|
1563
|
+
models: ["anthropic/claude-opus-4.7"],
|
|
1564
|
+
supportedProtocols: ["chat_completions"]
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1018
1567
|
throw new Error(`unexpected fetch url ${url}`);
|
|
1019
1568
|
}
|
|
1020
1569
|
|
|
1021
|
-
function
|
|
1022
|
-
const
|
|
1023
|
-
return
|
|
1570
|
+
function isBootstrapSellersAdd(args: string[]): boolean {
|
|
1571
|
+
const addIndex = args.indexOf("add");
|
|
1572
|
+
return addIndex >= 2 && args[addIndex - 2] === "bootstrap" && args[addIndex - 1] === "sellers";
|
|
1024
1573
|
}
|
|
1025
1574
|
|
|
1026
1575
|
function jsonResponse(body: unknown, status = 200): Response {
|