@tokenbuddy/tokenbuddy 1.0.25 → 1.0.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/tb-clawtip-proof.js +2 -0
  2. package/dist/src/clawtip-bootstrap.d.ts +1 -0
  3. package/dist/src/clawtip-bootstrap.d.ts.map +1 -1
  4. package/dist/src/clawtip-bootstrap.js +1 -0
  5. package/dist/src/clawtip-bootstrap.js.map +1 -1
  6. package/dist/src/cli.d.ts +1 -0
  7. package/dist/src/cli.d.ts.map +1 -1
  8. package/dist/src/cli.js +172 -51
  9. package/dist/src/cli.js.map +1 -1
  10. package/dist/src/daemon.d.ts +6 -0
  11. package/dist/src/daemon.d.ts.map +1 -1
  12. package/dist/src/daemon.js +562 -292
  13. package/dist/src/daemon.js.map +1 -1
  14. package/dist/src/init-clawtip-activation.d.ts +12 -0
  15. package/dist/src/init-clawtip-activation.d.ts.map +1 -1
  16. package/dist/src/init-clawtip-activation.js +82 -2
  17. package/dist/src/init-clawtip-activation.js.map +1 -1
  18. package/dist/src/package-update.d.ts +60 -0
  19. package/dist/src/package-update.d.ts.map +1 -0
  20. package/dist/src/package-update.js +220 -0
  21. package/dist/src/package-update.js.map +1 -0
  22. package/dist/src/registry-trust.d.ts +7 -0
  23. package/dist/src/registry-trust.d.ts.map +1 -0
  24. package/dist/src/registry-trust.js +37 -0
  25. package/dist/src/registry-trust.js.map +1 -0
  26. package/dist/src/route-failover.d.ts +2 -2
  27. package/dist/src/route-failover.d.ts.map +1 -1
  28. package/dist/src/route-failover.js +11 -0
  29. package/dist/src/route-failover.js.map +1 -1
  30. package/dist/src/seller-catalog.d.ts +20 -0
  31. package/dist/src/seller-catalog.d.ts.map +1 -1
  32. package/dist/src/seller-catalog.js +41 -4
  33. package/dist/src/seller-catalog.js.map +1 -1
  34. package/dist/src/seller-concurrency-limiter.d.ts +36 -0
  35. package/dist/src/seller-concurrency-limiter.d.ts.map +1 -0
  36. package/dist/src/seller-concurrency-limiter.js +126 -0
  37. package/dist/src/seller-concurrency-limiter.js.map +1 -0
  38. package/dist/src/seller-pool.d.ts +7 -1
  39. package/dist/src/seller-pool.d.ts.map +1 -1
  40. package/dist/src/seller-pool.js +18 -0
  41. package/dist/src/seller-pool.js.map +1 -1
  42. package/dist/src/seller-route-planner.d.ts +21 -0
  43. package/dist/src/seller-route-planner.d.ts.map +1 -1
  44. package/dist/src/seller-route-planner.js +98 -20
  45. package/dist/src/seller-route-planner.js.map +1 -1
  46. package/dist/src/tb-clawtip-proof.d.ts +3 -0
  47. package/dist/src/tb-clawtip-proof.d.ts.map +1 -0
  48. package/dist/src/tb-clawtip-proof.js +24 -0
  49. package/dist/src/tb-clawtip-proof.js.map +1 -0
  50. package/dist/src/tb-proxyd.js +45 -3
  51. package/dist/src/tb-proxyd.js.map +1 -1
  52. package/package.json +3 -2
  53. package/src/clawtip-bootstrap.ts +1 -0
  54. package/src/cli.ts +200 -47
  55. package/src/daemon.ts +347 -50
  56. package/src/init-clawtip-activation.ts +77 -1
  57. package/src/package-update.ts +313 -0
  58. package/src/registry-trust.ts +51 -0
  59. package/src/route-failover.ts +14 -2
  60. package/src/seller-catalog.ts +67 -4
  61. package/src/seller-concurrency-limiter.ts +161 -0
  62. package/src/seller-pool.ts +20 -0
  63. package/src/seller-route-planner.ts +142 -20
  64. package/src/tb-clawtip-proof.ts +28 -0
  65. package/src/tb-proxyd.ts +48 -3
  66. package/static/ui/assets/index-Bzbrp7Qe.css +1 -0
  67. package/static/ui/assets/index-UAfOhbwC.js +236 -0
  68. package/static/ui/assets/index-UAfOhbwC.js.map +1 -0
  69. package/static/ui/index.html +2 -2
  70. package/tests/cli-routing.test.ts +37 -4
  71. package/tests/control-plane-ui-endpoints.test.ts +7 -7
  72. package/tests/daemon-trusted-registry-cache.test.ts +132 -0
  73. package/tests/e2e.test.ts +14 -1
  74. package/tests/package-update.test.ts +132 -0
  75. package/tests/registry-trust.test.ts +28 -0
  76. package/tests/route-failover.test.ts +13 -0
  77. package/tests/seller-catalog-413.test.ts +60 -1
  78. package/tests/seller-concurrency-limiter.test.ts +83 -0
  79. package/tests/seller-pool.test.ts +23 -0
  80. package/tests/seller-route-planner.test.ts +78 -0
  81. package/tests/tokenbuddy.test.ts +316 -34
  82. package/static/ui/assets/index-1uuyCCzj.css +0 -1
  83. package/static/ui/assets/index-BJSOFJIU.js +0 -236
  84. package/static/ui/assets/index-BJSOFJIU.js.map +0 -1
@@ -64,6 +64,40 @@ describe("seller route planner", () => {
64
64
  expect(result.source).toBe("registry_fallback");
65
65
  expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
66
66
  expect(result.routes.map((route) => route.seller.id)).toEqual(["s1", "s2"]);
67
+ expect(result.diagnostics).toMatchObject({
68
+ prewarmCandidateCount: 2,
69
+ prewarmUsableCount: 0,
70
+ prewarmMissingSellerIds: ["missing"],
71
+ prewarmBlockedSellerIds: [],
72
+ prewarmIncompatibleSellerIds: ["s3"]
73
+ });
74
+ });
75
+
76
+ test("diagnostics explain prewarm fallback caused by blocked warm candidates", () => {
77
+ const now = 10_000;
78
+ const result = plan({
79
+ now,
80
+ prewarmCandidates: [
81
+ { sellerId: "s1", url: "https://s1.example.com", healthScore: 100 },
82
+ { sellerId: "s2", url: "https://s2.example.com", healthScore: 90 }
83
+ ],
84
+ sellerMetrics: [
85
+ { sellerId: "s1", circuit: "open" },
86
+ { sellerId: "s2", capacityBlockedUntil: now + 1000 }
87
+ ]
88
+ });
89
+
90
+ expect(result.source).toBe("registry_fallback");
91
+ expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
92
+ expect(result.routes).toEqual([]);
93
+ expect(result.diagnostics).toMatchObject({
94
+ prewarmCandidateCount: 2,
95
+ prewarmUsableCount: 0,
96
+ prewarmBlockedSellerIds: ["s1", "s2"],
97
+ blockedOpenCircuitCount: 1,
98
+ blockedCapacityCount: 1,
99
+ blockedSellerIds: ["s1", "s2"]
100
+ });
67
101
  });
68
102
 
69
103
  test("filters registry fallback by requested model, protocol, and payment method", () => {
@@ -79,6 +113,21 @@ describe("seller route planner", () => {
79
113
  expect(result.source).toBe("registry_fallback");
80
114
  expect(result.routes.map((route) => route.seller.id)).toEqual(["ok"]);
81
115
  expect(result.reason).toBe("fullAuto:balanced:routes_1");
116
+ expect(result.diagnostics).toEqual({
117
+ registryVisibleCount: 4,
118
+ prewarmCandidateCount: 0,
119
+ prewarmUsableCount: 0,
120
+ prewarmMissingSellerIds: [],
121
+ prewarmBlockedSellerIds: [],
122
+ prewarmIncompatibleSellerIds: [],
123
+ sourceCandidateCount: 1,
124
+ blockedOpenCircuitCount: 0,
125
+ blockedCapacityCount: 0,
126
+ blockedLocalConcurrencyCount: 0,
127
+ blockedSellerIds: [],
128
+ incompatibleCount: 3,
129
+ incompatibleSellerIds: ["wrong-model", "wrong-payment", "wrong-protocol"]
130
+ });
82
131
  });
83
132
 
84
133
  test("only active registry sellers are visible to buyer routing", () => {
@@ -161,6 +210,8 @@ describe("seller route planner", () => {
161
210
  });
162
211
 
163
212
  expect(result.routes.map((route) => route.seller.id)).toEqual(["s2"]);
213
+ expect(result.diagnostics.blockedOpenCircuitCount).toBe(1);
214
+ expect(result.diagnostics.blockedSellerIds).toEqual(["s1"]);
164
215
  });
165
216
 
166
217
  test("active capacity blocks are excluded before strategy selection", () => {
@@ -175,6 +226,8 @@ describe("seller route planner", () => {
175
226
  });
176
227
 
177
228
  expect(blocked.routes.map((route) => route.seller.id)).toEqual(["s2"]);
229
+ expect(blocked.diagnostics.blockedCapacityCount).toBe(1);
230
+ expect(blocked.diagnostics.blockedSellerIds).toEqual(["s1"]);
178
231
 
179
232
  const expired = plan({
180
233
  now: now + 1001,
@@ -186,5 +239,30 @@ describe("seller route planner", () => {
186
239
  });
187
240
 
188
241
  expect(expired.routes.map((route) => route.seller.id)).toEqual(["s1", "s2"]);
242
+ expect(expired.diagnostics.blockedCapacityCount).toBe(0);
243
+ expect(expired.diagnostics.blockedSellerIds).toEqual([]);
244
+ });
245
+
246
+ test("local concurrency blocks are excluded before choosing prewarm or registry candidates", () => {
247
+ const result = plan({
248
+ prewarmCandidates: [
249
+ { sellerId: "s1", url: "https://s1.example.com", healthScore: 100 }
250
+ ],
251
+ sellerMetrics: [
252
+ { sellerId: "s1", localConcurrencyActive: 1, localConcurrencyLimit: 1 },
253
+ { sellerId: "s2", localConcurrencyActive: 0, localConcurrencyLimit: 1 }
254
+ ]
255
+ });
256
+
257
+ expect(result.source).toBe("registry_fallback");
258
+ expect(result.sourceReason).toBe("prewarm_no_compatible_candidates");
259
+ expect(result.routes.map((route) => route.seller.id)).toEqual(["s2"]);
260
+ expect(result.diagnostics).toMatchObject({
261
+ prewarmCandidateCount: 1,
262
+ prewarmUsableCount: 0,
263
+ prewarmBlockedSellerIds: ["s1"],
264
+ blockedLocalConcurrencyCount: 1,
265
+ blockedSellerIds: ["s1"]
266
+ });
189
267
  });
190
268
  });
@@ -5,6 +5,7 @@ import {
5
5
  buildLaunchdPlistContent,
6
6
  buildCli,
7
7
  fetchClawtipBootstrap,
8
+ installLaunchAgentWithRunner,
8
9
  normalizeClawtipBootstrapResourceUrl,
9
10
  restartLaunchAgent,
10
11
  runWebInitLauncher,
@@ -15,6 +16,7 @@ import {
15
16
  readClawtipPayCredential,
16
17
  resolveNpxCommand,
17
18
  resolveClawtipQrMediaPath,
19
+ createClawtipPaymentProof,
18
20
  startClawtipWalletBootstrap,
19
21
  waitForClawtipActivationConfirmation,
20
22
  writeClawtipOrderFile,
@@ -74,7 +76,7 @@ describe("TokenBuddy CLI command surface", () => {
74
76
  .filter(command => command !== "help")
75
77
  .sort();
76
78
 
77
- expect(commandNames).toEqual(["daemon", "doctor", "init", "models", "payment", "routing", "ui"]);
79
+ expect(commandNames).toEqual(["daemon", "doctor", "init", "models", "payment", "routing", "ui", "update"]);
78
80
  });
79
81
 
80
82
  test("tb daemon help exposes restart", () => {
@@ -116,7 +118,8 @@ describe("TokenBuddy CLI command surface", () => {
116
118
 
117
119
  expect(packageJson.bin).toEqual({
118
120
  tb: "bin/tb.js",
119
- "tb-proxyd": "bin/tb-proxyd.js"
121
+ "tb-proxyd": "bin/tb-proxyd.js",
122
+ "tb-clawtip-proof": "bin/tb-clawtip-proof.js"
120
123
  });
121
124
  });
122
125
 
@@ -136,7 +139,7 @@ describe("TokenBuddy CLI command surface", () => {
136
139
  stderrPath: "/Users/example/.tokenbuddy-store/tb-proxyd.stderr.log",
137
140
  controlPort: 17820,
138
141
  proxyPort: 17821,
139
- sellerRegistryUrl: "https://tb-wallet-bootstrap.fly.dev/registry/sellers",
142
+ sellerRegistryUrl: "https://registry.tokenbuddy.ai/v1/registry.json",
140
143
  pathEnv: "/usr/bin:/bin",
141
144
  });
142
145
 
@@ -146,7 +149,7 @@ describe("TokenBuddy CLI command surface", () => {
146
149
  expect(plist).toContain("<key>TB_PROXYD_PROXY_PORT</key>");
147
150
  expect(plist).toContain("<string>17821</string>");
148
151
  expect(plist).toContain("<key>TB_PROXYD_SELLER_REGISTRY_URL</key>");
149
- expect(plist).toContain("<string>https://tb-wallet-bootstrap.fly.dev/registry/sellers</string>");
152
+ expect(plist).toContain("<string>https://registry.tokenbuddy.ai/v1/registry.json</string>");
150
153
  expect(plist).toContain("<key>PATH</key>");
151
154
  expect(plist).toContain("<string>/opt/homebrew/bin:/usr/bin:/bin</string>");
152
155
  });
@@ -160,7 +163,7 @@ describe("TokenBuddy CLI command surface", () => {
160
163
  stderrPath: "/Users/example/.tokenbuddy-store/tb-proxyd.stderr.log",
161
164
  controlPort: 17820,
162
165
  proxyPort: 17821,
163
- sellerRegistryUrl: "https://tb-wallet-bootstrap.fly.dev/registry/sellers",
166
+ sellerRegistryUrl: "https://registry.tokenbuddy.ai/v1/registry.json",
164
167
  clawtipProofCommand: "/Users/example/bin/tokenbuddy-clawtip-proof-openclaw.sh",
165
168
  clawtipProofTimeoutMs: 180000,
166
169
  });
@@ -173,6 +176,63 @@ describe("TokenBuddy CLI command surface", () => {
173
176
  expect(plist).not.toContain("PAYMENT_PROOF");
174
177
  });
175
178
 
179
+ test("web init launcher installs the default ClawTip proof provider into launchd", async () => {
180
+ const writtenFiles: Array<{ filePath: string; content: string }> = [];
181
+ const result = await runWebInitLauncher({
182
+ platform: "darwin",
183
+ controlPort: 3210,
184
+ proxyPort: 3211,
185
+ sellerRegistryUrl: "https://registry.example.test/sellers",
186
+ homeDir: "/Users/example",
187
+ nodePath: "/opt/node",
188
+ scriptPath: "/opt/tokenbuddy/dist/src/tb-proxyd.js",
189
+ pathEnv: "/usr/bin:/bin",
190
+ mkdirSync: () => undefined,
191
+ writeFileSync: (filePath, content) => {
192
+ writtenFiles.push({ filePath, content });
193
+ },
194
+ installLaunchAgent: () => undefined,
195
+ waitForDaemonStatus: async () => ({
196
+ running: true,
197
+ status: { pid: 42, controlPort: 3210, proxyPort: 3211 }
198
+ }),
199
+ fetchInitState: async () => ({
200
+ freshMachine: true,
201
+ setup: { status: "not_started", version: 1, completedSteps: [] }
202
+ }),
203
+ launchControlUi: () => "http://127.0.0.1:3210/init"
204
+ });
205
+
206
+ expect(result.serviceInstalled).toBe(true);
207
+ expect(writtenFiles[0].content).toContain("<key>TB_PROXYD_CLAWTIP_PROOF_COMMAND</key>");
208
+ expect(writtenFiles[0].content).toContain("tb-clawtip-proof.js");
209
+ });
210
+
211
+ test("installLaunchAgent reloads an existing LaunchAgent so new plist env takes effect", () => {
212
+ const calls: Array<{ args: string[]; ignoreFailure?: boolean }> = [];
213
+ let bootstrapAttempts = 0;
214
+ installLaunchAgentWithRunner("/Users/example/Library/LaunchAgents/com.tokenbuddy.proxyd.plist", "com.tokenbuddy.proxyd", (args, ignoreFailure) => {
215
+ calls.push({ args, ignoreFailure });
216
+ if (args[0] === "bootstrap") {
217
+ bootstrapAttempts += 1;
218
+ if (bootstrapAttempts === 1) {
219
+ throw new Error("launchctl bootstrap transient failure");
220
+ }
221
+ }
222
+ });
223
+
224
+ expect(calls).toEqual([
225
+ { args: ["bootout", expect.stringMatching(/^gui\/\d+\/com\.tokenbuddy\.tb-proxyd$/)], ignoreFailure: true },
226
+ { args: ["bootout", expect.stringMatching(/^gui\/\d+\/homebrew\.mxcl\.tokenbuddy$/)], ignoreFailure: true },
227
+ { args: ["print", expect.stringMatching(/^gui\/\d+\/com\.tokenbuddy\.proxyd$/)], ignoreFailure: undefined },
228
+ { args: ["bootout", expect.stringMatching(/^gui\/\d+\/com\.tokenbuddy\.proxyd$/)], ignoreFailure: true },
229
+ { args: ["bootstrap", expect.stringMatching(/^gui\/\d+$/), "/Users/example/Library/LaunchAgents/com.tokenbuddy.proxyd.plist"], ignoreFailure: undefined },
230
+ { args: ["bootstrap", expect.stringMatching(/^gui\/\d+$/), "/Users/example/Library/LaunchAgents/com.tokenbuddy.proxyd.plist"], ignoreFailure: undefined },
231
+ { args: ["kickstart", "-k", expect.stringMatching(/^gui\/\d+\/com\.tokenbuddy\.proxyd$/)], ignoreFailure: undefined },
232
+ ]);
233
+ expect(bootstrapAttempts).toBe(2);
234
+ });
235
+
176
236
  test("restartLaunchAgent kickstarts the installed LaunchAgent and waits for readiness", async () => {
177
237
  const launchctlCalls: string[][] = [];
178
238
  const result = await restartLaunchAgent(17820, {
@@ -1145,16 +1205,47 @@ describe("TokenBuddy init payment options", () => {
1145
1205
  payTo: "pay-to-test",
1146
1206
  encryptedData: "ciphertext",
1147
1207
  indicator: "indicator_error",
1148
- slug: "tb-wallet-bootstrap",
1149
- skillId: "si-tb-wallet-bootstrap",
1208
+ slug: "tb-registry",
1209
+ skillId: "si-tb-registry",
1150
1210
  description: "TokenBuddy ClawTip wallet activation",
1151
- resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
1211
+ resourceUrl: "https://tb-registry.fly.dev"
1152
1212
  }, {
1153
1213
  home: path.join(TEMP_STORE_ROOT, "clawtip-error-home"),
1154
1214
  runClawtipCommand: async () => "ClawTip 返回错误:商家信息有误",
1155
1215
  })).rejects.toThrow("ClawTip pay failed: ClawTip 返回错误:商家信息有误");
1156
1216
  });
1157
1217
 
1218
+ test("treats ClawTip returned payment failure messages as failed proofs", async () => {
1219
+ const parsed = parseClawtipCliOutput("返回消息: 收付款方账户不能相同\n已获取到支付凭证");
1220
+
1221
+ expect(parsed.failureMessage).toContain("收付款方账户不能相同");
1222
+ await expect(createClawtipPaymentProof({
1223
+ paymentInstructions: {
1224
+ method: "clawtip",
1225
+ clawtip: {
1226
+ orderNo: "order_same_account",
1227
+ amountFen: 2,
1228
+ payTo: "pay-to-test",
1229
+ encryptedData: "ciphertext",
1230
+ indicator: "indicator_same_account",
1231
+ slug: "tb-seller",
1232
+ skillId: "si-tb-seller",
1233
+ description: "TokenBuddy inference purchase",
1234
+ resourceUrl: "https://seller.example.test"
1235
+ }
1236
+ }
1237
+ }, {
1238
+ home: path.join(TEMP_STORE_ROOT, "clawtip-same-account-home"),
1239
+ runClawtipCommand: async () => "返回消息: 收付款方账户不能相同\n已获取到支付凭证",
1240
+ })).rejects.toThrow("ClawTip pay failed: 返回消息: 收付款方账户不能相同");
1241
+ });
1242
+
1243
+ test("accepts ClawTip returned success messages with payment details", () => {
1244
+ const parsed = parseClawtipCliOutput("返回消息: 本次交易在授权范围内,ClawTip付费成功。支付0.02元,余额0.50元\n已获取到支付凭证");
1245
+
1246
+ expect(parsed.failureMessage).toBeUndefined();
1247
+ });
1248
+
1158
1249
  test("uses only ClawTip CLI media paths for QR resolution", () => {
1159
1250
  expect(resolveClawtipQrMediaPath({
1160
1251
  authUrl: "https://clawtip.jd.com/qrcode?clawtipId=device-789",
@@ -1192,17 +1283,17 @@ describe("TokenBuddy init payment options", () => {
1192
1283
 
1193
1284
  test("normalizes the bootstrap resource URL away from the public registry endpoint", () => {
1194
1285
  expect(normalizeClawtipBootstrapResourceUrl(
1195
- "https://tb-wallet-bootstrap.fly.dev",
1196
- "https://tb-wallet-bootstrap.fly.dev/registry/sellers"
1197
- )).toBe("https://tb-wallet-bootstrap.fly.dev");
1286
+ "https://tb-registry.fly.dev",
1287
+ "https://tb-registry.fly.dev/registry/sellers"
1288
+ )).toBe("https://tb-registry.fly.dev");
1198
1289
 
1199
1290
  expect(normalizeClawtipBootstrapResourceUrl(
1200
- "https://tb-wallet-bootstrap.fly.dev/base",
1201
- "https://tb-wallet-bootstrap.fly.dev/registry/sellers"
1202
- )).toBe("https://tb-wallet-bootstrap.fly.dev/base");
1291
+ "https://tb-registry.fly.dev/base",
1292
+ "https://tb-registry.fly.dev/registry/sellers"
1293
+ )).toBe("https://tb-registry.fly.dev/base");
1203
1294
 
1204
1295
  expect(normalizeClawtipBootstrapResourceUrl(
1205
- "https://tb-wallet-bootstrap.fly.dev",
1296
+ "https://tb-registry.fly.dev",
1206
1297
  "https://example.test/pay"
1207
1298
  )).toBe("https://example.test/pay");
1208
1299
  });
@@ -1215,7 +1306,7 @@ describe("TokenBuddy init payment options", () => {
1215
1306
  orderNo: "order_placeholder",
1216
1307
  indicator: "indicator_placeholder",
1217
1308
  payTo: "bootstrap-pay-to",
1218
- resourceUrl: "https://tb-wallet-bootstrap.fly.dev/registry/sellers",
1309
+ resourceUrl: "https://registry.tokenbuddy.ai/v1/registry.json",
1219
1310
  }
1220
1311
  }), {
1221
1312
  status: 200,
@@ -1223,7 +1314,7 @@ describe("TokenBuddy init payment options", () => {
1223
1314
  })) as typeof fetch;
1224
1315
 
1225
1316
  try {
1226
- await expect(fetchClawtipBootstrap("https://tb-wallet-bootstrap.fly.dev")).rejects.toThrow(
1317
+ await expect(fetchClawtipBootstrap("https://tb-registry.fly.dev")).rejects.toThrow(
1227
1318
  "ClawTip bootstrap service is misconfigured: payTo is still the placeholder"
1228
1319
  );
1229
1320
  } finally {
@@ -1241,10 +1332,10 @@ describe("TokenBuddy init payment options", () => {
1241
1332
  payTo: "pay-to-test",
1242
1333
  encryptedData: "ciphertext",
1243
1334
  indicator: "indicator_123",
1244
- slug: "tb-wallet-bootstrap",
1245
- skillId: "si-tb-wallet-bootstrap",
1335
+ slug: "tb-registry",
1336
+ skillId: "si-tb-registry",
1246
1337
  description: "TokenBuddy ClawTip wallet activation",
1247
- resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
1338
+ resourceUrl: "https://tb-registry.fly.dev"
1248
1339
  }, home);
1249
1340
 
1250
1341
  expect(orderFile).toBe(path.join(
@@ -1258,13 +1349,13 @@ describe("TokenBuddy init payment options", () => {
1258
1349
 
1259
1350
  const saved = JSON.parse(fs.readFileSync(orderFile, "utf8"));
1260
1351
  expect(saved).toEqual(expect.objectContaining({
1261
- "skill-id": "si-tb-wallet-bootstrap",
1352
+ "skill-id": "si-tb-registry",
1262
1353
  order_no: "order_123",
1263
1354
  amount: 1,
1264
1355
  encrypted_data: "ciphertext",
1265
1356
  pay_to: "pay-to-test",
1266
- slug: "tb-wallet-bootstrap",
1267
- resource_url: "https://tb-wallet-bootstrap.fly.dev"
1357
+ slug: "tb-registry",
1358
+ resource_url: "https://tb-registry.fly.dev"
1268
1359
  }));
1269
1360
  });
1270
1361
 
@@ -1278,10 +1369,10 @@ describe("TokenBuddy init payment options", () => {
1278
1369
  payTo: "pay-to-test",
1279
1370
  encryptedData: "ciphertext",
1280
1371
  indicator: "indicator_456",
1281
- slug: "tb-wallet-bootstrap",
1282
- skillId: "si-tb-wallet-bootstrap",
1372
+ slug: "tb-registry",
1373
+ skillId: "si-tb-registry",
1283
1374
  description: "TokenBuddy ClawTip wallet activation",
1284
- resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
1375
+ resourceUrl: "https://tb-registry.fly.dev"
1285
1376
  }, {
1286
1377
  home,
1287
1378
  runClawtipCommand: async () => {
@@ -1312,6 +1403,65 @@ describe("TokenBuddy init payment options", () => {
1312
1403
  expect(readClawtipPayCredential(activation.orderFile)).toBe("credential_456");
1313
1404
  });
1314
1405
 
1406
+ test("creates a ClawTip payment proof from seller payment instructions", async () => {
1407
+ const home = path.join(TEMP_STORE_ROOT, "clawtip-proof-home");
1408
+ rmDir(home);
1409
+
1410
+ const proof = await createClawtipPaymentProof({
1411
+ paymentInstructions: {
1412
+ method: "clawtip",
1413
+ clawtip: {
1414
+ orderNo: "order_789",
1415
+ amountFen: 2,
1416
+ payTo: "pay-to-test",
1417
+ encryptedData: "ciphertext",
1418
+ indicator: "indicator_789",
1419
+ slug: "tb-seller",
1420
+ skillId: "si-tb-seller",
1421
+ description: "TokenBuddy inference purchase",
1422
+ resourceUrl: "https://seller.example.test"
1423
+ }
1424
+ }
1425
+ }, {
1426
+ home,
1427
+ runClawtipCommand: async (args) => {
1428
+ expect(args).toEqual([
1429
+ "--yes",
1430
+ "@clawtip/clawtip-cli@1.0.4",
1431
+ "pay",
1432
+ "-o",
1433
+ "order_789",
1434
+ "-i",
1435
+ "indicator_789",
1436
+ "-v",
1437
+ "1.0.12"
1438
+ ]);
1439
+ const orderFile = path.join(
1440
+ home,
1441
+ ".openclaw",
1442
+ "skills",
1443
+ "orders",
1444
+ "indicator_789",
1445
+ "order_789.json"
1446
+ );
1447
+ const order = JSON.parse(fs.readFileSync(orderFile, "utf8"));
1448
+ expect(order).toEqual(expect.objectContaining({
1449
+ order_no: "order_789",
1450
+ amount: 2,
1451
+ encrypted_data: "ciphertext",
1452
+ pay_to: "pay-to-test",
1453
+ slug: "tb-seller",
1454
+ resource_url: "https://seller.example.test"
1455
+ }));
1456
+ order.payCredential = "credential_789";
1457
+ fs.writeFileSync(orderFile, JSON.stringify(order, null, 2), "utf8");
1458
+ return "已获取到支付凭证";
1459
+ }
1460
+ });
1461
+
1462
+ expect(proof).toBe("credential_789");
1463
+ });
1464
+
1315
1465
  test("recovers the latest generated ClawTip QR media path when pay output omits MEDIA", async () => {
1316
1466
  const home = path.join(TEMP_STORE_ROOT, "clawtip-bootstrap-qr-home");
1317
1467
  rmDir(home);
@@ -1322,10 +1472,10 @@ describe("TokenBuddy init payment options", () => {
1322
1472
  payTo: "pay-to-test",
1323
1473
  encryptedData: "ciphertext",
1324
1474
  indicator: "indicator_789",
1325
- slug: "tb-wallet-bootstrap",
1326
- skillId: "si-tb-wallet-bootstrap",
1475
+ slug: "tb-registry",
1476
+ skillId: "si-tb-registry",
1327
1477
  description: "TokenBuddy ClawTip wallet activation",
1328
- resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
1478
+ resourceUrl: "https://tb-registry.fly.dev"
1329
1479
  }, {
1330
1480
  home,
1331
1481
  runClawtipCommand: async () => {
@@ -1358,10 +1508,10 @@ describe("TokenBuddy init payment options", () => {
1358
1508
  payTo: "pay-to-test",
1359
1509
  encryptedData: "ciphertext",
1360
1510
  indicator: "indicator_credential_qr",
1361
- slug: "tb-wallet-bootstrap",
1362
- skillId: "si-tb-wallet-bootstrap",
1511
+ slug: "tb-registry",
1512
+ skillId: "si-tb-registry",
1363
1513
  description: "TokenBuddy ClawTip wallet activation",
1364
- resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
1514
+ resourceUrl: "https://tb-registry.fly.dev"
1365
1515
  }, {
1366
1516
  home,
1367
1517
  runClawtipCommand: async () => {
@@ -2803,17 +2953,31 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
2803
2953
  });
2804
2954
 
2805
2955
  test("fails closed when no compatible seller can serve the requested model", async () => {
2956
+ const requestId = "missing_model_route_diagnostics";
2806
2957
  const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
2807
2958
  method: "POST",
2808
2959
  headers: { "Content-Type": "application/json" },
2809
2960
  body: JSON.stringify({
2810
2961
  model: "missing-model",
2811
- messages: [{ role: "user", content: "hello" }]
2962
+ messages: [{ role: "user", content: "hello" }],
2963
+ requestId
2812
2964
  })
2813
2965
  });
2814
2966
  expect(response.status).toBe(502);
2815
2967
  const data = await response.json() as any;
2816
2968
  expect(data.error.message).toContain("no compatible seller");
2969
+
2970
+ await new Promise((resolve) => setTimeout(resolve, 100));
2971
+ const logFile = resolveModuleLogFile("tb-proxyd");
2972
+ const logs = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
2973
+ const requestLogs = logs
2974
+ .split("\n")
2975
+ .filter((line) => line.includes(`requestId=${requestId}`))
2976
+ .join("\n");
2977
+ expect(requestLogs).toContain("event=route.candidates.prewarmed");
2978
+ expect(requestLogs).toContain("routeReason=no_compatible_seller");
2979
+ expect(requestLogs).toContain("sellerCount=0");
2980
+ expect(requestLogs).toContain("candidateDiagnostics=");
2817
2981
  });
2818
2982
  });
2819
2983
 
@@ -2827,6 +2991,7 @@ describe("TokenBuddy seller routing strategies", () => {
2827
2991
  let primaryPurchaseSucceeds = false;
2828
2992
  let primaryInferenceFails = false;
2829
2993
  let primaryInferenceBusy = false;
2994
+ let primaryInferenceDelayMs = 0;
2830
2995
  const dbPath = path.resolve(__dirname, "../../data-test/manual-routing-test.db");
2831
2996
  const routeEvents = (): Array<{ seller: string; url?: string }> => events
2832
2997
  .filter((event) => event.url !== "/primary/health" && event.url !== "/backup/health");
@@ -2926,6 +3091,9 @@ describe("TokenBuddy seller routing strategies", () => {
2926
3091
 
2927
3092
  if (req.url === "/primary/v1/chat/completions") {
2928
3093
  events.push({ seller: "primary-seller", url: req.url });
3094
+ if (primaryInferenceDelayMs > 0) {
3095
+ await new Promise((resolve) => setTimeout(resolve, primaryInferenceDelayMs));
3096
+ }
2929
3097
  if (primaryInferenceBusy) {
2930
3098
  res.statusCode = 429;
2931
3099
  res.end(JSON.stringify({ error: { code: "busy_capacity", message: "primary seller capacity is full" } }));
@@ -3003,6 +3171,7 @@ describe("TokenBuddy seller routing strategies", () => {
3003
3171
  primaryPurchaseSucceeds = false;
3004
3172
  primaryInferenceFails = false;
3005
3173
  primaryInferenceBusy = false;
3174
+ primaryInferenceDelayMs = 0;
3006
3175
  rmSqliteFiles(dbPath);
3007
3176
  const store = new BuyerStore({ dbPath });
3008
3177
  store.savePayment({
@@ -3309,6 +3478,13 @@ describe("TokenBuddy seller routing strategies", () => {
3309
3478
  .join("\n");
3310
3479
  expect(requestLogs).toContain("event=route.failover.retry_scheduled");
3311
3480
  expect(requestLogs).toContain("event=route.failover.triggered");
3481
+ expect(requestLogs).toContain("event=route.candidates.prewarmed");
3482
+ expect(requestLogs).toContain("event=route.selected");
3483
+ expect(requestLogs).toContain("routePlanSource=registry_fallback");
3484
+ expect(requestLogs).toContain("routePlanReason=fullAuto:balanced:routes_2");
3485
+ expect(requestLogs).toContain("candidateDiagnostics=");
3486
+ expect(requestLogs).toContain("hasNextRoute=true");
3487
+ expect(requestLogs).toContain("attemptNumber=");
3312
3488
  expect(requestLogs).toContain("event=purchase.create.started");
3313
3489
  expect(requestLogs).toContain("event=purchase.ledger.recorded");
3314
3490
  expect(requestLogs).toContain("event=inference.ledger.recorded");
@@ -3373,6 +3549,112 @@ describe("TokenBuddy seller routing strategies", () => {
3373
3549
  ]);
3374
3550
  });
3375
3551
 
3552
+ test("fullAuto routing skips locally saturated sellers for concurrent requests", async () => {
3553
+ daemon.stop();
3554
+ events.length = 0;
3555
+ primaryPurchaseSucceeds = true;
3556
+ primaryInferenceDelayMs = 250;
3557
+ daemon = new TokenbuddyDaemon({
3558
+ controlPort: 0,
3559
+ proxyPort: 0,
3560
+ dbPath,
3561
+ sellerRegistryUrl: `http://127.0.0.1:${sellerPort}/registry/sellers`,
3562
+ sellerRouting: {
3563
+ mode: "fullAuto",
3564
+ scorer: "balanced"
3565
+ },
3566
+ sellerConcurrency: {
3567
+ enabled: true,
3568
+ maxInFlightPerSeller: 1,
3569
+ leaseTtlMs: 5000
3570
+ }
3571
+ });
3572
+ daemon.start();
3573
+ daemonControlPort = ((daemon as any).controlServer.address() as AddressInfo).port;
3574
+ daemonProxyPort = ((daemon as any).proxyServer.address() as AddressInfo).port;
3575
+ (daemon as any).prewarmCache.commitWarm({
3576
+ modelId: "gpt-manual",
3577
+ protocol: "chat_completions",
3578
+ paymentMethod: "mock",
3579
+ ttlMs: 600000,
3580
+ candidates: [
3581
+ {
3582
+ sellerId: "primary-seller",
3583
+ url: `http://127.0.0.1:${sellerPort}/primary`,
3584
+ healthScore: 100,
3585
+ avgLatencyMs: 10
3586
+ },
3587
+ {
3588
+ sellerId: "backup-seller",
3589
+ url: `http://127.0.0.1:${sellerPort}/backup`,
3590
+ healthScore: 90,
3591
+ avgLatencyMs: 20
3592
+ }
3593
+ ]
3594
+ });
3595
+ (daemon as any).sellerPool.sync();
3596
+
3597
+ const first = fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
3598
+ method: "POST",
3599
+ headers: { "Content-Type": "application/json" },
3600
+ body: JSON.stringify({
3601
+ model: "gpt-manual",
3602
+ messages: [{ role: "user", content: "hold primary local capacity" }],
3603
+ requestId: "local_capacity_primary"
3604
+ })
3605
+ });
3606
+
3607
+ for (let i = 0; i < 30; i += 1) {
3608
+ if (events.some((event) => event.url === "/primary/v1/chat/completions")) {
3609
+ break;
3610
+ }
3611
+ await new Promise((resolve) => setTimeout(resolve, 10));
3612
+ }
3613
+ expect(events.some((event) => event.url === "/primary/v1/chat/completions")).toBe(true);
3614
+
3615
+ const second = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
3616
+ method: "POST",
3617
+ headers: { "Content-Type": "application/json" },
3618
+ body: JSON.stringify({
3619
+ model: "gpt-manual",
3620
+ messages: [{ role: "user", content: "use backup while primary is locally full" }],
3621
+ requestId: "local_capacity_backup"
3622
+ })
3623
+ });
3624
+
3625
+ expect(second.ok).toBe(true);
3626
+ expect((await second.json() as any).id).toBe("backup-chat");
3627
+
3628
+ const firstResponse = await first;
3629
+ expect(firstResponse.ok).toBe(true);
3630
+ expect((await firstResponse.json() as any).id).toBe("primary-chat");
3631
+
3632
+ const primaryInferenceCalls = routeEvents().filter((event) => event.url === "/primary/v1/chat/completions");
3633
+ const backupInferenceCalls = routeEvents().filter((event) => event.url === "/backup/v1/chat/completions");
3634
+ expect(primaryInferenceCalls).toHaveLength(1);
3635
+ expect(backupInferenceCalls).toHaveLength(1);
3636
+
3637
+ const status = await (await fetch(`http://127.0.0.1:${daemonControlPort}/status`)).json() as any;
3638
+ expect(status.sellerConcurrency.enabled).toBe(true);
3639
+ expect(status.sellerConcurrency.active).toEqual([]);
3640
+
3641
+ await new Promise((resolve) => setTimeout(resolve, 100));
3642
+ const logFile = resolveModuleLogFile("tb-proxyd");
3643
+ const logs = fs.existsSync(logFile) ? fs.readFileSync(logFile, "utf8") : "";
3644
+ const backupRequestLogs = logs
3645
+ .split("\n")
3646
+ .filter((line) => line.includes("requestId=local_capacity_backup"))
3647
+ .join("\n");
3648
+ expect(backupRequestLogs).toContain("event=route.candidates.prewarmed");
3649
+ expect(backupRequestLogs).toContain("event=route.selected");
3650
+ expect(backupRequestLogs).toContain("primary-seller");
3651
+ expect(backupRequestLogs).toContain("sellerKey=backup-seller");
3652
+ expect(backupRequestLogs).toContain("\"blockedLocalConcurrencyCount\":1");
3653
+ expect(backupRequestLogs).toContain("\"prewarmBlockedSellerIds\":[\"primary-seller\"]");
3654
+ expect(backupRequestLogs).toContain("routePlanSellerCount=1");
3655
+ expect(backupRequestLogs).toContain("localConcurrencyEnabled=true");
3656
+ });
3657
+
3376
3658
  test("fullAuto routing logs purchase failure failover before trying the backup seller", async () => {
3377
3659
  daemon.stop();
3378
3660
  events.length = 0;
@@ -3420,7 +3702,7 @@ describe("TokenBuddy seller routing strategies", () => {
3420
3702
  .join("\n");
3421
3703
  expect(requestLogs).toContain("event=route.failover.triggered");
3422
3704
  expect(requestLogs).toContain("reason=purchase_failed");
3423
- expect(requestLogs).toContain("controllerAction=retry_same_seller");
3705
+ expect(requestLogs).toContain("controllerAction=failover_next");
3424
3706
  expect(requestLogs).not.toContain("event=route.failover.retry_scheduled");
3425
3707
  });
3426
3708
  });