@tokenbuddy/tb-admin 1.0.36 → 1.0.38

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 +98 -25
  2. package/dist/src/config.d.ts +8 -2
  3. package/dist/src/config.js +17 -5
  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 +9 -3
  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,467 +0,0 @@
1
- /**
2
- * Step 13 v1.1: admin web UI seller row 渲染测试.
3
- *
4
- * 原则: 不引入 jsdom / linkedom. 直接 evaluate ui-static.ts bundle 的
5
- * inline script 抽出 sellerRow 函数 (regex 匹配 function 声明) + 喂 mock
6
- * window.__tbFmt, 然后 string match 验证 4 类行 (dataSource=both / fly /
7
- * registry / 老 1.0.31 缺字段) 渲染出正确的 CSS class + 文案 + 按钮.
8
- *
9
- * 覆盖 (跟 docs/processes/seller-fleet-data-sources.md 一致):
10
- * 1. dataSource="both" → "app-row" (无红/灰 class), 绿点 active, "已发布" chip
11
- * 2. dataSource="fly" → "app-row app-row-fly-only" (灰), "未发布" chip
12
- * + publish-hint-btn 文案
13
- * 3. dataSource="registry" → "app-row app-row-registry-only" (整行红边)
14
- * + "严重事故" + alert-reason + remove-hint-btn
15
- * 4. dataSource 老 1.0.31 缺失 → 兜底 "both" (默认行, 不报红)
16
- *
17
- * 验证 grep 锚点 (跟 spec 一致):
18
- * - "立即下线 (registry-only)" 必须在 remove-hint-btn 出现
19
- * - "registry 收录了但 fly app 失踪" 必须在 alert-reason 出现
20
- * - "未发布 — 可申请发布" 必须在 publish-hint-btn 出现
21
- */
22
-
23
- import { adminUiHtml } from "../src/ui-static.js";
24
-
25
- interface MockFmt {
26
- UNKNOWN_VALUE: string;
27
- formatDuration: (v: unknown) => string;
28
- formatSpeed: (v: unknown) => string;
29
- formatBalanceAmount: () => string;
30
- formatSellerCapacity: () => string;
31
- formatDiscountRatio: (v: unknown) => string;
32
- formatSellerStatus: (s: unknown) => string;
33
- sellerStatusTone: (s: unknown) => string;
34
- normalizeStatusLabel: (s: unknown) => string;
35
- formatTimeCompact: (s: unknown) => string;
36
- formatCount: (n: unknown) => string;
37
- }
38
-
39
- const MOCK_FMT: MockFmt = {
40
- UNKNOWN_VALUE: "—",
41
- formatDuration: () => "123ms",
42
- formatSpeed: () => "12.3 tok/s",
43
- formatBalanceAmount: () => "0.00",
44
- formatSellerCapacity: () => "3/8",
45
- formatDiscountRatio: () => "1.0x",
46
- formatSellerStatus: (s) => String(s || "unknown"),
47
- sellerStatusTone: (s) => {
48
- const k = String(s || "unknown").toLowerCase();
49
- if (k === "active" || k === "healthy") return "green";
50
- if (k === "draining" || k === "degraded") return "amber";
51
- if (k === "offline" || k === "unhealthy") return "red";
52
- if (k === "busy_capacity") return "blue";
53
- return "gray";
54
- },
55
- normalizeStatusLabel: (s) => String(s || "—"),
56
- formatTimeCompact: () => "12:34",
57
- formatCount: (n) => String(n ?? 0)
58
- };
59
-
60
- /**
61
- * Step 13 v1.1: 不引 jsdom, 用 `new Function` 跑抽出 sellerRow 函数体.
62
- * ui-static.ts 整个文件是 `<script>...</script>` 模板字符串, 我们用
63
- * 简单 regex 提取 inline script 内容, 加上 mock window, 跑 sellerRow 拿 HTML.
64
- *
65
- * 限制: 不模拟 DOM 树. 验证方式是直接看 sellerRow() 返回的 HTML 字符串
66
- * 是否含预期的 class + 文案 + 按钮 (跟 spec grep 锚点一一对应).
67
- */
68
- function renderRow(row: Record<string, unknown>): string {
69
- const html = adminUiHtml();
70
- // 抽 <script>...</script>
71
- const scriptMatch = html.match(/<script>([\s\S]*?)<\/script>/);
72
- if (!scriptMatch) {
73
- throw new Error("adminUiHtml() did not include a <script> tag");
74
- }
75
- const scriptBody = scriptMatch[1];
76
- // mock window + 把 sellerRow 暴露
77
- const code = `
78
- var noopArr = () => [];
79
- var fakeEl = { classList: { toggle: () => undefined, add: () => undefined, remove: () => undefined }, innerHTML: "", textContent: "" };
80
- var window = { __tbFmt: ${JSON.stringify(MOCK_FMT)}, addEventListener: () => undefined };
81
- var document = {
82
- createElement: () => fakeEl,
83
- addEventListener: () => undefined,
84
- querySelectorAll: noopArr,
85
- querySelector: () => fakeEl,
86
- getElementById: () => fakeEl
87
- };
88
- var setTimeout = () => 0;
89
- var setInterval = () => 0;
90
- var clearTimeout = () => undefined;
91
- var clearInterval = () => undefined;
92
- ${scriptBody}
93
- return sellerRow(${JSON.stringify(row)});
94
- `;
95
- // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
96
- const fn = new Function(code);
97
- return fn();
98
- }
99
-
100
- describe("admin web seller row 渲染 (Step 13 v1.1 双源 spec)", () => {
101
- test("dataSource=both → 正常行 (无红/灰 class), 绿点, 已发布 chip", () => {
102
- const out = renderRow({
103
- id: "tbs-alpha",
104
- name: "tbs-alpha",
105
- app: "tbs-alpha",
106
- url: "https://tbs-alpha.fly.dev",
107
- registryStatus: "active",
108
- nodeStatus: "active",
109
- upstreamDomain: "openrouter.ai",
110
- upstreamStatus: "healthy",
111
- modelsCount: 1,
112
- dataSource: "both",
113
- flyApp: { name: "tbs-alpha", status: "running" }
114
- });
115
- // 行 class 不带 registry-only / fly-only
116
- expect(out).toContain('class="app-row"');
117
- expect(out).not.toContain("app-row-registry-only");
118
- expect(out).not.toContain("app-row-fly-only");
119
- // 绿点 active → tone-green
120
- expect(out).toMatch(/app-dot tone-green/);
121
- expect(out).toContain('title="registry: active · upstream: healthy · publish: published · detail: pending"');
122
- // 已发布 chip
123
- expect(out).toMatch(/datasource-chip published/);
124
- expect(out).toContain("已发布");
125
- expect(out).not.toContain("status-badge");
126
- // 无 alert-reason / remove-hint-btn / publish-hint-btn
127
- expect(out).not.toContain("alert-reason");
128
- expect(out).not.toContain("remove-hint-btn");
129
- expect(out).not.toContain("publish-hint-btn");
130
- });
131
-
132
- test("dataSource=fly → 灰底 + 未发布 chip + publish-hint-btn", () => {
133
- const out = renderRow({
134
- id: "tbs-flyonly",
135
- name: "tbs-flyonly",
136
- app: "tbs-flyonly",
137
- url: "https://tbs-flyonly.fly.dev",
138
- registryStatus: "unknown",
139
- nodeStatus: "unknown",
140
- upstreamDomain: "tbs-flyonly.fly.dev",
141
- upstreamStatus: "unknown",
142
- modelsCount: 0,
143
- dataSource: "fly",
144
- publishStatus: "unpublished",
145
- publishHint: "未发布 — 可申请发布 (走 vendor-bootstrap stage)",
146
- flyApp: { name: "tbs-flyonly", status: "running" }
147
- });
148
- expect(out).toContain("app-row-fly-only");
149
- expect(out).not.toContain("app-row-registry-only");
150
- expect(out).toMatch(/datasource-chip unpublished/);
151
- expect(out).toContain("未发布");
152
- // publish-hint-btn 出现 + 文案含 spec 关键词
153
- expect(out).toContain("publish-hint-btn");
154
- expect(out).toContain("未发布 — 可申请发布");
155
- });
156
-
157
- test("registryStatus=offline overrides Pub chip even when Fly app still exists", () => {
158
- const out = renderRow({
159
- id: "tbs-offline",
160
- name: "tbs-offline",
161
- app: "tbs-offline",
162
- url: "https://tbs-offline.fly.dev",
163
- registryStatus: "offline",
164
- nodeStatus: "unknown",
165
- upstreamDomain: "api.example.com",
166
- upstreamStatus: "unknown",
167
- modelsCount: 1,
168
- dataSource: "both",
169
- publishStatus: "published",
170
- flyApp: { name: "tbs-offline", status: "running" }
171
- });
172
-
173
- expect(out).toMatch(/datasource-chip offline/);
174
- expect(out).toContain("已下线");
175
- expect(out).toContain("Publish: offline · registry: offline · source: both");
176
- expect(out).toContain('title="registry: offline · upstream: unknown · publish: offline · detail: pending"');
177
- expect(out).not.toContain(">已发布</span>");
178
- expect(out).not.toContain("registry: error");
179
- });
180
-
181
- test("registryStatus=draining overrides Pub chip even when publish relation is published", () => {
182
- const out = renderRow({
183
- id: "tbs-draining",
184
- name: "tbs-draining",
185
- app: "tbs-draining",
186
- url: "https://tbs-draining.fly.dev",
187
- registryStatus: "draining",
188
- nodeStatus: "active",
189
- upstreamDomain: "api.example.com",
190
- upstreamStatus: "healthy",
191
- modelsCount: 1,
192
- dataSource: "both",
193
- publishStatus: "published",
194
- flyApp: { name: "tbs-draining", status: "running" }
195
- });
196
-
197
- expect(out).toMatch(/datasource-chip draining/);
198
- expect(out).toContain("下线中");
199
- expect(out).toContain("Publish: draining · registry: draining · source: both");
200
- });
201
-
202
- test("publishStatus=checking → 不把未知发布状态显示成未发布", () => {
203
- const out = renderRow({
204
- id: "tbs-checking",
205
- name: "tbs-checking",
206
- app: "tbs-checking",
207
- url: "https://tbs-checking.fly.dev",
208
- registryStatus: "unknown",
209
- nodeStatus: "unknown",
210
- upstreamDomain: "tbs-checking.fly.dev",
211
- upstreamStatus: "unknown",
212
- modelsCount: 0,
213
- dataSource: "fly",
214
- publishStatus: "checking",
215
- detailStatus: "pending",
216
- flyApp: { name: "tbs-checking", status: "running" }
217
- });
218
- expect(out).not.toContain("app-row-fly-only");
219
- expect(out).toMatch(/datasource-chip checking/);
220
- expect(out).toContain("发布待确认");
221
- expect(out).not.toContain("publish-hint-btn");
222
- expect(out).not.toContain(">未发布<");
223
- });
224
-
225
- test("列表列语义: connection 显示连接数, resources 无第二排, upstream 不显示 seller host / models", () => {
226
- const out = renderRow({
227
- id: "tbs-hide-host",
228
- name: "tbs-hide-host",
229
- app: "tbs-hide-host",
230
- url: "https://tbs-hide-host.fly.dev",
231
- registryStatus: "active",
232
- nodeStatus: "active",
233
- upstreamDomain: "tbs-hide-host.fly.dev",
234
- upstreamStatus: "healthy",
235
- upstreamBalanceUsdMicros: 1020000,
236
- upstreamBalanceCurrency: "USD",
237
- upstreamBalanceSource: "usage_generic",
238
- capacityUsed: 3,
239
- capacityLimit: 8,
240
- resourceCpuPercent: 12.5,
241
- resourceMemoryPercent: 48,
242
- resourceMemoryRssMb: 246,
243
- resourceMemoryLimitMb: 512,
244
- discountRatio: 1,
245
- modelsCount: 7,
246
- specs: { cpuCores: 1, memoryMb: 512, machines: 1, runningMachines: 1, volumeGb: 1 },
247
- dataSource: "both",
248
- publishStatus: "published",
249
- detailStatus: "fresh",
250
- detailUpdatedAt: "2026-06-12T13:06:00.000Z",
251
- detailNextRefreshAt: "2999-01-01T00:00:00.000Z",
252
- flyApp: { name: "tbs-hide-host", status: "running" }
253
- });
254
-
255
- expect(out).toContain("op-value");
256
- expect(out).toContain(">3 / 8</strong>");
257
- expect(out).toContain("12.5%/48%");
258
- expect(out).toContain("spec 1 vCPU / 512 MB");
259
- expect(out).toContain("balance-line tone-red");
260
- expect(out).toContain("op-chip tone-red");
261
- expect(out).not.toMatch(/Resources<\/span><strong[^>]*>1 vCPU \/ 512 MB<\/strong><span class="metric-sub"/);
262
- expect(out).not.toContain("cap 3 / 8");
263
- expect(out).not.toContain("<strong>tbs-hide-host.fly.dev</strong>");
264
- expect(out).not.toContain("7 models · disc");
265
- expect(out).not.toContain("usage_generic");
266
- expect(out).not.toContain("balance-badge tone-green");
267
- expect(out).toContain("op-chip tone-green");
268
- expect(out).toContain(">healthy</span>");
269
- });
270
-
271
- test("CPU/RAM 列只显示占比, 无运行数据时显示 -/-", () => {
272
- const out = renderRow({
273
- id: "tbs-no-runtime",
274
- name: "tbs-no-runtime",
275
- app: "tbs-no-runtime",
276
- url: "https://tbs-no-runtime.fly.dev",
277
- registryStatus: "active",
278
- nodeStatus: "active",
279
- upstreamDomain: "api.example.com",
280
- upstreamStatus: "healthy",
281
- capacityUsed: 0,
282
- capacityLimit: 8,
283
- dataSource: "both",
284
- publishStatus: "published",
285
- flyApp: { name: "tbs-no-runtime", status: "running" }
286
- });
287
-
288
- expect(out).toContain("-/-");
289
- expect(out).not.toContain("CPU — / MEM —");
290
- });
291
-
292
- test("上游 status 从 seller /operator/status 单列显示, unhealthy 显示红色告警", () => {
293
- const out = renderRow({
294
- id: "tbs-upstream-down",
295
- name: "tbs-upstream-down",
296
- app: "tbs-upstream-down",
297
- url: "https://tbs-upstream-down.fly.dev",
298
- registryStatus: "active",
299
- nodeStatus: "active",
300
- upstreamDomain: "api.downstream.example",
301
- upstreamStatus: "unhealthy",
302
- capacityUsed: 0,
303
- capacityLimit: 8,
304
- dataSource: "both",
305
- publishStatus: "published",
306
- flyApp: { name: "tbs-upstream-down", status: "running" }
307
- });
308
-
309
- expect(out).toContain(">Status</span>");
310
- expect(out).toContain("Upstream status from seller /operator/status");
311
- expect(out).toContain("op-chip tone-red");
312
- expect(out).toContain(">unhealthy</span>");
313
- expect(out).toContain("row-alert");
314
- });
315
-
316
- test("上游 status 未知时显示中性 -, 不显示 unknown 文案", () => {
317
- const out = renderRow({
318
- id: "tbs-upstream-unknown",
319
- name: "tbs-upstream-unknown",
320
- app: "tbs-upstream-unknown",
321
- url: "https://tbs-upstream-unknown.fly.dev",
322
- registryStatus: "active",
323
- nodeStatus: "active",
324
- upstreamDomain: "api.example.com",
325
- upstreamStatus: "unknown",
326
- capacityUsed: 0,
327
- capacityLimit: 8,
328
- dataSource: "both",
329
- publishStatus: "published",
330
- flyApp: { name: "tbs-upstream-unknown", status: "running" }
331
- });
332
-
333
- expect(out).toContain(">Status</span>");
334
- expect(out).toContain("op-chip tone-gray");
335
- expect(out).toContain(">-\u003c/span>");
336
- expect(out).not.toContain(">unknown</span>");
337
- expect(out).not.toContain(">unkown</span>");
338
- });
339
-
340
- test("余额探测失败只显示 -, 不显示额外状态文案", () => {
341
- const out = renderRow({
342
- id: "tbs-balance-blocked",
343
- name: "tbs-balance-blocked",
344
- app: "tbs-balance-blocked",
345
- url: "https://tbs-balance-blocked.fly.dev",
346
- registryStatus: "active",
347
- nodeStatus: "active",
348
- upstreamDomain: "api.example.com",
349
- upstreamStatus: "healthy",
350
- upstreamBalanceError: "unauthorized: check upstreamApiKey",
351
- capacityUsed: 0,
352
- capacityLimit: 8,
353
- dataSource: "both",
354
- publishStatus: "published",
355
- flyApp: { name: "tbs-balance-blocked", status: "running" }
356
- });
357
-
358
- expect(out).toContain('title="unauthorized: check upstreamApiKey">-</span>');
359
- expect(out).not.toContain("probe blocked");
360
- expect(out).not.toContain("balance-badge tone-gray");
361
- expect(out).not.toContain("balance-badge tone-red");
362
- expect(out).not.toContain("balance-line tone-red");
363
- });
364
-
365
- test("详情刷新 queued/loading 时 Next 列显示 spinner, 不显示状态文本", () => {
366
- const out = renderRow({
367
- id: "tbs-refreshing",
368
- name: "tbs-refreshing",
369
- app: "tbs-refreshing",
370
- url: "https://tbs-refreshing.fly.dev",
371
- registryStatus: "active",
372
- nodeStatus: "active",
373
- upstreamDomain: "api.example.com",
374
- upstreamStatus: "healthy",
375
- capacityUsed: 0,
376
- capacityLimit: 8,
377
- detailStatus: "loading",
378
- dataSource: "both",
379
- publishStatus: "published",
380
- flyApp: { name: "tbs-refreshing", status: "running" }
381
- });
382
-
383
- expect(out).toContain("inline-spinner");
384
- expect(out).toContain("aria-label=\"detail loading\"");
385
- expect(out).not.toContain(">loading</strong>");
386
- expect(out).not.toContain(">queued</strong>");
387
- });
388
-
389
- test("运维关键指标按风险突出: 满连接/高资源/慢 TTFT 显示红黄 chip", () => {
390
- const out = renderRow({
391
- id: "tbs-hot",
392
- name: "tbs-hot",
393
- app: "tbs-hot",
394
- url: "https://tbs-hot.fly.dev",
395
- registryStatus: "active",
396
- nodeStatus: "active",
397
- upstreamDomain: "openrouter.ai",
398
- upstreamStatus: "healthy",
399
- capacityUsed: 8,
400
- capacityLimit: 8,
401
- resourceCpuPercent: 81,
402
- resourceMemoryPercent: 93,
403
- ttftMs: 6500,
404
- upstreamBalanceUsdMicros: 50000000,
405
- upstreamBalanceCurrency: "USD",
406
- dataSource: "both",
407
- publishStatus: "published",
408
- flyApp: { name: "tbs-hot", status: "running" }
409
- });
410
-
411
- expect(out).toContain("row-alert");
412
- expect(out).toContain("op-chip tone-red");
413
- expect(out).toContain(">8 / 8</span>");
414
- expect(out).toContain("81%/93%");
415
- expect(out).toContain(">6.50s</span>");
416
- });
417
-
418
- test("dataSource=registry → 整行红边 + 严重事故 + alert-reason + 立即下线 (registry-only) 按钮", () => {
419
- const out = renderRow({
420
- id: "tbs-ghost",
421
- name: "tbs-ghost",
422
- app: "tbs-ghost",
423
- url: "https://tbs-ghost.fly.dev",
424
- registryStatus: "active",
425
- nodeStatus: "unknown",
426
- upstreamDomain: "tbs-ghost.fly.dev",
427
- upstreamStatus: "unknown",
428
- modelsCount: 1,
429
- dataSource: "registry",
430
- publishStatus: "registry_only",
431
- registryAlert: true,
432
- alertReason: "registry 收录了但 fly app 失踪 — 严重事故, 立即下线",
433
- removeHint: "立即下线 (registry-only)"
434
- });
435
- expect(out).toContain("app-row-registry-only");
436
- // 严重事故 文案
437
- expect(out).toContain("严重事故");
438
- // alert-reason
439
- expect(out).toContain("alert-reason");
440
- expect(out).toContain("registry 收录了但 fly app 失踪");
441
- // 立即下线 (registry-only) 按钮
442
- expect(out).toContain("remove-hint-btn");
443
- expect(out).toContain("立即下线 (registry-only)");
444
- expect(out).toMatch(/data-action="remove"/);
445
- expect(out).toMatch(/data-seller-id="tbs-ghost"/);
446
- });
447
-
448
- test("dataSource 缺失 (老 1.0.31 行) → 兜底 'both', 不报错不标红", () => {
449
- const out = renderRow({
450
- id: "legacy",
451
- name: "legacy",
452
- app: "legacy",
453
- url: "https://legacy.fly.dev",
454
- registryStatus: "active",
455
- nodeStatus: "active",
456
- upstreamDomain: "legacy.fly.dev",
457
- upstreamStatus: "healthy",
458
- modelsCount: 1
459
- // 故意没 dataSource 字段, 模拟 1.0.31 老 row
460
- });
461
- expect(out).toContain('class="app-row"');
462
- expect(out).not.toContain("app-row-registry-only");
463
- expect(out).not.toContain("app-row-fly-only");
464
- // 兜底 chip = published
465
- expect(out).toMatch(/datasource-chip published/);
466
- });
467
- });