@tokenbuddy/tb-admin 1.0.35 → 1.0.37

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.
Files changed (93) hide show
  1. package/dist/src/cli.js +92 -19
  2. package/dist/src/config.d.ts +7 -1
  3. package/dist/src/config.js +16 -4
  4. package/dist/src/display-format.js +6 -14
  5. package/dist/src/init-command.d.ts +50 -0
  6. package/dist/src/init-command.js +347 -0
  7. package/dist/src/providers/fly-io.d.ts +3 -0
  8. package/dist/src/providers/fly-io.js +137 -0
  9. package/dist/src/providers/provider-definition.d.ts +38 -0
  10. package/dist/src/providers/provider-definition.js +2 -0
  11. package/dist/src/seller.d.ts +2 -0
  12. package/dist/src/seller.js +30 -13
  13. package/dist/src/server-cmd.d.ts +1 -0
  14. package/dist/src/server-cmd.js +9 -2
  15. package/dist/src/ui-actions.d.ts +3 -0
  16. package/dist/src/ui-actions.js +199 -27
  17. package/dist/src/ui-command.js +3 -2
  18. package/dist/src/ui-state.d.ts +1 -3
  19. package/dist/src/ui-state.js +4 -8
  20. package/dist/src/ui-static.js +43 -15
  21. package/dist/src/workdir.d.ts +21 -0
  22. package/dist/src/workdir.js +50 -0
  23. package/package.json +8 -2
  24. package/templates/providers/fly.io/admin.toml.example +18 -0
  25. package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
  26. package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
  27. package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
  28. package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
  29. package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
  30. package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
  31. package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
  32. package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
  33. package/templates/providers/fly.io/env/deploy.env.example +12 -0
  34. package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
  35. package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
  36. package/templates/providers/fly.io/provider.toml.example +10 -0
  37. package/dist/src/bootstrap-registry.d.ts.map +0 -1
  38. package/dist/src/bootstrap-registry.js.map +0 -1
  39. package/dist/src/cli.d.ts.map +0 -1
  40. package/dist/src/cli.js.map +0 -1
  41. package/dist/src/client.d.ts.map +0 -1
  42. package/dist/src/client.js.map +0 -1
  43. package/dist/src/config.d.ts.map +0 -1
  44. package/dist/src/config.js.map +0 -1
  45. package/dist/src/display-format.d.ts.map +0 -1
  46. package/dist/src/display-format.js.map +0 -1
  47. package/dist/src/index.d.ts.map +0 -1
  48. package/dist/src/index.js.map +0 -1
  49. package/dist/src/provider.d.ts.map +0 -1
  50. package/dist/src/provider.js.map +0 -1
  51. package/dist/src/seller.d.ts.map +0 -1
  52. package/dist/src/seller.js.map +0 -1
  53. package/dist/src/server-cmd.d.ts.map +0 -1
  54. package/dist/src/server-cmd.js.map +0 -1
  55. package/dist/src/ui-actions.d.ts.map +0 -1
  56. package/dist/src/ui-actions.js.map +0 -1
  57. package/dist/src/ui-command.d.ts.map +0 -1
  58. package/dist/src/ui-command.js.map +0 -1
  59. package/dist/src/ui-server.d.ts.map +0 -1
  60. package/dist/src/ui-server.js.map +0 -1
  61. package/dist/src/ui-state.d.ts.map +0 -1
  62. package/dist/src/ui-state.js.map +0 -1
  63. package/dist/src/ui-static.d.ts.map +0 -1
  64. package/dist/src/ui-static.js.map +0 -1
  65. package/dist/src/upstream-balance-probe.d.ts.map +0 -1
  66. package/dist/src/upstream-balance-probe.js.map +0 -1
  67. package/dist/src/vendor-client.d.ts.map +0 -1
  68. package/dist/src/vendor-client.js.map +0 -1
  69. package/dist/src/vendor-commands.d.ts.map +0 -1
  70. package/dist/src/vendor-commands.js.map +0 -1
  71. package/src/bootstrap-registry.ts +0 -90
  72. package/src/cli.ts +0 -1614
  73. package/src/client.ts +0 -179
  74. package/src/config.ts +0 -194
  75. package/src/display-format.ts +0 -411
  76. package/src/index.ts +0 -11
  77. package/src/provider.ts +0 -150
  78. package/src/seller.ts +0 -538
  79. package/src/server-cmd.ts +0 -362
  80. package/src/ui-actions.ts +0 -1040
  81. package/src/ui-command.ts +0 -44
  82. package/src/ui-server.ts +0 -353
  83. package/src/ui-state.ts +0 -1318
  84. package/src/ui-static.ts +0 -673
  85. package/src/upstream-balance-probe.ts +0 -13
  86. package/src/vendor-client.ts +0 -23
  87. package/src/vendor-commands.ts +0 -65
  88. package/tests/admin.test.ts +0 -2162
  89. package/tests/seller.test.ts +0 -388
  90. package/tests/ui-state-fleet.test.ts +0 -526
  91. package/tests/ui-static-row.test.ts +0 -467
  92. package/tests/vendor-cli.test.ts +0 -241
  93. package/tsconfig.json +0 -8
@@ -1,2162 +0,0 @@
1
- import { ConfigManager } from "../src/config.js";
2
- import { buildAdminCli } from "../src/cli.js";
3
- import { FlyProvider, parseFlyMachineIds, requirePublishedDockerImage } from "../src/server-cmd.js";
4
- import { startAdminUiServer } from "../src/ui-server.js";
5
- import { defaultUiProfile } from "../src/ui-command.js";
6
- import { AdminUiState } from "../src/ui-state.js";
7
- import { UiActions, type CreateSellerRequest, type UiActionResult } from "../src/ui-actions.js";
8
- import { adminUiHtml } from "../src/ui-static.js";
9
- import {
10
- formatBalanceAmount,
11
- formatCount,
12
- formatDiscountRatio,
13
- formatDuration,
14
- formatMoney,
15
- formatPercent,
16
- formatPricePair,
17
- formatSellerCapacity,
18
- formatSellerStatus,
19
- formatSellerId,
20
- formatSpeed,
21
- formatTimeCompact,
22
- formatTimeFull,
23
- formatTimeLedger,
24
- sellerStatusTone,
25
- statusTone,
26
- UNKNOWN_VALUE
27
- } from "../src/display-format.js";
28
- import {
29
- BalanceProbeCache,
30
- probeUpstreamBalance
31
- } from "../src/upstream-balance-probe.js";
32
- import {
33
- validateRegistryDocument
34
- } from "../src/bootstrap-registry.js";
35
- import * as fs from "fs";
36
- import * as http from "http";
37
- import * as path from "path";
38
- import * as vm from "vm";
39
-
40
- const TEMP_CONF_PATH = path.resolve(__dirname, "../../data-test/admin-config.json");
41
- const PACKAGE_JSON = path.resolve(__dirname, "../package.json");
42
- const ADMIN_UI_STATE_TEST_TIMEOUT_MS = 15_000;
43
-
44
- describe("Admin CLI Config Profile Management Tests", () => {
45
- beforeEach(() => {
46
- if (fs.existsSync(TEMP_CONF_PATH)) {
47
- try { fs.unlinkSync(TEMP_CONF_PATH); } catch (e) {}
48
- }
49
- });
50
-
51
- afterAll(() => {
52
- if (fs.existsSync(TEMP_CONF_PATH)) {
53
- try { fs.unlinkSync(TEMP_CONF_PATH); } catch (e) {}
54
- }
55
- const dir = path.dirname(TEMP_CONF_PATH);
56
- if (fs.existsSync(dir)) {
57
- try { fs.rmdirSync(dir); } catch (e) {}
58
- }
59
- });
60
-
61
- test("Load and save profile seamlessly", () => {
62
- const mgr = new ConfigManager(TEMP_CONF_PATH);
63
-
64
- // Initial empty
65
- expect(mgr.listProfiles().length).toBe(0);
66
-
67
- // Save profile 1
68
- mgr.setProfile("prod", { url: "http://127.0.0.1:8000", token: "secret-op" });
69
- expect(mgr.listProfiles()).toContain("prod");
70
-
71
- const p = mgr.getProfile("prod");
72
- expect(p?.url).toBe("http://127.0.0.1:8000");
73
- expect(p?.token).toBe("secret-op");
74
-
75
- // Default profile is automatically set
76
- const defaultP = mgr.getProfile();
77
- expect(defaultP?.url).toBe("http://127.0.0.1:8000");
78
- });
79
-
80
- test("tb-admin version follows package version", () => {
81
- const packageJson = JSON.parse(fs.readFileSync(PACKAGE_JSON, "utf8"));
82
- const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
83
-
84
- expect(program.version()).toBe(packageJson.version);
85
- });
86
-
87
- test("admin UI inline script is valid JavaScript", () => {
88
- const html = adminUiHtml();
89
- const scripts = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((match) => match[1]);
90
-
91
- expect(scripts.length).toBe(1);
92
- expect(() => new vm.Script(scripts[0])).not.toThrow();
93
- });
94
-
95
- test("admin UI keeps release state in the seller table instead of a separate releases page", () => {
96
- const html = adminUiHtml();
97
-
98
- expect(html).toContain("<span>Pub</span>");
99
- expect(html).not.toContain("Release Requests");
100
- expect(html).not.toContain('data-page="releases"');
101
- expect(html).not.toContain('id="page-releases"');
102
- expect(html).not.toContain('id="releasesGrid"');
103
- expect(html).not.toContain('btn.dataset.page === "releases"');
104
- expect(html).not.toContain("/api/vendor/release-requests");
105
- expect(html).not.toContain('id="page-bootstrap"');
106
- expect(html).not.toContain('getElementById("bootstrapGrid")');
107
- });
108
-
109
- test("Switch default profiles", () => {
110
- const mgr = new ConfigManager(TEMP_CONF_PATH);
111
- mgr.setProfile("prod", { url: "http://127.0.0.1:8000", token: "secret-op" });
112
- mgr.setProfile("dev", { url: "http://127.0.0.1:8001", token: "dev-op" });
113
-
114
- mgr.useProfile("dev");
115
- const active = mgr.getProfile();
116
- expect(active?.url).toBe("http://127.0.0.1:8001");
117
- expect(active?.token).toBe("dev-op");
118
-
119
- expect(() => {
120
- mgr.useProfile("non-exist");
121
- }).toThrow("does not exist");
122
- });
123
-
124
- test("bootstrap sellers commands parse without global option conflicts", () => {
125
- const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
126
- const bootstrap = program.commands.find((command) => command.name() === "bootstrap");
127
- const sellers = bootstrap?.commands.find((command) => command.name() === "sellers");
128
- const sellerConfig = program.commands.find((command) => command.name() === "seller-config");
129
- const payments = program.commands.find((command) => command.name() === "payments");
130
- const pricingMonitor = program.commands.find((command) => command.name() === "pricing-monitor");
131
- const upstreams = program.commands.find((command) => command.name() === "upstreams");
132
- const models = program.commands.find((command) => command.name() === "models");
133
- const ui = program.commands.find((command) => command.name() === "ui");
134
-
135
- expect(bootstrap).toBeDefined();
136
- expect(sellers).toBeDefined();
137
- expect(sellers?.commands.map((command) => command.name()).sort()).toEqual([
138
- "add",
139
- "get",
140
- "list",
141
- "put",
142
- "remove",
143
- "status",
144
- "update",
145
- "validate"
146
- ]);
147
- expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
148
- expect(sellers?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--force")).toBe(true);
149
- expect(sellers?.commands.find((command) => command.name() === "add")?.options.some((option) => option.long === "--expect-version")).toBe(true);
150
- expect(sellers?.commands.find((command) => command.name() === "update")?.options.some((option) => option.long === "--expect-version")).toBe(true);
151
- expect(sellers?.commands.find((command) => command.name() === "status")?.options.some((option) => option.long === "--expect-version")).toBe(true);
152
- expect(sellers?.commands.find((command) => command.name() === "remove")?.options.some((option) => option.long === "--expect-version")).toBe(true);
153
- expect(sellers?.commands.find((command) => command.name() === "validate")?.options.some((option) => option.long === "--file")).toBe(true);
154
- const defaultSeller = bootstrap?.commands.find((command) => command.name() === "default-seller");
155
- const registry = bootstrap?.commands.find((command) => command.name() === "registry");
156
- expect(defaultSeller?.commands.map((command) => command.name()).sort()).toEqual(["set"]);
157
- expect(defaultSeller?.commands.find((command) => command.name() === "set")?.options.some((option) => option.long === "--expect-version")).toBe(true);
158
- expect(registry?.commands.map((command) => command.name()).sort()).toEqual(["diff", "import", "publish", "versions"]);
159
- expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--dry-run")).toBe(true);
160
- expect(registry?.commands.find((command) => command.name() === "import")?.options.some((option) => option.long === "--force")).toBe(true);
161
- expect(sellerConfig).toBeDefined();
162
- expect(sellerConfig?.commands.map((command) => command.name()).sort()).toEqual(["get", "put", "validate"]);
163
- expect(sellerConfig?.commands.find((command) => command.name() === "put")?.options.some((option) => option.long === "--file")).toBe(true);
164
- expect(sellerConfig?.commands.find((command) => command.name() === "validate")?.options.some((option) => option.long === "--file")).toBe(true);
165
- expect(upstreams).toBeDefined();
166
- expect(ui).toBeDefined();
167
- expect(ui?.options.find((option) => option.long === "--host")?.defaultValue).toBe("127.0.0.1");
168
- expect(ui?.options.find((option) => option.long === "--port")?.defaultValue).toBe(17822);
169
- expect(models).toBeDefined();
170
- expect(pricingMonitor).toBeDefined();
171
- expect(pricingMonitor?.commands.map((command) => command.name()).sort()).toEqual(["disable", "enable", "status"]);
172
- expect(models?.options.some((option) => option.long === "--json")).toBe(true);
173
- expect(upstreams?.commands.map((command) => command.name()).sort()).toEqual(["get", "refresh", "update"]);
174
- expect(upstreams?.commands.find((command) => command.name() === "update")?.options.some((option) => option.long === "--upstream-url")).toBe(true);
175
- expect(upstreams?.commands.find((command) => command.name() === "refresh")?.options.some((option) => option.long === "--auto-models")).toBe(true);
176
- expect(payments?.commands.map((command) => command.name()).sort()).toEqual([
177
- "advertise-mock",
178
- "clear-clawtip",
179
- "disable-mock",
180
- "enable-mock",
181
- "hide-mock",
182
- "list",
183
- "set-clawtip"
184
- ]);
185
- expect(payments?.commands.find((command) => command.name() === "enable-mock")?.options.some((option) => option.long === "--internal")).toBe(true);
186
- });
187
-
188
- test("payments mock commands separate internal and public mock flags", async () => {
189
- const fixture = await startMutableSellerConfigServer({
190
- upstreamUrl: "https://seller.example.test/api",
191
- allowMock: false,
192
- publicMockPayments: true
193
- });
194
- const mgr = new ConfigManager(TEMP_CONF_PATH);
195
- mgr.setProfile("seller", { url: fixture.baseUrl, token: "seller-token" });
196
- const logs = jest.spyOn(console, "log").mockImplementation(() => {});
197
- const errors = jest.spyOn(console, "error").mockImplementation(() => {});
198
- const run = async (...args: string[]) => {
199
- const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
200
- await program.parseAsync(["node", "tb-admin", "--profile", "seller", ...args], { from: "node" });
201
- };
202
-
203
- try {
204
- await run("payments", "enable-mock", "--internal");
205
- expect(fixture.config).toMatchObject({ allowMock: true, publicMockPayments: false });
206
-
207
- await run("payments", "advertise-mock");
208
- expect(fixture.config).toMatchObject({ allowMock: true, publicMockPayments: true });
209
-
210
- await run("payments", "hide-mock");
211
- expect(fixture.config).toMatchObject({ allowMock: true, publicMockPayments: false });
212
-
213
- await run("payments", "disable-mock");
214
- expect(fixture.config).toMatchObject({ allowMock: false, publicMockPayments: false });
215
- expect(fixture.puts).toHaveLength(4);
216
- expect(errors).not.toHaveBeenCalled();
217
- } finally {
218
- logs.mockRestore();
219
- errors.mockRestore();
220
- await fixture.close();
221
- }
222
- });
223
-
224
- test("pricing-monitor commands update seller config flag", async () => {
225
- const fixture = await startMutableSellerConfigServer({
226
- upstreamUrl: "https://seller.example.test/api",
227
- allowMock: false,
228
- pricingDriftMonitorEnabled: false
229
- });
230
- const mgr = new ConfigManager(TEMP_CONF_PATH);
231
- mgr.setProfile("seller", { url: fixture.baseUrl, token: "seller-token" });
232
- const logs = jest.spyOn(console, "log").mockImplementation(() => {});
233
- const errors = jest.spyOn(console, "error").mockImplementation(() => {});
234
- const run = async (...args: string[]) => {
235
- const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
236
- await program.parseAsync(["node", "tb-admin", "--profile", "seller", ...args], { from: "node" });
237
- };
238
-
239
- try {
240
- await run("pricing-monitor", "status");
241
- expect(logs).toHaveBeenLastCalledWith("Pricing drift monitor: disabled");
242
-
243
- await run("pricing-monitor", "enable");
244
- expect(fixture.config.pricingDriftMonitorEnabled).toBe(true);
245
-
246
- await run("pricing-monitor", "disable");
247
- expect(fixture.config.pricingDriftMonitorEnabled).toBe(false);
248
- expect(fixture.puts).toHaveLength(2);
249
- expect(errors).not.toHaveBeenCalled();
250
- } finally {
251
- logs.mockRestore();
252
- errors.mockRestore();
253
- await fixture.close();
254
- }
255
- });
256
-
257
- test("seller Fly commands separate create config from deploy image updates", () => {
258
- const program = buildAdminCli(new ConfigManager(TEMP_CONF_PATH));
259
- const seller = program.commands.find((command) => command.name() === "seller");
260
- const create = seller?.commands.find((command) => command.name() === "create");
261
- const deploy = seller?.commands.find((command) => command.name() === "deploy");
262
-
263
- expect(create).toBeDefined();
264
- expect(deploy).toBeDefined();
265
- expect(create?.options.some((option) => option.long === "--fly-config" && option.required)).toBe(true);
266
- expect(create?.options.some((option) => option.long === "--image" && option.required)).toBe(true);
267
- expect(create?.options.some((option) => option.long === "--initial-config")).toBe(true);
268
- expect(create?.options.some((option) => option.long === "--config")).toBe(false);
269
- expect(deploy?.options.some((option) => option.long === "--fly-config")).toBe(false);
270
- expect(deploy?.options.some((option) => option.long === "--image" && option.required)).toBe(true);
271
- expect(deploy?.options.some((option) => option.long === "--config")).toBe(false);
272
-
273
- const provider = new FlyProvider({
274
- default_image: "registry.fly.io/tb-seller:latest",
275
- default_config: "fly.toml"
276
- });
277
- expect(() => provider.createSeller({
278
- name: "test",
279
- operatorSecret: "secret",
280
- dryRun: true
281
- })).toThrow("seller create requires --image");
282
- expect(() => provider.deploySeller({
283
- app: "tbs-test",
284
- dryRun: true
285
- })).toThrow("seller deploy requires --image");
286
-
287
- expect(provider.deploySeller({
288
- app: "tbs-test",
289
- image: "registry.fly.io/tb-seller:1.0.13",
290
- dryRun: true
291
- })).toContain("Volumes: unchanged");
292
- });
293
-
294
- test("seller create checks the published image before creating Fly resources", () => {
295
- expect(() => requirePublishedDockerImage("registry.fly.io/tb-seller:missing", () => ({
296
- ok: false,
297
- error: "not found"
298
- }))).toThrow("No Fly app was created");
299
-
300
- const commands: string[] = [];
301
- const provider = new FlyProvider(undefined, {
302
- checkFlyctlInstalled: () => true,
303
- imageInspector: () => ({ ok: false, error: "not found" }),
304
- execSync: (command) => {
305
- commands.push(command);
306
- return "";
307
- }
308
- });
309
-
310
- expect(() => provider.createSeller({
311
- name: "tbs-test",
312
- app: "tbs-test",
313
- image: "registry.fly.io/tb-seller:missing",
314
- flyConfig: "deploy/fly.io/fly.tb-seller.toml",
315
- operatorSecret: "operator-secret"
316
- })).toThrow("seller image is not published or is not accessible");
317
-
318
- expect(commands).toEqual([]);
319
- });
320
-
321
- test("FlyProvider passes configured Fly token to flyctl commands", () => {
322
- const envs: Array<string | undefined> = [];
323
- const provider = new FlyProvider({ token: "fly-token-value" }, {
324
- checkFlyctlInstalled: () => true,
325
- imageInspector: () => ({ ok: true }),
326
- execSync: (_command, options) => {
327
- envs.push(options?.env?.FLY_API_TOKEN);
328
- return "";
329
- },
330
- spawnSync: (_command, _args, options) => {
331
- envs.push(options?.env?.FLY_API_TOKEN);
332
- return { status: 0, signal: null, stdout: "", stderr: "", pid: 1, output: [] };
333
- }
334
- });
335
-
336
- provider.createSeller({
337
- name: "tbs-test",
338
- app: "tbs-test",
339
- image: "registry.fly.io/tb-seller:latest",
340
- flyConfig: "deploy/fly.io/fly.tb-seller.toml",
341
- operatorSecret: "operator-secret"
342
- });
343
-
344
- expect(envs).toEqual(["fly-token-value", "fly-token-value", "fly-token-value"]);
345
- });
346
-
347
- test("parseFlyMachineIds reads machine ids and rejects unusable machine lists", () => {
348
- expect(parseFlyMachineIds(JSON.stringify([
349
- { id: "machine-1" },
350
- { id: "machine-2" }
351
- ]), "tbs-test")).toEqual(["machine-1", "machine-2"]);
352
-
353
- expect(() => parseFlyMachineIds("{", "tbs-test")).toThrow("invalid JSON");
354
- expect(() => parseFlyMachineIds("[]", "tbs-test")).toThrow("has no machines");
355
- });
356
-
357
- test("bootstrap registry validation accepts demo config and rejects unsafe variants", () => {
358
- const document = JSON.parse(
359
- fs.readFileSync(path.resolve(__dirname, "../../wallet-bootstrap/seed/demo-sellers.json"), "utf8")
360
- );
361
-
362
- expect(() => validateRegistryDocument(document)).not.toThrow();
363
-
364
- const httpUrl = structuredClone(document);
365
- httpUrl.sellers[0].url = "http://seller.example.com";
366
- expect(() => validateRegistryDocument(httpUrl)).toThrow("url must be https");
367
-
368
- const duplicate = structuredClone(document);
369
- duplicate.sellers[1].id = duplicate.sellers[0].id;
370
- expect(() => validateRegistryDocument(duplicate)).toThrow("duplicate seller id");
371
-
372
- const badDefault = structuredClone(document);
373
- badDefault.defaultSeller = "missing";
374
- expect(() => validateRegistryDocument(badDefault)).toThrow("defaultSeller `missing`");
375
- });
376
-
377
- test("tb-admin ui prefers vendor bootstrap profile when available", () => {
378
- const mgr = new ConfigManager(TEMP_CONF_PATH);
379
- mgr.setProfile("bootstrap", { url: "https://tb-registry.fly.dev", token: "operator-token" });
380
- expect(defaultUiProfile(mgr)).toBe("bootstrap");
381
-
382
- mgr.setProfile("bootstrap-vendor", { url: "https://tb-registry.fly.dev", token: "vendor-token" });
383
- expect(defaultUiProfile(mgr)).toBe("bootstrap-vendor");
384
- });
385
-
386
- test("tb-admin ui server binds loopback, rejects cross-origin APIs, and serves bootstrap data", async () => {
387
- const mgr = new ConfigManager(TEMP_CONF_PATH);
388
- const started = await startAdminUiServer({
389
- host: "127.0.0.1",
390
- port: 0,
391
- openBrowser: false,
392
- configManager: mgr,
393
- url: "https://bootstrap.example.test",
394
- fetchJson: async () => ({
395
- version: 7,
396
- sellers: [{
397
- id: "tbs-sin-06",
398
- name: "tbs-sin-06",
399
- url: "https://seller.example.test",
400
- status: "active",
401
- region: "sin",
402
- supportedProtocols: ["responses"],
403
- paymentMethods: ["clawtip"]
404
- }]
405
- })
406
- });
407
- try {
408
- expect(started.url).not.toContain("session=");
409
- const blocked = await fetch(`${started.url}api/bootstrap`, {
410
- headers: { Origin: "http://malicious.example" }
411
- });
412
- expect(blocked.status).toBe(403);
413
- await expect(blocked.json()).resolves.toMatchObject({ error: "invalid UI origin" });
414
-
415
- const response = await fetch(`${started.url}api/bootstrap`);
416
- await expect(response.json()).resolves.toMatchObject({
417
- status: "available",
418
- registryVersion: 7,
419
- sellerEntries: 1,
420
- regions: ["sin"]
421
- });
422
- } finally {
423
- await new Promise<void>((resolve) => started.server.close(() => resolve()));
424
- }
425
-
426
- await expect(startAdminUiServer({
427
- host: "0.0.0.0",
428
- port: 0,
429
- openBrowser: false,
430
- configManager: mgr
431
- })).rejects.toThrow("loopback");
432
- });
433
-
434
- test("tb-admin ui status refresh updates existing rows without inventory reload", async () => {
435
- const seller = http.createServer((req, res) => {
436
- const url = new URL(req.url || "/", "http://127.0.0.1");
437
- if (req.method === "GET" && url.pathname === "/manifest") {
438
- sendJson(res, { sellerId: "tbs-status-only" });
439
- return;
440
- }
441
- if (req.method === "GET" && url.pathname === "/operator/status") {
442
- expect(req.headers.authorization).toBe("Bearer operator-secret");
443
- sendJson(res, {
444
- status: "healthy",
445
- upstream: { status: "healthy" },
446
- capacity: { activeConnections: 2, maxConnections: 9 },
447
- runtime: { cpuPercent: 8.4, memoryPercent: 41.2, memoryRssMb: 211, memoryLimitMb: 512 },
448
- latency: { ttftMs: 123, avgTokensPerSecond: 42.5, sampleCount: 7 }
449
- });
450
- return;
451
- }
452
- if (req.method === "GET" && url.pathname === "/operator/admin/upstream-balance") {
453
- expect(req.headers.authorization).toBe("Bearer operator-secret");
454
- sendJson(res, {
455
- status: "ok",
456
- balance: {
457
- rawAmount: 12.75,
458
- amountUsdMicros: 12750000,
459
- currency: "USD",
460
- source: "openrouter",
461
- fetchedAt: 1800000000000
462
- }
463
- });
464
- return;
465
- }
466
- res.statusCode = 404;
467
- sendJson(res, { error: "not found" });
468
- });
469
- await new Promise<void>((resolve) => seller.listen(0, "127.0.0.1", () => resolve()));
470
- const address = seller.address();
471
- if (!address || typeof address !== "object") {
472
- throw new Error("seller fixture did not bind a TCP port");
473
- }
474
- const sellerUrl = `http://127.0.0.1:${address.port}`;
475
- const mgr = new ConfigManager(TEMP_CONF_PATH);
476
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
477
- const started = await startAdminUiServer({
478
- host: "127.0.0.1",
479
- port: 0,
480
- openBrowser: false,
481
- configManager: mgr
482
- });
483
- try {
484
- const response = await fetch(`${started.url}api/sellers/status`, {
485
- method: "POST",
486
- headers: { "Content-Type": "application/json" },
487
- body: JSON.stringify({
488
- rows: [{
489
- id: "tbs-status-only",
490
- name: "tbs-status-only",
491
- app: "tbs-status-only",
492
- url: sellerUrl,
493
- registryStatus: "unknown",
494
- nodeStatus: "unknown",
495
- upstreamDomain: "seller.example",
496
- upstreamStatus: "unknown",
497
- upstreamBalanceError: "unauthorized: check upstreamApiKey",
498
- dataSource: "fly",
499
- flyApp: { name: "tbs-status-only", status: "deployed" }
500
- }]
501
- })
502
- });
503
- expect(response.status).toBe(200);
504
- const wrongMethodResponse = await fetch(`${started.url}api/sellers/status`);
505
- expect(wrongMethodResponse.status).toBe(405);
506
- await expect(wrongMethodResponse.json()).resolves.toMatchObject({ error: "method not allowed" });
507
- const rows = await response.json() as any[];
508
- expect(rows).toHaveLength(1);
509
- expect(rows[0]).toMatchObject({
510
- id: "tbs-status-only",
511
- nodeStatus: "active",
512
- upstreamStatus: "healthy",
513
- capacityUsed: 2,
514
- capacityLimit: 9,
515
- resourceCpuPercent: 8.4,
516
- resourceMemoryPercent: 41.2,
517
- ttftMs: 123,
518
- avgTokensPerSecond: 42.5,
519
- latencySamples: 7,
520
- upstreamBalanceUsdMicros: 12750000,
521
- upstreamBalanceCurrency: "USD",
522
- upstreamBalanceSource: "openrouter"
523
- });
524
- expect(rows[0].upstreamBalanceError).toBeUndefined();
525
- } finally {
526
- await new Promise<void>((resolve) => started.server.close(() => resolve()));
527
- await new Promise<void>((resolve) => seller.close(() => resolve()));
528
- }
529
- });
530
-
531
- test("tb-admin ui release requests endpoint proxies the active vendor profile", async () => {
532
- const registry = http.createServer((req, res) => {
533
- const url = new URL(req.url || "/", "http://127.0.0.1");
534
- if (req.method === "GET" && url.pathname === "/platform/release-requests") {
535
- expect(req.headers.authorization).toBe("Bearer vendor-token-value");
536
- expect(url.searchParams.get("limit")).toBe("20");
537
- sendJson(res, {
538
- releaseRequests: [{
539
- id: 42,
540
- status: "pending",
541
- submittedAt: "2026-06-11T00:00:00.000Z",
542
- sellerCount: 1,
543
- note: "fixture release"
544
- }]
545
- });
546
- return;
547
- }
548
- res.statusCode = 404;
549
- sendJson(res, { error: "not found" });
550
- });
551
- await new Promise<void>((resolve) => registry.listen(0, "127.0.0.1", () => resolve()));
552
- const address = registry.address();
553
- if (!address || typeof address !== "object") {
554
- throw new Error("registry fixture did not bind a TCP port");
555
- }
556
-
557
- const mgr = new ConfigManager(TEMP_CONF_PATH);
558
- mgr.setProfile("bootstrap", { url: `http://127.0.0.1:${address.port}`, token: "vendor-token-value" });
559
- const started = await startAdminUiServer({
560
- host: "127.0.0.1",
561
- port: 0,
562
- openBrowser: false,
563
- configManager: mgr,
564
- profile: "bootstrap"
565
- });
566
- try {
567
- const response = await fetch(`${started.url}api/vendor/release-requests`);
568
- expect(response.status).toBe(200);
569
- await expect(response.json()).resolves.toMatchObject({
570
- releaseRequests: [{ id: 42, status: "pending", note: "fixture release" }]
571
- });
572
- } finally {
573
- await new Promise<void>((resolve) => started.server.close(() => resolve()));
574
- await new Promise<void>((resolve) => registry.close(() => resolve()));
575
- }
576
- });
577
-
578
- test("UiActions setRegistryStatus uses vendor-scoped seller list and mutation auth", async () => {
579
- const calls: Array<{ method?: string; path: string; authorization?: string; body?: any }> = [];
580
- const registry = http.createServer((req, res) => {
581
- const url = new URL(req.url || "/", "http://127.0.0.1");
582
- let body = "";
583
- req.on("data", (chunk) => {
584
- body += chunk;
585
- });
586
- req.on("end", () => {
587
- const parsed = body ? JSON.parse(body) : undefined;
588
- calls.push({ method: req.method, path: url.pathname, authorization: req.headers.authorization, body: parsed });
589
- if (req.method === "GET" && url.pathname === "/platform/sellers") {
590
- expect(req.headers.authorization).toBe("Bearer vendor-token-value");
591
- sendJson(res, {
592
- sellers: [{
593
- id: "vendor-live-1",
594
- name: "Vendor Live 1",
595
- app: "tbs-vendor-live-1",
596
- url: "https://tbs-vendor-live-1.fly.dev",
597
- status: "active",
598
- supportedProtocols: ["chat_completions"],
599
- paymentMethods: ["clawtip"]
600
- }]
601
- });
602
- return;
603
- }
604
- if (req.method === "GET" && url.pathname === "/registry/sellers") {
605
- expect(req.headers.authorization).toBeUndefined();
606
- sendJson(res, { version: 12, sellers: [] });
607
- return;
608
- }
609
- if (req.method === "PUT" && url.pathname === "/platform/sellers/vendor-live-1/status") {
610
- expect(req.headers.authorization).toBe("Bearer vendor-token-value");
611
- expect(parsed).toMatchObject({ status: "draining", expectedVersion: 12 });
612
- expect(req.headers["idempotency-key"]).toMatch(/^tb-admin-ui-vendor-live-1-draining-12-/);
613
- sendJson(res, { registry: { version: 13, sellers: [] } });
614
- return;
615
- }
616
- res.statusCode = 404;
617
- sendJson(res, { error: "not found" });
618
- });
619
- });
620
- await new Promise<void>((resolve) => registry.listen(0, "127.0.0.1", () => resolve()));
621
- const address = registry.address();
622
- if (!address || typeof address !== "object") {
623
- throw new Error("registry fixture did not bind a TCP port");
624
- }
625
-
626
- const mgr = new ConfigManager(TEMP_CONF_PATH);
627
- mgr.setProfile("bootstrap", { url: `http://127.0.0.1:${address.port}`, token: "vendor-token-value" });
628
- const actions = new UiActions({ configManager: mgr, profile: "bootstrap" });
629
- try {
630
- const result = await actions.setRegistryStatus("vendor-live-1", "draining");
631
-
632
- expect(result.ok).toBe(true);
633
- expect(result.stdout).toContain("version=13");
634
- expect(calls.map((call) => `${call.method} ${call.path}`)).toEqual([
635
- "GET /platform/sellers",
636
- "GET /registry/sellers",
637
- "PUT /platform/sellers/vendor-live-1/status"
638
- ]);
639
- } finally {
640
- await new Promise<void>((resolve) => registry.close(() => resolve()));
641
- }
642
- });
643
-
644
- test("tb-admin ui create seller returns a progress job", async () => {
645
- const mgr = new ConfigManager(TEMP_CONF_PATH);
646
- mgr.setSellerProvider("fly", { operator_secret: "operator-token-value" });
647
- const calls: string[][] = [];
648
- const started = await startAdminUiServer({
649
- host: "127.0.0.1",
650
- port: 0,
651
- openBrowser: false,
652
- configManager: mgr,
653
- url: "https://bootstrap.example.test",
654
- fetchJson: async (url) => createSellerFetchJson(url),
655
- commandRunner: async (args): Promise<UiActionResult> => {
656
- if (isBootstrapSellersAdd(args)) {
657
- const filePath = args[args.indexOf("--file") + 1];
658
- const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
659
- expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
660
- }
661
- calls.push(args);
662
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
663
- }
664
- });
665
- try {
666
- const response = await fetch(`${started.url}api/sellers`, {
667
- method: "POST",
668
- headers: { "Content-Type": "application/json" },
669
- body: JSON.stringify({
670
- sellerName: "tbs-nrt-07",
671
- app: "tbs-nrt-07",
672
- region: "nrt",
673
- image: "registry.fly.io/tb-seller:latest",
674
- upstreamWebsite: "https://openrouter.ai",
675
- upstreamUrl: "https://openrouter.ai/api/v1",
676
- upstreamApiKey: "fixture-upstream-key",
677
- upstreamBalanceProbeTemplate: "openrouter",
678
- upstreamBalanceProbeUrl: "https://openrouter.ai/api/v1/credits",
679
- upstreamBalanceProbeRechargeUrl: "https://openrouter.ai/settings/credits",
680
- maxConnections: 8,
681
- maxQueueDepth: 4,
682
- markupRatio: 1.2,
683
- discountRatio: 1,
684
- paymentMethods: ["clawtip", "mock"],
685
- clawtipPayTo: "pay-to-seller",
686
- clawtipSm4KeyBase64: "0123456789abcdef012345==",
687
- clawtipSkillSlug: "tokenbuddy-seller",
688
- clawtipSkillId: "si-tokenbuddy-seller",
689
- clawtipDescription: "TokenBuddy Seller",
690
- clawtipResourceUrl: "https://tbs-nrt-07.fly.dev",
691
- clawtipActivationFeeFen: 1,
692
- clawtipMicrosPerFen: 10000,
693
- flyConfig: "deploy/fly.io/fly.tb-seller.toml"
694
- })
695
- });
696
- expect(response.status).toBe(202);
697
- const created = await response.json() as { jobId: string };
698
- expect(created.jobId).toBeTruthy();
699
-
700
- let job: any;
701
- for (let attempt = 0; attempt < 20; attempt += 1) {
702
- const poll = await fetch(`${started.url}api/jobs/${created.jobId}`);
703
- job = await poll.json();
704
- if (job.status !== "running") {
705
- break;
706
- }
707
- await new Promise((resolve) => setTimeout(resolve, 10));
708
- }
709
-
710
- expect(job.status).toBe("succeeded");
711
- expect(job.events.map((event: any) => event.stepId)).toEqual([
712
- "check_registry",
713
- "validate_config",
714
- "create_deployment",
715
- "wait_seller",
716
- "apply_config",
717
- "refresh_models",
718
- "publish_registry"
719
- ]);
720
- expect(job.events.find((event: any) => event.stepId === "validate_config").message).toContain("https://openrouter.ai/api");
721
- expect(JSON.stringify(job)).not.toContain("fixture-upstream-key");
722
- expect(JSON.stringify(job)).not.toContain("0123456789abcdef012345==");
723
- expect(JSON.stringify(job)).not.toContain("operator-token-value");
724
- const commandLines = calls.map((args) => args.join(" "));
725
- expect(commandLines).toEqual([
726
- expect.stringContaining("seller-config validate --file"),
727
- expect.stringContaining("seller create tbs-nrt-07"),
728
- expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value status"),
729
- expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value seller-config put --file"),
730
- expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-token-value upstreams refresh --auto-models"),
731
- expect.stringContaining("bootstrap sellers add --file"),
732
- ]);
733
- expect(commandLines[5]).toContain("--expect-version 7");
734
- expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
735
- } finally {
736
- await new Promise<void>((resolve) => started.server.close(() => resolve()));
737
- }
738
- });
739
-
740
- test("AdminUiState reads seller list from bootstrap registry and masks seller detail API keys", async () => {
741
- const fixture = await startFixtureAdminServer();
742
- try {
743
- const mgr = new ConfigManager(TEMP_CONF_PATH);
744
- mgr.setProfile("bootstrap", { url: fixture.baseUrl, token: "bootstrap-token" });
745
- mgr.setProfile("seller-sin", { url: fixture.baseUrl, token: "seller-token" });
746
-
747
- const state = new AdminUiState({
748
- configManager: mgr,
749
- profile: "bootstrap",
750
- // Step 13 v1.1: 双源. fixture seller tbs-sin-06 在 fly + registry
751
- // 都有, dataSource=both, 走老 4-endpoint merge 路径.
752
- flyApps: async () => [{
753
- name: "tbs-sin-06",
754
- status: "running",
755
- owner: "vendor-a",
756
- raw: {}
757
- }],
758
- flyMachineSpecs: async (appName) => {
759
- expect(appName).toBe("tbs-sin-06");
760
- return {
761
- machines: 1,
762
- runningMachines: 1,
763
- cpuKind: "shared",
764
- cpuCores: 1,
765
- memoryMb: 512,
766
- volumeGb: 1,
767
- regions: ["sin"]
768
- };
769
- },
770
- balanceFetch: async () => {
771
- throw new Error("admin UI must use seller operator balance endpoint before local probing");
772
- }
773
- });
774
- const sellers = await state.sellers();
775
- expect(sellers).toHaveLength(1);
776
- expect(sellers[0]).toMatchObject({
777
- id: "tbs-sin-06",
778
- upstreamDomain: "openrouter.ai",
779
- capacityUsed: 3,
780
- capacityLimit: 8,
781
- ttftMs: 321,
782
- avgInferenceMs: 640,
783
- lastInferenceMs: 700,
784
- latencySamples: 2,
785
- upstreamStatus: "healthy",
786
- upstreamBalanceUsdMicros: 47_950_000,
787
- upstreamBalanceCurrency: "USD",
788
- upstreamBalanceSource: "openrouter"
789
- });
790
- expect(sellers[0].specs).toMatchObject({
791
- machines: 1,
792
- runningMachines: 1,
793
- cpuCores: 1,
794
- memoryMb: 512,
795
- volumeGb: 1
796
- });
797
-
798
- const detail = await state.sellerDetail("tbs-sin-06");
799
- expect(detail.configuration.upstreamApiKeyMasked).toBe("configured");
800
- expect(detail.configuration.upstreamBalance).toBe("USD 47.95");
801
- expect(detail.configuration.upstreamBalanceSource).toBe("openrouter");
802
- expect(detail.configuration.upstreamBalanceProbeTemplate).toBe("openrouter");
803
- expect(detail.configuration.upstreamBalanceProbeUrl).toBe("https://openrouter.ai/api/v1/credits");
804
- expect(detail.configuration.upstreamBalanceProbeRechargeUrl).toBe("https://openrouter.ai/settings/credits");
805
- expect(JSON.stringify(detail)).not.toContain("fixture-live-key");
806
- expect(detail.models[0]).toMatchObject({
807
- upstreamModel: "openai/gpt-5.4",
808
- billingModel: "gpt-5.4"
809
- });
810
- } finally {
811
- await fixture.close();
812
- }
813
- }, ADMIN_UI_STATE_TEST_TIMEOUT_MS);
814
-
815
- test("AdminUiState uses Fly provider operator secret for registry sellers without local profiles", async () => {
816
- const mgr = new ConfigManager(TEMP_CONF_PATH);
817
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
818
- const state = new AdminUiState({
819
- configManager: mgr,
820
- url: "https://bootstrap.example.test",
821
- // Step 13 v1.1: 双源. fixture seller tbs-openrouter-ai-qecae 在 fly + registry
822
- // 都有, dataSource=both, 走老 4-endpoint merge (用 Fly provider secret).
823
- flyApps: async () => [{
824
- name: "tbs-openrouter-ai-qecae",
825
- status: "running",
826
- owner: "vendor-a",
827
- raw: {}
828
- }],
829
- fetchJson: async (url, init) => {
830
- const pathName = new URL(url).pathname;
831
- if (pathName === "/registry/sellers") {
832
- return {
833
- version: 9,
834
- sellers: [{
835
- id: "tbs-openrouter-ai-qecae",
836
- name: "tbs-openrouter-ai-qecae",
837
- app: "tbs-openrouter-ai-qecae",
838
- // Step 13 v1.1: 双源下, 4 个老 endpoint 也用 entry.url. 测试
839
- // 故意用 bootstrap URL, 这样 mock fetchJson 能 catch 全部 4 个
840
- // fetch (manifest probe 仍走 entry.url, 404 时 fallback 走 4-endpoint).
841
- url: "https://bootstrap.example.test",
842
- status: "active",
843
- region: "sin",
844
- modelsCount: 2,
845
- supportedProtocols: ["chat_completions"],
846
- paymentMethods: ["clawtip"]
847
- }]
848
- };
849
- }
850
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
851
- if (pathName === "/operator/status") {
852
- return { status: "healthy", upstream: { status: "healthy" }, capacity: { activeConnections: 1, maxConnections: 8 } };
853
- }
854
- if (pathName === "/operator/admin/service") {
855
- return { sellerId: "node-seller-core", modelsCount: 2, capacity: { maxConnections: 8, maxQueueDepth: 4 } };
856
- }
857
- if (pathName === "/operator/admin/upstreams") {
858
- return {
859
- upstreams: [{
860
- upstreamUrl: "https://openrouter.ai/api",
861
- upstreamApiKey: "****abcd",
862
- models: [{ id: "openai/gpt-5.4" }, { id: "anthropic/claude-opus-4.7" }]
863
- }]
864
- };
865
- }
866
- if (pathName === "/operator/admin/config") {
867
- return {
868
- config: {
869
- upstreamUrl: "https://openrouter.ai/api",
870
- upstreamApiKey: "live-secret-abcd",
871
- upstreamBalanceProbe: { template: "none" },
872
- markupRatio: 1.2,
873
- discountRatio: 1,
874
- maxConnections: 8,
875
- maxQueueDepth: 4
876
- }
877
- };
878
- }
879
- throw new Error(`unexpected fetch url ${url}`);
880
- }
881
- });
882
-
883
- const sellers = await state.sellers();
884
- expect(sellers[0]).toMatchObject({
885
- id: "tbs-openrouter-ai-qecae",
886
- name: "tbs-openrouter-ai-qecae",
887
- nodeStatus: "active",
888
- upstreamDomain: "openrouter.ai",
889
- profile: "tbs-openrouter-ai-qecae",
890
- modelsCount: 2
891
- });
892
-
893
- const detail = await state.sellerDetail("tbs-openrouter-ai-qecae");
894
- expect(detail.configuration.upstreamUrl).toBe("https://openrouter.ai/api");
895
- expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** abcd");
896
- expect(detail.models.map((model) => model.upstreamModel)).toEqual(["openai/gpt-5.4", "anthropic/claude-opus-4.7"]);
897
- }, ADMIN_UI_STATE_TEST_TIMEOUT_MS);
898
-
899
- test("AdminUiState loads Fly seller detail when vendor registry auth fails", async () => {
900
- const mgr = new ConfigManager(TEMP_CONF_PATH);
901
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
902
- const state = new AdminUiState({
903
- configManager: mgr,
904
- url: "https://bootstrap.example.test",
905
- flyApps: async () => [{
906
- name: "tbs-flyonly-node",
907
- status: "running",
908
- owner: "vendor-a",
909
- raw: {}
910
- }],
911
- fetchJson: async (url, init) => {
912
- const pathName = new URL(url).pathname;
913
- if (pathName === "/platform/sellers") {
914
- throw new Error('HTTP Error 401: {"error":"vendor_auth_failed"}');
915
- }
916
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
917
- if (pathName === "/operator/status") {
918
- return { status: "healthy", upstream: { status: "healthy" }, capacity: { activeConnections: 2, maxConnections: 6 } };
919
- }
920
- if (pathName === "/operator/admin/service") {
921
- return { sellerId: "tbs-flyonly-node", modelsCount: 1, capacity: { maxConnections: 6, maxQueueDepth: 3 } };
922
- }
923
- if (pathName === "/operator/admin/upstreams") {
924
- return {
925
- upstreams: [{
926
- upstreamUrl: "https://api.moonshot.cn/v1",
927
- models: [{ id: "moonshot-v1-8k" }]
928
- }]
929
- };
930
- }
931
- if (pathName === "/operator/admin/config") {
932
- return {
933
- config: {
934
- upstreamUrl: "https://api.moonshot.cn/v1",
935
- upstreamApiKey: "moonshot-live-secret",
936
- upstreamBalanceProbe: { template: "none" },
937
- maxConnections: 6,
938
- maxQueueDepth: 3
939
- }
940
- };
941
- }
942
- throw new Error(`unexpected fetch url ${url}`);
943
- }
944
- });
945
-
946
- const detail = await state.sellerDetail("tbs-flyonly-node");
947
-
948
- expect(detail.row).toMatchObject({
949
- id: "tbs-flyonly-node",
950
- dataSource: "fly",
951
- nodeStatus: "active",
952
- upstreamDomain: "api.moonshot.cn",
953
- capacityUsed: 2,
954
- capacityLimit: 6
955
- });
956
- expect(detail.configuration.upstreamApiKeyMasked).toBe("**** **** **** cret");
957
- expect(detail.models.map((model) => model.upstreamModel)).toEqual(["moonshot-v1-8k"]);
958
- }, ADMIN_UI_STATE_TEST_TIMEOUT_MS);
959
-
960
- test("UiActions updates new registry seller config without a local profile", async () => {
961
- const mgr = new ConfigManager(TEMP_CONF_PATH);
962
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
963
- const calls: string[][] = [];
964
- const actions = new UiActions({
965
- configManager: mgr,
966
- url: "https://bootstrap.example.test",
967
- fetchJson: async (url, init) => {
968
- const pathName = new URL(url).pathname;
969
- if (pathName === "/registry/sellers") {
970
- return {
971
- version: 10,
972
- sellers: [{
973
- id: "tbs-openrouter-ai-qecae",
974
- name: "tbs-openrouter-ai-qecae",
975
- app: "tbs-openrouter-ai-qecae",
976
- url: "https://tbs-openrouter-ai-qecae.fly.dev",
977
- status: "active",
978
- supportedProtocols: ["chat_completions"],
979
- paymentMethods: ["clawtip"]
980
- }]
981
- };
982
- }
983
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
984
- if (pathName === "/operator/admin/config") {
985
- return {
986
- config: {
987
- upstreamUrl: "https://openrouter.ai/api",
988
- upstreamApiKey: "live-secret-abcd",
989
- maxConnections: 8,
990
- maxQueueDepth: 4
991
- }
992
- };
993
- }
994
- throw new Error(`unexpected fetch url ${url}`);
995
- },
996
- commandRunner: async (args): Promise<UiActionResult> => {
997
- calls.push(args);
998
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
999
- }
1000
- });
1001
-
1002
- const result = await actions.updateSellerConfig("tbs-openrouter-ai-qecae", { maxConnections: 12 });
1003
-
1004
- expect(result.ok).toBe(true);
1005
- expect(calls.map((args) => args.join(" "))).toEqual([
1006
- expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config validate --file"),
1007
- expect.stringContaining("--url https://tbs-openrouter-ai-qecae.fly.dev --token operator-secret seller-config put --file")
1008
- ]);
1009
- });
1010
-
1011
- test("UiActions updates Fly seller config when vendor registry auth fails", async () => {
1012
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1013
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1014
- const calls: string[][] = [];
1015
- const fetchPaths: string[] = [];
1016
- const actions = new UiActions({
1017
- configManager: mgr,
1018
- url: "https://bootstrap.example.test",
1019
- token: "vendor-token",
1020
- flyApps: async () => [{
1021
- name: "tbs-flyonly-node",
1022
- status: "running",
1023
- owner: "vendor-a",
1024
- raw: {}
1025
- }],
1026
- fetchJson: async (url, init) => {
1027
- const pathName = new URL(url).pathname;
1028
- fetchPaths.push(pathName);
1029
- if (pathName === "/platform/sellers") {
1030
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer vendor-token");
1031
- throw new Error('HTTP Error 401: {"error":"vendor_auth_failed"}');
1032
- }
1033
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer operator-secret");
1034
- if (pathName === "/operator/admin/config") {
1035
- return {
1036
- config: {
1037
- upstreamUrl: "https://api.moonshot.cn/v1",
1038
- upstreamApiKey: "live-secret-abcd",
1039
- maxConnections: 6,
1040
- maxQueueDepth: 3
1041
- }
1042
- };
1043
- }
1044
- throw new Error(`unexpected fetch url ${url}`);
1045
- },
1046
- commandRunner: async (args): Promise<UiActionResult> => {
1047
- calls.push(args);
1048
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1049
- }
1050
- });
1051
-
1052
- const result = await actions.updateSellerConfig("tbs-flyonly-node", { maxConnections: 9 });
1053
-
1054
- expect(result.ok).toBe(true);
1055
- expect(fetchPaths).toEqual([
1056
- "/platform/sellers",
1057
- "/registry/sellers",
1058
- "/operator/admin/config"
1059
- ]);
1060
- expect(calls.map((args) => args.join(" "))).toEqual([
1061
- expect.stringContaining("--url https://tbs-flyonly-node.fly.dev --token operator-secret seller-config validate --file"),
1062
- expect.stringContaining("--url https://tbs-flyonly-node.fly.dev --token operator-secret seller-config put --file")
1063
- ]);
1064
- });
1065
-
1066
- test("admin UI seller rows render missing telemetry without unknown data labels", () => {
1067
- const html = adminUiHtml();
1068
- expect(html).toContain("class=\"spinner\"");
1069
- expect(html).toContain("@keyframes spin");
1070
- expect(html).toContain("role=\"status\" aria-label=\"Loading sellers\"");
1071
- expect(html).toContain("<th>Enable</th>");
1072
- expect(html).toContain("data-model-enabled");
1073
- expect(html).toContain("id=\"createStatus\" class=\"status-line hidden\"");
1074
- expect(html).toContain("id=\"createProgress\"");
1075
- expect(html).toContain("create-progress");
1076
- expect(html).toContain("progress-step");
1077
- expect(html).toContain("progress-log");
1078
- expect(html).toContain("currentCreateJob");
1079
- expect(html).toContain("setCreateFormDisabled(true)");
1080
- expect(html).toContain("setCreateFormDisabled(false)");
1081
- expect(html).toContain("Retry create");
1082
- expect(html).toContain("data-progress-step");
1083
- expect(html).toContain("aria-expanded");
1084
- expect(html).toContain("progress-title");
1085
- expect(html).toContain("Show details");
1086
- expect(html).toContain("Hide details");
1087
- expect(html).toContain("/api/jobs/");
1088
- expect(html).toContain("function uiErrorMessage");
1089
- expect(html).toContain("Admin UI connection lost. Restart tb-admin ui and reload this page.");
1090
- expect(html).toContain("Admin profile authentication failed. Check the configured operator token.");
1091
- expect(html).toContain("renderCreateJob");
1092
- expect(html).toContain("pollCreateJob");
1093
- expect(html).toContain("Created and added to bootstrap registry.");
1094
- expect(html).toContain("upstreamUrl:\"https://openrouter.ai/api/v1\"");
1095
- expect(html).toContain("loadingSpinner(\"Loading registry\")");
1096
- expect(html).toContain('id="detailGrid" class="detail-grid hidden"');
1097
- expect(html).toContain('showDetailStatus("Loading seller data", true)');
1098
- expect(html).toContain('document.getElementById("detailGrid").classList.add("hidden")');
1099
- expect(html).toContain('document.getElementById("detailGrid").classList.remove("hidden")');
1100
- expect(html).not.toContain("loadingSpinner(\"Loading configuration\")");
1101
- expect(html).not.toContain("loadingSpinner(\"Loading models\")");
1102
- expect(html).toContain("sellerStatusRefreshIntervalMs = 30000");
1103
- expect(html).toContain("/api/sellers/status");
1104
- expect(html).toContain("function pumpSellerDetailQueue()");
1105
- expect(html).toContain("function statusRefreshRow(row)");
1106
- expect(html).toContain("function scheduleSellerDetailRefresh(row)");
1107
- expect(html).toContain("const nextRefreshAt = new Date(Date.now() + sellerStatusRefreshIntervalMs).toISOString()");
1108
- expect(html).toContain("sellerDetailTimers.set(key, timer)");
1109
- expect(html).toContain("Refreshing details");
1110
- expect(html).not.toContain("sellerLastUpdated");
1111
- expect(html).not.toContain("Last updated:");
1112
- expect(html).not.toContain("s ago");
1113
- expect(html).toContain("sellerClockTimer = setInterval(() => { updateSellerRefreshMeta(); updateSellerRowRefreshCountdowns(); }, 1000)");
1114
- expect(html).toContain("function renderSellerRows(rows)");
1115
- expect(html).toContain("sellerRefreshLoaded = true");
1116
- expect(html).toContain("readonly-value");
1117
- expect(html).toContain("--seller-grid");
1118
- expect(html).toContain("grid-template-columns:var(--seller-grid)");
1119
- expect(html).toContain("#sellerRows{display:grid;gap:8px;width:100%;min-width:0}");
1120
- expect(html).toContain("gap:8px;width:100%;min-width:0");
1121
- expect(html).toContain("detailFieldsHtml");
1122
- expect(html).toContain("data-original");
1123
- expect(html).toContain('id="closeDetail" class="modal-close"');
1124
- expect(html).toContain('aria-label="Close detail"');
1125
- expect(html).not.toContain('id="closeDetail" class="btn">Close</button>');
1126
- expect(html).toContain("function registryStatusForAction(action)");
1127
- expect(html).toContain("function patchSellerRegistryStatus(id, status)");
1128
- expect(html).toContain('showDetailStatus("Updating registry status", true)');
1129
- expect(html).toContain("patchSellerRegistryStatus(id, status)");
1130
- expect(html).toContain('document.getElementById("detailModal").classList.remove("open")');
1131
- expect(html).not.toContain("Set \"+currentDetail.row.name+\" registry status via");
1132
- expect(html).toContain("function registryStatusDisplay(status)");
1133
- expect(html).toContain('["registryStatus", registryStatusDisplay(c.registryStatus || d.row.registryStatus)]');
1134
- expect(html).toContain("function setDetailSavingBusy(busy)");
1135
- expect(html).toContain('edit.textContent = busy ? "Saving" : (editing ? "Save changes" : "Edit config")');
1136
- expect(html).toContain('showDetailStatus("Saving seller config", true)');
1137
- expect(html).toContain('if (!result.ok) throw new Error(result.stderr || "Save failed")');
1138
- expect(html).toContain("editing = false; renderDetail(); loadSellers();");
1139
- expect(html).toContain('catch (err) { showDetailStatus(err.message || "Save failed", false); }');
1140
- expect(html).not.toContain('showDetailStatus(result.ok ? "Saved"');
1141
- // Design-spec header labels keep monitoring fields visible.
1142
- expect(html).toContain(">Connection</span>");
1143
- expect(html).toContain(">Resources</span>");
1144
- expect(html).toContain(">Balance</span>");
1145
- expect(html).toContain(">TTFT</span>");
1146
- expect(html).toContain(">Status</span>");
1147
- expect(html).toContain("function upstreamStatusTone(status)");
1148
- expect(html).toContain("Upstream status from seller /operator/status");
1149
- expect(html).not.toContain(">Latency</span>");
1150
- expect(html).not.toContain(">Discount</span>");
1151
- // Spec-compliant token formats
1152
- expect(html).toContain("tok/s");
1153
- expect(html).toContain("AVG speed");
1154
- expect(html).toContain("Samples");
1155
- // Spec-compliant unknown-value char (em dash) lives in formatter
1156
- expect(html).toContain("UNKNOWN_VALUE");
1157
- expect(html).toContain("formatDuration");
1158
- expect(html).toContain("formatSellerStatus");
1159
- expect(html).toContain("formatDiscountRatio");
1160
- expect(html).toContain("formatBalanceAmount");
1161
- expect(html).not.toContain("status-badge");
1162
- expect(html).not.toContain("balance-badge");
1163
- expect(html).not.toContain("probe blocked");
1164
- expect(html).toContain("runningMachines");
1165
- expect(html).toContain("memoryMb");
1166
- expect(html).toContain("resourceCpuPercent");
1167
- expect(html).toContain("resourceMemoryPercent");
1168
- // No glassmorphism per spec
1169
- expect(html).not.toContain("backdrop-filter");
1170
- // Detail field references
1171
- expect(html).toContain("avgTokensPerSecond");
1172
- expect(html).toContain("lastTokensPerSecond");
1173
- expect(html).toContain("lastInferenceMs");
1174
- expect(html).toContain("upstreamBalanceSource");
1175
- expect(html).toContain("upstreamBalanceFetchedAt");
1176
- expect(html).toContain("upstreamBalanceError");
1177
- expect(html).toContain("upstreamBalanceProbeTemplate");
1178
- expect(html).toContain("upstreamBalanceProbeUrl");
1179
- expect(html).toContain("upstreamBalanceProbeUserId");
1180
- expect(html).toContain("upstreamBalanceProbeRechargeUrl");
1181
- // Payment tabs
1182
- expect(html).toContain("paymentMethods");
1183
- expect(html).toContain("payment-tabs");
1184
- expect(html).toContain("payment-tab");
1185
- expect(html).toContain("payment-panel");
1186
- expect(html).toContain("pill-switch");
1187
- expect(html).toContain("data-payment-tab");
1188
- expect(html).toContain("data-payment-panel");
1189
- expect(html).toContain("data-payment-toggle");
1190
- expect(html).toContain("enabledPaymentMethods()");
1191
- expect(html).toContain("setupPaymentTabs()");
1192
- expect(html).toContain("togglePaymentMethod");
1193
- expect(html).toContain("selectPaymentTab");
1194
- expect(html).toContain("updatePaymentPanels");
1195
- expect(html).toContain("data-enabled");
1196
- expect(html).toContain("aria-pressed");
1197
- expect(html).toContain("启用即可使用 mock 支付方式,无需额外参数");
1198
- expect(html).toContain("required-star");
1199
- expect(html).toContain("Seller name");
1200
- expect(html).toContain("tbs-<seller-name>-<random>");
1201
- expect(html).toContain("Fly app name");
1202
- expect(html).toContain("Seller image");
1203
- expect(html).toContain("Fly config file");
1204
- expect(html).toContain("Balance probe URL (auto from template)");
1205
- expect(html).toContain("Recharge URL");
1206
- expect(html).toContain("randomAppSuffix");
1207
- expect(html).toContain("updateGeneratedCreateFields");
1208
- expect(html).toContain("appNameFromSellerName");
1209
- expect(html).toContain("balanceProbeUrlForTemplate");
1210
- expect(html).toContain("data-generated-summary=\"clawtip\"");
1211
- expect(html).toContain("clawtipPayTo");
1212
- expect(html).toContain("clawtipSm4KeyBase64");
1213
- expect(html).toContain("clawtipSkillSlug");
1214
- expect(html).toContain("clawtipSkillId");
1215
- expect(html).toContain("clawtipDescription");
1216
- expect(html).toContain("clawtipResourceUrl");
1217
- expect(html).toContain("clawtipActivationFeeFen");
1218
- expect(html).toContain("clawtipMicrosPerFen");
1219
- // Spec uses — (em dash) for unknown, not "not reported" or "n/a"
1220
- expect(html).not.toContain("not reported");
1221
- expect(html).not.toContain("\"n/a\"");
1222
- expect(html).toContain("function usagePercent(value)");
1223
- expect(html).toContain("usage CPU ");
1224
- expect(html).toContain('const resourcePrimary = (cpuUsageText === "—" ? "-" : cpuUsageText) + "/" + (memoryUsageText === "—" ? "-" : memoryUsageText)');
1225
- expect(html).not.toContain(">i</span>");
1226
- expect(html).not.toContain(">×</button>");
1227
- expect(html).not.toContain("Loading sellers...");
1228
- expect(html).not.toContain("Loading seller data...");
1229
- expect(html).not.toContain("Loading configuration...");
1230
- expect(html).not.toContain("Loading models...");
1231
- expect(html).not.toContain("<div id=\"createStatus\" class=\"status-line\">Ready</div>");
1232
- expect(html).not.toContain("not set");
1233
- expect(html).not.toContain(" multiple");
1234
- });
1235
-
1236
- test("UiActions validates then puts seller config without shell command strings", async () => {
1237
- const fixture = await startFixtureAdminServer();
1238
- try {
1239
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1240
- mgr.setProfile("bootstrap", { url: fixture.baseUrl, token: "bootstrap-token" });
1241
- mgr.setProfile("seller-sin", { url: fixture.baseUrl, token: "seller-token" });
1242
- const calls: string[][] = [];
1243
- const actions = new UiActions({
1244
- configManager: mgr,
1245
- profile: "bootstrap",
1246
- flyApps: async () => [],
1247
- commandRunner: async (args): Promise<UiActionResult> => {
1248
- calls.push(args);
1249
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1250
- }
1251
- });
1252
-
1253
- const result = await actions.updateSellerConfig("tbs-sin-06", {
1254
- markupRatio: 1.4,
1255
- upstreamApiKey: "new-secret"
1256
- });
1257
-
1258
- expect(result.ok).toBe(true);
1259
- expect(calls).toHaveLength(2);
1260
- expect(calls[0]).toContain("validate");
1261
- expect(calls[1]).toContain("put");
1262
- expect(calls.flat()).not.toContain("seller-config validate");
1263
- expect(calls[0]).toContain("--profile");
1264
- expect(calls[0]).toContain("seller-sin");
1265
- } finally {
1266
- await fixture.close();
1267
- }
1268
- });
1269
-
1270
- test("probeUpstreamBalance parses OpenRouter balance and caches failed probes", async () => {
1271
- const cache = new BalanceProbeCache();
1272
- let calls = 0;
1273
- const openRouter = await probeUpstreamBalance({
1274
- upstreamUrl: "https://openrouter.ai/api",
1275
- upstreamBalanceUrl: "https://openrouter.ai/api/v1/credits",
1276
- upstreamApiKey: "fixture-key"
1277
- }, {
1278
- now: () => 1000,
1279
- cache,
1280
- fetch: async (url, init) => {
1281
- calls += 1;
1282
- expect(String(url)).toBe("https://openrouter.ai/api/v1/credits");
1283
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer fixture-key");
1284
- return jsonResponse({ data: { total_credits: 50, total_usage: 2.05 } });
1285
- }
1286
- });
1287
-
1288
- expect(openRouter).toMatchObject({
1289
- source: "openrouter",
1290
- rawAmount: 47.95,
1291
- amountUsdMicros: 47950000,
1292
- currency: "USD"
1293
- });
1294
- expect(calls).toBe(1);
1295
-
1296
- const firstFailure = await probeUpstreamBalance({
1297
- upstreamUrl: "https://custom-upstream.example/v1",
1298
- upstreamBalanceUrl: "https://custom-upstream.example/api/user/quota",
1299
- upstreamApiKey: "fixture-key"
1300
- }, {
1301
- now: () => 2000,
1302
- cache,
1303
- fetch: async () => {
1304
- calls += 1;
1305
- return jsonResponse({ success: false });
1306
- }
1307
- });
1308
- const cachedFailure = await probeUpstreamBalance({
1309
- upstreamUrl: "https://custom-upstream.example/v1",
1310
- upstreamBalanceUrl: "https://custom-upstream.example/api/user/quota",
1311
- upstreamApiKey: "fixture-key"
1312
- }, {
1313
- now: () => 3000,
1314
- cache,
1315
- fetch: async () => {
1316
- throw new Error("cache miss");
1317
- }
1318
- });
1319
-
1320
- expect(firstFailure.error?.message).toBe("missing upstreamUserId for newapi upstream");
1321
- expect(cachedFailure).toBe(firstFailure);
1322
- expect(calls).toBe(1);
1323
- });
1324
-
1325
- test("probeUpstreamBalance sends New-Api-User for generic newapi balances", async () => {
1326
- const snapshot = await probeUpstreamBalance({
1327
- upstreamUrl: "https://custom-upstream.example/v1",
1328
- upstreamBalanceUrl: "https://custom-upstream.example/api/user/quota",
1329
- upstreamApiKey: "fixture-key",
1330
- upstreamUserId: "12345"
1331
- }, {
1332
- now: () => 4000,
1333
- fetch: async (url, init) => {
1334
- expect(String(url)).toBe("https://custom-upstream.example/api/user/quota");
1335
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer fixture-key");
1336
- expect((init?.headers as Record<string, string>)?.["New-Api-User"]).toBe("12345");
1337
- return jsonResponse({ success: true, data: { quota: 50000000, used_quota: 1250000 } });
1338
- }
1339
- });
1340
-
1341
- expect(snapshot).toMatchObject({
1342
- source: "newapi_generic",
1343
- rawAmount: 97.5,
1344
- amountUsdMicros: 97500000,
1345
- currency: "USD"
1346
- });
1347
- });
1348
-
1349
- test("probeUpstreamBalance parses generic /v1/usage balances without user id", async () => {
1350
- const snapshot = await probeUpstreamBalance({
1351
- upstreamUrl: "https://code.shoestravel.xin",
1352
- upstreamApiKey: "fixture-key"
1353
- }, {
1354
- now: () => 5000,
1355
- fetch: async (url, init) => {
1356
- expect(String(url)).toBe("https://code.shoestravel.xin/v1/usage");
1357
- expect((init?.headers as Record<string, string>)?.Authorization).toBe("Bearer fixture-key");
1358
- expect((init?.headers as Record<string, string>)?.["New-Api-User"]).toBeUndefined();
1359
- return jsonResponse({
1360
- balance: 37.96221984,
1361
- isValid: true,
1362
- remaining: 37.96221984,
1363
- unit: "USD"
1364
- });
1365
- }
1366
- });
1367
-
1368
- expect(snapshot).toMatchObject({
1369
- source: "usage_generic",
1370
- rawAmount: 37.96221984,
1371
- amountUsdMicros: 37962220,
1372
- currency: "USD"
1373
- });
1374
- });
1375
-
1376
- test("probeUpstreamBalance honors explicit balance probe templates", async () => {
1377
- const newApi = await probeUpstreamBalance({
1378
- upstreamUrl: "https://custom-upstream.example/v1",
1379
- upstreamApiKey: "fixture-key",
1380
- upstreamBalanceProbe: {
1381
- template: "newapi_generic",
1382
- url: "https://custom-upstream.example/api/user/self",
1383
- userId: "67890"
1384
- }
1385
- }, {
1386
- now: () => 7000,
1387
- fetch: async (url, init) => {
1388
- expect(String(url)).toBe("https://custom-upstream.example/api/user/self");
1389
- expect((init?.headers as Record<string, string>)?.["New-Api-User"]).toBe("67890");
1390
- return jsonResponse({ success: true, data: { quota: 1000000, used_quota: 500000 } });
1391
- }
1392
- });
1393
-
1394
- expect(newApi).toMatchObject({
1395
- source: "newapi_generic",
1396
- rawAmount: 1,
1397
- amountUsdMicros: 1000000,
1398
- currency: "USD"
1399
- });
1400
-
1401
- const usage = await probeUpstreamBalance({
1402
- upstreamUrl: "https://code.shoestravel.xin/v1",
1403
- upstreamApiKey: "fixture-key",
1404
- upstreamBalanceProbe: {
1405
- template: "usage_generic"
1406
- }
1407
- }, {
1408
- now: () => 8000,
1409
- fetch: async (url) => {
1410
- expect(String(url)).toBe("https://code.shoestravel.xin/v1/usage");
1411
- return jsonResponse({ remaining: 37.96221984, unit: "USD", isValid: true });
1412
- }
1413
- });
1414
-
1415
- expect(usage).toMatchObject({
1416
- source: "usage_generic",
1417
- rawAmount: 37.96221984,
1418
- amountUsdMicros: 37962220,
1419
- currency: "USD"
1420
- });
1421
- });
1422
-
1423
- test("probeUpstreamBalance reports inactive generic /v1/usage keys", async () => {
1424
- const snapshot = await probeUpstreamBalance({
1425
- upstreamUrl: "https://custom-upstream.example/v1",
1426
- upstreamBalanceUrl: "https://custom-upstream.example/v1/usage",
1427
- upstreamApiKey: "fixture-key"
1428
- }, {
1429
- now: () => 6000,
1430
- fetch: async () => jsonResponse({
1431
- remaining: 12.5,
1432
- unit: "USD",
1433
- isValid: false
1434
- })
1435
- });
1436
-
1437
- expect(snapshot).toMatchObject({
1438
- source: "usage_generic",
1439
- rawAmount: 12.5,
1440
- amountUsdMicros: 12500000,
1441
- currency: "USD",
1442
- error: {
1443
- httpStatus: 200,
1444
- message: "upstream key is not active"
1445
- }
1446
- });
1447
- });
1448
-
1449
- test("UiActions create validates, creates, then applies initial seller config", async () => {
1450
- const fixture = await startFixtureAdminServer();
1451
- try {
1452
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1453
- mgr.setProfile("bootstrap", { url: fixture.baseUrl, token: "bootstrap-token" });
1454
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1455
- const calls: string[][] = [];
1456
- const actions = new UiActions({
1457
- configManager: mgr,
1458
- configPath: TEMP_CONF_PATH,
1459
- profile: "bootstrap",
1460
- fetchJson: async (url) => createSellerFetchJson(url),
1461
- commandRunner: async (args): Promise<UiActionResult> => {
1462
- if (isBootstrapSellersAdd(args)) {
1463
- const filePath = args[args.indexOf("--file") + 1];
1464
- const entry = JSON.parse(fs.readFileSync(filePath, "utf8")) as { id: string; status?: string };
1465
- expect(entry).toMatchObject({ id: "tbs-nrt-07", status: "active" });
1466
- }
1467
- calls.push(args);
1468
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1469
- }
1470
- });
1471
-
1472
- const response = await actions.createSeller({
1473
- sellerName: "tbs-nrt-07",
1474
- app: "tbs-nrt-07",
1475
- region: "nrt",
1476
- image: "registry.fly.io/tb-seller:latest",
1477
- upstreamWebsite: "https://openrouter.ai",
1478
- upstreamUrl: "https://openrouter.ai/api/v1",
1479
- upstreamApiKey: "fixture-upstream-key",
1480
- upstreamBalanceProbeTemplate: "usage_generic",
1481
- upstreamBalanceProbeUrl: "https://code.shoestravel.xin/v1/usage",
1482
- upstreamBalanceProbeRechargeUrl: "https://code.shoestravel.xin/topup",
1483
- maxConnections: 8,
1484
- maxQueueDepth: 4,
1485
- markupRatio: 1.2,
1486
- discountRatio: 1,
1487
- paymentMethods: ["clawtip", "mock"],
1488
- clawtipPayTo: "pay-to-seller",
1489
- clawtipSm4KeyBase64: "0123456789abcdef012345==",
1490
- clawtipSkillSlug: "tokenbuddy-seller",
1491
- clawtipSkillId: "si-tokenbuddy-seller",
1492
- clawtipDescription: "TokenBuddy Seller",
1493
- clawtipResourceUrl: "https://tbs-nrt-07.fly.dev",
1494
- clawtipActivationFeeFen: 1,
1495
- clawtipMicrosPerFen: 10000,
1496
- flyConfig: "deploy/fly.io/fly.tb-seller.toml"
1497
- });
1498
-
1499
- expect(response.result.ok).toBe(true);
1500
- expect(response.configPut?.ok).toBe(true);
1501
- expect(response.modelsRefresh?.ok).toBe(true);
1502
- expect(response.registryPublish?.ok).toBe(true);
1503
- expect(response.publishRegistry).toBe("completed");
1504
- expect(response.configPreview.upstreamUrl).toBe("https://openrouter.ai/api");
1505
- expect(response.configPreview.sellerId).toBe("tbs-nrt-07");
1506
- expect(response.configPreview.manifestVersion).toBe("manifest.v1");
1507
- expect(response.configPreview.upstreamBalanceProbe).toEqual({
1508
- template: "usage_generic",
1509
- url: "https://code.shoestravel.xin/v1/usage",
1510
- userId: undefined,
1511
- rechargeUrl: "https://code.shoestravel.xin/topup"
1512
- });
1513
- expect(response.configPreview.upstreamBalanceUrl).toBe("https://code.shoestravel.xin/v1/usage");
1514
- expect(response.configPreview.upstreamRechargeUrl).toBe("https://code.shoestravel.xin/topup");
1515
- expect(response.configPreview.allowMock).toBe(true);
1516
- expect(response.configPreview.clawtip).toEqual({
1517
- payTo: "pay-to-seller",
1518
- sm4KeyBase64: "********",
1519
- skillSlug: "tokenbuddy-seller",
1520
- skillId: "si-tokenbuddy-seller",
1521
- description: "TokenBuddy Seller",
1522
- resourceUrl: "https://tbs-nrt-07.fly.dev",
1523
- activationFeeFen: 1,
1524
- microsPerFen: 10000
1525
- });
1526
- const commandLines = calls.map((args) => args.join(" "));
1527
- expect(commandLines).toEqual([
1528
- expect.stringContaining("seller-config validate --file"),
1529
- expect.stringContaining("seller create tbs-nrt-07"),
1530
- expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret status"),
1531
- expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret seller-config put --file"),
1532
- expect.stringContaining("--url https://tbs-nrt-07.fly.dev --token operator-secret upstreams refresh --auto-models"),
1533
- expect.stringContaining("bootstrap sellers add --file")
1534
- ]);
1535
- expect(commandLines[5]).toContain("--expect-version 7");
1536
- expect(commandLines.find((line) => line.includes("seller create tbs-nrt-07"))).toContain("--initial-config");
1537
- expect(mgr.getProfile("tbs-nrt-07")).toEqual({
1538
- url: "https://tbs-nrt-07.fly.dev",
1539
- token: "operator-secret"
1540
- });
1541
- } finally {
1542
- await fixture.close();
1543
- }
1544
- });
1545
-
1546
- test("UiActions create retries transient upstream model refresh failures", async () => {
1547
- const fixture = await startFixtureAdminServer();
1548
- try {
1549
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1550
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1551
- const registry = await createSellerFetchJson(`${fixture.baseUrl}/registry/sellers`);
1552
- let refreshAttempts = 0;
1553
- const calls: string[][] = [];
1554
- const actions = new UiActions({
1555
- configManager: mgr,
1556
- url: fixture.baseUrl,
1557
- fetchJson: async (url) => {
1558
- const pathName = new URL(url).pathname;
1559
- if (pathName === "/registry/sellers") {
1560
- return registry;
1561
- }
1562
- return createSellerFetchJson(url);
1563
- },
1564
- commandRunner: async (args): Promise<UiActionResult> => {
1565
- calls.push(args);
1566
- if (args.includes("upstreams") && args.includes("refresh")) {
1567
- refreshAttempts += 1;
1568
- if (refreshAttempts < 3) {
1569
- return { ok: false, stdout: "", stderr: "Error: Connection failed: fetch failed", command: ["node", "tb-admin", ...args] };
1570
- }
1571
- }
1572
- if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
1573
- const filePath = args[args.indexOf("--file") + 1];
1574
- const doc = JSON.parse(fs.readFileSync(filePath, "utf8"));
1575
- expect(doc).toMatchObject({ id: "tbs-nrt-07" });
1576
- expect(doc.models).toHaveLength(2);
1577
- }
1578
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1579
- }
1580
- });
1581
-
1582
- const response = await actions.createSeller({
1583
- sellerName: "tbs-nrt-07",
1584
- app: "tbs-nrt-07",
1585
- region: "nrt",
1586
- image: "registry.fly.io/tb-seller:latest",
1587
- upstreamWebsite: "https://openrouter.ai",
1588
- upstreamUrl: "https://openrouter.ai/api/v1",
1589
- upstreamApiKey: "fixture-upstream-key",
1590
- upstreamBalanceProbeTemplate: "none",
1591
- maxConnections: 8,
1592
- maxQueueDepth: 4,
1593
- markupRatio: 1.2,
1594
- discountRatio: 1,
1595
- paymentMethods: ["mock"],
1596
- flyConfig: "deploy/fly.io/fly.tb-seller.toml"
1597
- });
1598
-
1599
- expect(response.modelsRefresh?.ok).toBe(true);
1600
- expect(response.registryPublish?.ok).toBe(true);
1601
- expect(refreshAttempts).toBe(3);
1602
- expect(calls.filter((args) => args.includes("upstreams") && args.includes("refresh"))).toHaveLength(3);
1603
- } finally {
1604
- await fixture.close();
1605
- }
1606
- }, 30000);
1607
-
1608
- test("UiActions create publishes registry from wrapped upstream metadata", async () => {
1609
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1610
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1611
- let published: any;
1612
- const actions = new UiActions({
1613
- configManager: mgr,
1614
- url: "https://bootstrap.example.test",
1615
- fetchJson: async (url) => createSellerFetchJson(url, "wrappedArray"),
1616
- commandRunner: async (args): Promise<UiActionResult> => {
1617
- if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
1618
- const filePath = args[args.indexOf("--file") + 1];
1619
- published = JSON.parse(fs.readFileSync(filePath, "utf8"));
1620
- }
1621
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1622
- }
1623
- });
1624
-
1625
- const response = await actions.createSeller(createSellerRequestFixture({
1626
- sellerName: "openrouter-ai",
1627
- app: "tbs-openrouter-ai-qecae"
1628
- }));
1629
-
1630
- expect(response.registryPublish?.ok).toBe(true);
1631
- expect(published).toMatchObject({
1632
- id: "tbs-openrouter-ai-qecae",
1633
- name: "tbs-openrouter-ai-qecae",
1634
- app: "tbs-openrouter-ai-qecae",
1635
- status: "active",
1636
- models: ["openai/gpt-5.4", "openai/gpt-5.4-mini"],
1637
- supportedProtocols: ["chat_completions"]
1638
- });
1639
- expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
1640
- url: "https://tbs-openrouter-ai-qecae.fly.dev",
1641
- token: "operator-secret"
1642
- });
1643
- });
1644
-
1645
- test("UiActions create falls back to public manifest models for registry publish", async () => {
1646
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1647
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1648
- let published: any;
1649
- const actions = new UiActions({
1650
- configManager: mgr,
1651
- url: "https://bootstrap.example.test",
1652
- fetchJson: async (url) => createSellerFetchJson(url, "manifest"),
1653
- commandRunner: async (args): Promise<UiActionResult> => {
1654
- if (args.includes("bootstrap") && args.includes("sellers") && args.includes("add")) {
1655
- const filePath = args[args.indexOf("--file") + 1];
1656
- published = JSON.parse(fs.readFileSync(filePath, "utf8"));
1657
- }
1658
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1659
- }
1660
- });
1661
-
1662
- const response = await actions.createSeller(createSellerRequestFixture({
1663
- sellerName: "openrouter-ai",
1664
- app: "tbs-openrouter-ai-qecae"
1665
- }));
1666
-
1667
- expect(response.registryPublish?.ok).toBe(true);
1668
- expect(published).toMatchObject({
1669
- id: "tbs-openrouter-ai-qecae",
1670
- name: "tbs-openrouter-ai-qecae",
1671
- app: "tbs-openrouter-ai-qecae",
1672
- status: "active",
1673
- models: ["anthropic/claude-opus-4.7"],
1674
- supportedProtocols: ["chat_completions"]
1675
- });
1676
- expect(mgr.getProfile("tbs-openrouter-ai-qecae")).toEqual({
1677
- url: "https://tbs-openrouter-ai-qecae.fly.dev",
1678
- token: "operator-secret"
1679
- });
1680
- });
1681
-
1682
- test("UiActions create supports mock-only payment without ClawTip parameters", async () => {
1683
- const fixture = await startFixtureAdminServer();
1684
- try {
1685
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1686
- mgr.setProfile("bootstrap", { url: fixture.baseUrl, token: "bootstrap-token" });
1687
- mgr.setSellerProvider("fly", { operator_secret: "operator-secret" });
1688
- const actions = new UiActions({
1689
- configManager: mgr,
1690
- profile: "bootstrap",
1691
- commandRunner: async (args): Promise<UiActionResult> => {
1692
- return { ok: true, stdout: "ok", stderr: "", command: ["node", "tb-admin", ...args] };
1693
- }
1694
- });
1695
-
1696
- const response = await actions.createSeller({
1697
- sellerName: "tbs-nrt-08",
1698
- app: "tbs-nrt-08",
1699
- region: "nrt",
1700
- image: "registry.fly.io/tb-seller:latest",
1701
- upstreamWebsite: "https://openrouter.ai",
1702
- upstreamUrl: "https://openrouter.ai/api/v1",
1703
- upstreamApiKey: "fixture-upstream-key",
1704
- upstreamBalanceProbeTemplate: "none",
1705
- maxConnections: 8,
1706
- maxQueueDepth: 4,
1707
- markupRatio: 1.2,
1708
- discountRatio: 1,
1709
- paymentMethods: ["mock"],
1710
- flyConfig: "deploy/fly.io/fly.tb-seller.toml",
1711
- dryRun: true
1712
- });
1713
-
1714
- expect(response.result.ok).toBe(true);
1715
- expect(response.readiness).toBeUndefined();
1716
- expect(response.configPut).toBeUndefined();
1717
- expect(response.configPreview.allowMock).toBe(true);
1718
- expect(response.configPreview.clawtip).toBeUndefined();
1719
- } finally {
1720
- await fixture.close();
1721
- }
1722
- });
1723
-
1724
- test("UiActions create requires ClawTip parameters for ClawTip payment", async () => {
1725
- const mgr = new ConfigManager(TEMP_CONF_PATH);
1726
- const actions = new UiActions({
1727
- configManager: mgr,
1728
- profile: "bootstrap",
1729
- commandRunner: async (): Promise<UiActionResult> => {
1730
- throw new Error("create should fail before running commands");
1731
- }
1732
- });
1733
-
1734
- await expect(actions.createSeller({
1735
- sellerName: "tbs-nrt-09",
1736
- app: "tbs-nrt-09",
1737
- region: "nrt",
1738
- image: "registry.fly.io/tb-seller:latest",
1739
- upstreamWebsite: "https://openrouter.ai",
1740
- upstreamUrl: "https://openrouter.ai/api/v1",
1741
- upstreamApiKey: "fixture-upstream-key",
1742
- upstreamBalanceProbeTemplate: "none",
1743
- maxConnections: 8,
1744
- maxQueueDepth: 4,
1745
- markupRatio: 1.2,
1746
- discountRatio: 1,
1747
- paymentMethods: ["clawtip"],
1748
- flyConfig: "deploy/fly.io/fly.tb-seller.toml"
1749
- })).rejects.toThrow("clawtipPayTo is required");
1750
- });
1751
-
1752
- test("UiActions subprocess runner uses the tb-admin bin entrypoint", async () => {
1753
- const previousArgv = process.argv[1];
1754
- process.argv[1] = path.resolve(process.env.HOME || "/tmp", "packages/admin-cli/bin/tb-admin.js");
1755
- const { runTbAdmin } = await import("../src/ui-actions.js");
1756
- try {
1757
- const result = await runTbAdmin(["--help"], 30000);
1758
- expect(result.ok).toBe(true);
1759
- expect(result.command[1]).toMatch(/packages\/admin-cli\/bin\/tb-admin\.js$/);
1760
- expect(fs.existsSync(result.command[1])).toBe(true);
1761
- expect(result.stdout).toContain("Remote admin CLI");
1762
- } finally {
1763
- process.argv[1] = previousArgv;
1764
- }
1765
- });
1766
- });
1767
-
1768
- describe("Admin CLI Display Format Spec Compliance", () => {
1769
- test("formatDuration uses ms under 1s and 2-decimal seconds above 1s", () => {
1770
- expect(formatDuration(undefined)).toBe(UNKNOWN_VALUE);
1771
- expect(formatDuration(0)).toBe("0ms");
1772
- expect(formatDuration(272)).toBe("272ms");
1773
- expect(formatDuration(999)).toBe("999ms");
1774
- expect(formatDuration(1000)).toBe("1.00s");
1775
- expect(formatDuration(3542)).toBe("3.54s");
1776
- expect(formatDuration(11540)).toBe("11.54s");
1777
- });
1778
-
1779
- test("formatMoney uses 4 decimals by default and 6 for tiny ledger amounts", () => {
1780
- expect(formatMoney(undefined)).toBe(UNKNOWN_VALUE);
1781
- expect(formatMoney(0)).toBe("$0.0000");
1782
- expect(formatMoney(138600)).toBe("$0.1386");
1783
- expect(formatMoney(2_772_200)).toBe("$2.7722");
1784
- expect(formatMoney(138600, { ledger: true })).toBe("$0.1386");
1785
- expect(formatMoney(5782, { ledger: true })).toBe("$0.005782");
1786
- });
1787
-
1788
- test("formatDiscountRatio renders the configured discount level", () => {
1789
- expect(formatDiscountRatio(undefined)).toBe(UNKNOWN_VALUE);
1790
- expect(formatDiscountRatio(0.5)).toBe("5折");
1791
- expect(formatDiscountRatio(0.3)).toBe("3折");
1792
- expect(formatDiscountRatio(0.75)).toBe("7.5折");
1793
- expect(formatDiscountRatio(1)).toBe("原价");
1794
- expect(formatDiscountRatio(0)).toBe("免费");
1795
- });
1796
-
1797
- test("formatPercent rounds to whole percent", () => {
1798
- expect(formatPercent(0.123)).toBe("12%");
1799
- expect(formatPercent(0.987)).toBe("99%");
1800
- expect(formatPercent(undefined)).toBe(UNKNOWN_VALUE);
1801
- });
1802
-
1803
- test("formatPricePair renders $/1M with 4 decimals per spec", () => {
1804
- expect(formatPricePair(undefined, undefined)).toBe(UNKNOWN_VALUE);
1805
- expect(formatPricePair(2_500_000, 3_000_000)).toBe("$2.5000 / $3.0000");
1806
- });
1807
-
1808
- test("formatCount uses en-US grouping", () => {
1809
- expect(formatCount(undefined)).toBe(UNKNOWN_VALUE);
1810
- expect(formatCount(7_529)).toBe("7,529");
1811
- expect(formatCount(218_700)).toBe("218,700");
1812
- });
1813
-
1814
- test("formatTimeCompact emits HH:mm for today and MM/DD HH:mm otherwise", () => {
1815
- const now = new Date();
1816
- const todayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 14, 51).toISOString();
1817
- const yesterdayIso = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 9, 5).toISOString();
1818
- expect(formatTimeCompact(undefined)).toBe(UNKNOWN_VALUE);
1819
- expect(formatTimeCompact(todayIso)).toBe("14:51");
1820
- expect(formatTimeCompact(yesterdayIso)).toMatch(/^\d{2}\/\d{2} 09:05$/);
1821
- });
1822
-
1823
- test("formatTimeFull uses YYYY-MM-DD HH:mm:ss for detail panels", () => {
1824
- expect(formatTimeFull(undefined)).toBe(UNKNOWN_VALUE);
1825
- expect(formatTimeFull("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/);
1826
- });
1827
-
1828
- test("formatTimeLedger uses YYYY/MM/DD HH:mm:ss for audit tables", () => {
1829
- expect(formatTimeLedger(undefined)).toBe(UNKNOWN_VALUE);
1830
- expect(formatTimeLedger("2026-06-07T00:25:51.000Z")).toMatch(/^\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}$/);
1831
- });
1832
-
1833
- test("formatSellerId trims long tbs ids to 10 chars", () => {
1834
- expect(formatSellerId(undefined)).toBe(UNKNOWN_VALUE);
1835
- expect(formatSellerId("tbs-825edb")).toBe("tbs-825edb");
1836
- expect(formatSellerId("tbs-openrouter-ai-k7p9x")).toBe("tbs-openro");
1837
- });
1838
-
1839
- test("formatSpeed renders to one decimal with tok/s suffix", () => {
1840
- expect(formatSpeed(undefined)).toBe(UNKNOWN_VALUE);
1841
- expect(formatSpeed(0)).toBe("0.0 tok/s");
1842
- expect(formatSpeed(11.54)).toBe("11.5 tok/s");
1843
- });
1844
-
1845
- test("formatSellerCapacity renders used / limit with en-US grouping", () => {
1846
- expect(formatSellerCapacity(undefined, undefined)).toBe(UNKNOWN_VALUE);
1847
- expect(formatSellerCapacity(8, 16)).toBe("8 / 16");
1848
- expect(formatSellerCapacity(undefined, 16)).toBe(`${UNKNOWN_VALUE} / 16`);
1849
- expect(formatSellerCapacity(8, undefined)).toBe(`8 / ${UNKNOWN_VALUE}`);
1850
- });
1851
-
1852
- test("formatBalanceAmount uses 0 decimals for >=100 and 2 decimals otherwise", () => {
1853
- expect(formatBalanceAmount(undefined, "USD")).toBe(UNKNOWN_VALUE);
1854
- expect(formatBalanceAmount(47_950_000, "USD")).toBe("USD 47.95");
1855
- expect(formatBalanceAmount(479_500_000, "USD")).toBe("USD 480");
1856
- expect(formatBalanceAmount(50_000_000, "EUR")).toBe("EUR 50.00");
1857
- });
1858
-
1859
- test("formatSellerStatus maps all internal node statuses to the 7 canonical labels", () => {
1860
- const canonical = new Set(["ok", "online", "configured", "pending", "degraded", "error", "unknown"]);
1861
- for (const value of ["active", "healthy", "online", "configured", "pending", "draining", "degraded", "busy_capacity", "offline", "unhealthy", "error", "auth_unknown", "unknown", undefined, "weird-state"]) {
1862
- expect(canonical.has(formatSellerStatus(value))).toBe(true);
1863
- }
1864
- expect(formatSellerStatus("active")).toBe("ok");
1865
- expect(formatSellerStatus("draining")).toBe("degraded");
1866
- expect(formatSellerStatus("busy_capacity")).toBe("degraded");
1867
- expect(formatSellerStatus("offline")).toBe("error");
1868
- expect(formatSellerStatus("auth_unknown")).toBe("unknown");
1869
- });
1870
-
1871
- test("statusTone / sellerStatusTone keep green/amber/red/blue/gray buckets stable", () => {
1872
- expect(statusTone("ok")).toBe("green");
1873
- expect(statusTone("online")).toBe("green");
1874
- expect(statusTone("configured")).toBe("green");
1875
- expect(statusTone("pending")).toBe("amber");
1876
- expect(statusTone("degraded")).toBe("amber");
1877
- expect(statusTone("error")).toBe("red");
1878
- expect(statusTone("offline")).toBe("red");
1879
- expect(statusTone("unknown")).toBe("gray");
1880
- expect(statusTone("running")).toBe("blue");
1881
- expect(sellerStatusTone("active")).toBe("green");
1882
- expect(sellerStatusTone("draining")).toBe("amber");
1883
- expect(sellerStatusTone("busy_capacity")).toBe("amber");
1884
- expect(sellerStatusTone("offline")).toBe("red");
1885
- expect(sellerStatusTone("auth_unknown")).toBe("gray");
1886
- });
1887
- });
1888
-
1889
- async function startMutableSellerConfigServer(initialConfig: Record<string, any>): Promise<{
1890
- baseUrl: string;
1891
- config: Record<string, any>;
1892
- puts: Record<string, any>[];
1893
- close: () => Promise<void>;
1894
- }> {
1895
- let config = { ...initialConfig };
1896
- const puts: Record<string, any>[] = [];
1897
- const server = http.createServer((req, res) => {
1898
- const pathName = new URL(req.url || "/", "http://127.0.0.1").pathname;
1899
- if (pathName !== "/operator/admin/config") {
1900
- res.statusCode = 404;
1901
- res.end();
1902
- return;
1903
- }
1904
- if (req.method === "GET") {
1905
- sendJson(res, { config });
1906
- return;
1907
- }
1908
- if (req.method === "PUT") {
1909
- let body = "";
1910
- req.on("data", (chunk) => {
1911
- body += chunk;
1912
- });
1913
- req.on("end", () => {
1914
- const parsed = JSON.parse(body || "{}");
1915
- config = { ...(parsed.config || parsed) };
1916
- puts.push(config);
1917
- sendJson(res, { status: "success", configPath: "/data/seller-config.yaml", config });
1918
- });
1919
- return;
1920
- }
1921
- res.statusCode = 405;
1922
- res.end();
1923
- });
1924
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
1925
- const address = server.address();
1926
- if (!address || typeof address === "string") {
1927
- throw new Error("fixture server did not bind a TCP port");
1928
- }
1929
- return {
1930
- baseUrl: `http://127.0.0.1:${address.port}`,
1931
- get config() {
1932
- return config;
1933
- },
1934
- puts,
1935
- close: () => new Promise<void>((resolve) => server.close(() => resolve()))
1936
- };
1937
- }
1938
-
1939
- async function startFixtureAdminServer(): Promise<{ baseUrl: string; close: () => Promise<void> }> {
1940
- const server = http.createServer((req, res) => {
1941
- const pathName = new URL(req.url || "/", "http://127.0.0.1").pathname;
1942
- const baseUrl = `http://${req.headers.host}`;
1943
- const registrySellers = [{
1944
- id: "tbs-sin-06",
1945
- name: "tbs-sin-06",
1946
- profile: "seller-sin",
1947
- app: "tbs-sin-06",
1948
- url: baseUrl,
1949
- status: "active",
1950
- region: "sin",
1951
- modelsCount: 1,
1952
- sampleModels: ["openai/gpt-5.4"],
1953
- supportedProtocols: ["responses"],
1954
- paymentMethods: ["clawtip"]
1955
- }];
1956
- if (pathName === "/platform/sellers") {
1957
- expect(req.headers.authorization).toBe("Bearer bootstrap-token");
1958
- sendJson(res, { sellers: registrySellers });
1959
- return;
1960
- }
1961
- if (pathName === "/registry/sellers") {
1962
- sendJson(res, {
1963
- version: 7,
1964
- updatedAt: "2026-06-05T00:00:00.000Z",
1965
- defaultSeller: "tbs-sin-06",
1966
- sellers: registrySellers
1967
- });
1968
- return;
1969
- }
1970
- if (pathName === "/manifest") {
1971
- sendJson(res, {
1972
- sellerId: "tbs-sin-06",
1973
- supportedProtocols: ["responses"],
1974
- models: [{ id: "openai/gpt-5.4" }]
1975
- });
1976
- return;
1977
- }
1978
- if (pathName === "/operator/status") {
1979
- sendJson(res, {
1980
- status: "healthy",
1981
- capacity: { activeConnections: 3, maxConnections: 8, queueDepth: 0, maxQueueDepth: 4 },
1982
- upstream: { status: "healthy" },
1983
- latency: { ttftMs: 321, avgInferenceMs: 640, lastInferenceMs: 700, sampleCount: 2 }
1984
- });
1985
- return;
1986
- }
1987
- if (pathName === "/operator/admin/service") {
1988
- sendJson(res, {
1989
- sellerId: "tbs-sin-06",
1990
- upstreamUrl: "https://openrouter.ai/api/v1",
1991
- modelsCount: 1,
1992
- capacity: { activeConnections: 3, maxConnections: 8, queueDepth: 0, maxQueueDepth: 4 }
1993
- });
1994
- return;
1995
- }
1996
- if (pathName === "/operator/admin/upstreams") {
1997
- sendJson(res, {
1998
- upstreamUrl: "https://openrouter.ai/api/v1",
1999
- upstreamApiKey: "****27f9",
2000
- markupRatio: 1.2,
2001
- discountRatio: 1,
2002
- modelAliases: { "openai/gpt-5.4": "gpt-5.4" },
2003
- models: [{
2004
- id: "openai/gpt-5.4",
2005
- inputPriceMicrosPer1m: 1000000,
2006
- outputPriceMicrosPer1m: 3000000
2007
- }]
2008
- });
2009
- return;
2010
- }
2011
- if (pathName === "/operator/admin/config") {
2012
- sendJson(res, {
2013
- config: {
2014
- upstreamUrl: "https://openrouter.ai/api/v1",
2015
- upstreamApiKey: "[redacted]",
2016
- upstreamBalanceUrl: "https://openrouter.ai/api/v1/credits",
2017
- upstreamRechargeUrl: "https://openrouter.ai/settings/credits",
2018
- upstreamBalanceProbe: {
2019
- template: "openrouter",
2020
- url: "https://openrouter.ai/api/v1/credits",
2021
- rechargeUrl: "https://openrouter.ai/settings/credits"
2022
- },
2023
- markupRatio: 1.2,
2024
- discountRatio: 1,
2025
- maxConnections: 8,
2026
- maxQueueDepth: 4,
2027
- modelAliases: { "openai/gpt-5.4": "gpt-5.4" },
2028
- models: [{ id: "openai/gpt-5.4" }]
2029
- }
2030
- });
2031
- return;
2032
- }
2033
- if (pathName === "/operator/admin/upstream-balance") {
2034
- sendJson(res, {
2035
- status: "ok",
2036
- balance: {
2037
- source: "openrouter",
2038
- rawAmount: 47.95,
2039
- amountUsdMicros: 47_950_000,
2040
- currency: "USD",
2041
- fetchedAt: 1781220000000
2042
- }
2043
- });
2044
- return;
2045
- }
2046
- res.statusCode = 404;
2047
- res.end();
2048
- });
2049
- await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
2050
- const address = server.address();
2051
- if (!address || typeof address === "string") {
2052
- throw new Error("fixture server did not bind a TCP port");
2053
- }
2054
- return {
2055
- baseUrl: `http://127.0.0.1:${address.port}`,
2056
- close: () => new Promise<void>((resolve) => server.close(() => resolve()))
2057
- };
2058
- }
2059
-
2060
- function sendJson(res: http.ServerResponse, body: unknown): void {
2061
- res.writeHead(200, { "Content-Type": "application/json" });
2062
- res.end(JSON.stringify(body));
2063
- }
2064
-
2065
- function createSellerRequestFixture(overrides: Partial<CreateSellerRequest> = {}): CreateSellerRequest {
2066
- return {
2067
- sellerName: "tbs-nrt-07",
2068
- app: "tbs-nrt-07",
2069
- region: "nrt",
2070
- image: "registry.fly.io/tb-seller:latest",
2071
- upstreamWebsite: "https://openrouter.ai",
2072
- upstreamUrl: "https://openrouter.ai/api/v1",
2073
- upstreamApiKey: "fixture-upstream-key",
2074
- upstreamBalanceProbeTemplate: "none",
2075
- maxConnections: 8,
2076
- maxQueueDepth: 4,
2077
- markupRatio: 1.2,
2078
- discountRatio: 1,
2079
- paymentMethods: ["mock"],
2080
- flyConfig: "deploy/fly.io/fly.tb-seller.toml",
2081
- ...overrides
2082
- };
2083
- }
2084
-
2085
- async function createSellerFetchJson(url: string, mode: "default" | "wrapped" | "wrappedArray" | "manifest" = "default"): Promise<unknown> {
2086
- const pathName = new URL(url).pathname;
2087
- if (pathName === "/registry/sellers") {
2088
- return {
2089
- version: 7,
2090
- sellers: []
2091
- };
2092
- }
2093
- if (pathName === "/operator/admin/upstreams") {
2094
- if (mode === "wrapped") {
2095
- return {
2096
- upstreams: {
2097
- upstreamUrl: "https://openrouter.ai/api",
2098
- models: [
2099
- { id: "openai/gpt-5.4" },
2100
- { id: "openai/gpt-5.4-mini" }
2101
- ],
2102
- supportedProtocols: ["chat_completions"]
2103
- }
2104
- };
2105
- }
2106
- if (mode === "wrappedArray") {
2107
- return {
2108
- upstreams: [{
2109
- upstreamUrl: "https://openrouter.ai/api",
2110
- models: [
2111
- { id: "openai/gpt-5.4" },
2112
- { id: "openai/gpt-5.4-mini" }
2113
- ],
2114
- supportedProtocols: ["chat_completions"]
2115
- }]
2116
- };
2117
- }
2118
- if (mode === "manifest") {
2119
- return {
2120
- upstreams: {
2121
- upstreamUrl: "https://openrouter.ai/api",
2122
- modelsCount: 1
2123
- }
2124
- };
2125
- }
2126
- return {
2127
- upstreamUrl: "https://openrouter.ai/api",
2128
- models: [
2129
- { id: "openai/gpt-5.4" },
2130
- { id: "openai/gpt-5.4-mini" }
2131
- ],
2132
- supportedProtocols: ["chat_completions"]
2133
- };
2134
- }
2135
- if (pathName === "/operator/admin/service") {
2136
- return {
2137
- sellerId: "tbs-nrt-07",
2138
- modelsCount: 2,
2139
- supportedProtocols: ["chat_completions"]
2140
- };
2141
- }
2142
- if (pathName === "/manifest" && mode === "manifest") {
2143
- return {
2144
- sellerId: "tbs-nrt-07",
2145
- models: ["anthropic/claude-opus-4.7"],
2146
- supportedProtocols: ["chat_completions"]
2147
- };
2148
- }
2149
- throw new Error(`unexpected fetch url ${url}`);
2150
- }
2151
-
2152
- function isBootstrapSellersAdd(args: string[]): boolean {
2153
- const addIndex = args.indexOf("add");
2154
- return addIndex >= 2 && args[addIndex - 2] === "bootstrap" && args[addIndex - 1] === "sellers";
2155
- }
2156
-
2157
- function jsonResponse(body: unknown, status = 200): Response {
2158
- return new Response(JSON.stringify(body), {
2159
- status,
2160
- headers: { "Content-Type": "application/json" }
2161
- });
2162
- }