@tokenbuddy/tokenbuddy 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tb-clawtip-proof.js +2 -0
- package/dist/src/clawtip-bootstrap.d.ts +1 -0
- package/dist/src/clawtip-bootstrap.d.ts.map +1 -1
- package/dist/src/clawtip-bootstrap.js +1 -0
- package/dist/src/clawtip-bootstrap.js.map +1 -1
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +172 -51
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +6 -0
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +562 -292
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +5 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -1
- package/dist/src/init-clawtip-activation.js +61 -1
- package/dist/src/init-clawtip-activation.js.map +1 -1
- package/dist/src/package-update.d.ts +60 -0
- package/dist/src/package-update.d.ts.map +1 -0
- package/dist/src/package-update.js +218 -0
- package/dist/src/package-update.js.map +1 -0
- package/dist/src/registry-trust.d.ts +7 -0
- package/dist/src/registry-trust.d.ts.map +1 -0
- package/dist/src/registry-trust.js +37 -0
- package/dist/src/registry-trust.js.map +1 -0
- package/dist/src/route-failover.d.ts +2 -2
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js +11 -0
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +20 -0
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +41 -4
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-concurrency-limiter.d.ts +36 -0
- package/dist/src/seller-concurrency-limiter.d.ts.map +1 -0
- package/dist/src/seller-concurrency-limiter.js +126 -0
- package/dist/src/seller-concurrency-limiter.js.map +1 -0
- package/dist/src/seller-pool.d.ts +7 -1
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +18 -0
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +21 -0
- package/dist/src/seller-route-planner.d.ts.map +1 -1
- package/dist/src/seller-route-planner.js +98 -20
- package/dist/src/seller-route-planner.js.map +1 -1
- package/dist/src/tb-clawtip-proof.d.ts +3 -0
- package/dist/src/tb-clawtip-proof.d.ts.map +1 -0
- package/dist/src/tb-clawtip-proof.js +24 -0
- package/dist/src/tb-clawtip-proof.js.map +1 -0
- package/dist/src/tb-proxyd.js +45 -3
- package/dist/src/tb-proxyd.js.map +1 -1
- package/package.json +3 -2
- package/src/clawtip-bootstrap.ts +1 -0
- package/src/cli.ts +200 -47
- package/src/daemon.ts +347 -50
- package/src/init-clawtip-activation.ts +77 -1
- package/src/package-update.ts +311 -0
- package/src/registry-trust.ts +51 -0
- package/src/route-failover.ts +14 -2
- package/src/seller-catalog.ts +67 -4
- package/src/seller-concurrency-limiter.ts +161 -0
- package/src/seller-pool.ts +20 -0
- package/src/seller-route-planner.ts +142 -20
- package/src/tb-clawtip-proof.ts +28 -0
- package/src/tb-proxyd.ts +48 -3
- package/static/ui/assets/index-Bzbrp7Qe.css +1 -0
- package/static/ui/assets/index-UAfOhbwC.js +236 -0
- package/static/ui/assets/index-UAfOhbwC.js.map +1 -0
- package/static/ui/index.html +2 -2
- package/tests/cli-routing.test.ts +37 -4
- package/tests/control-plane-ui-endpoints.test.ts +7 -7
- package/tests/daemon-trusted-registry-cache.test.ts +132 -0
- package/tests/e2e.test.ts +14 -1
- package/tests/package-update.test.ts +147 -0
- package/tests/registry-trust.test.ts +28 -0
- package/tests/route-failover.test.ts +13 -0
- package/tests/seller-catalog-413.test.ts +60 -1
- package/tests/seller-concurrency-limiter.test.ts +83 -0
- package/tests/seller-pool.test.ts +23 -0
- package/tests/seller-route-planner.test.ts +78 -0
- package/tests/tokenbuddy.test.ts +316 -34
- package/static/ui/assets/index-1uuyCCzj.css +0 -1
- package/static/ui/assets/index-cm_EgQZ-.js +0 -236
- package/static/ui/assets/index-cm_EgQZ-.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
|
});
|
package/tests/tokenbuddy.test.ts
CHANGED
|
@@ -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://
|
|
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://
|
|
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://
|
|
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-
|
|
1149
|
-
skillId: "si-tb-
|
|
1208
|
+
slug: "tb-registry",
|
|
1209
|
+
skillId: "si-tb-registry",
|
|
1150
1210
|
description: "TokenBuddy ClawTip wallet activation",
|
|
1151
|
-
resourceUrl: "https://tb-
|
|
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-
|
|
1196
|
-
"https://tb-
|
|
1197
|
-
)).toBe("https://tb-
|
|
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-
|
|
1201
|
-
"https://tb-
|
|
1202
|
-
)).toBe("https://tb-
|
|
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-
|
|
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://
|
|
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-
|
|
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-
|
|
1245
|
-
skillId: "si-tb-
|
|
1335
|
+
slug: "tb-registry",
|
|
1336
|
+
skillId: "si-tb-registry",
|
|
1246
1337
|
description: "TokenBuddy ClawTip wallet activation",
|
|
1247
|
-
resourceUrl: "https://tb-
|
|
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-
|
|
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-
|
|
1267
|
-
resource_url: "https://tb-
|
|
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-
|
|
1282
|
-
skillId: "si-tb-
|
|
1372
|
+
slug: "tb-registry",
|
|
1373
|
+
skillId: "si-tb-registry",
|
|
1283
1374
|
description: "TokenBuddy ClawTip wallet activation",
|
|
1284
|
-
resourceUrl: "https://tb-
|
|
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-
|
|
1326
|
-
skillId: "si-tb-
|
|
1475
|
+
slug: "tb-registry",
|
|
1476
|
+
skillId: "si-tb-registry",
|
|
1327
1477
|
description: "TokenBuddy ClawTip wallet activation",
|
|
1328
|
-
resourceUrl: "https://tb-
|
|
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-
|
|
1362
|
-
skillId: "si-tb-
|
|
1511
|
+
slug: "tb-registry",
|
|
1512
|
+
skillId: "si-tb-registry",
|
|
1363
1513
|
description: "TokenBuddy ClawTip wallet activation",
|
|
1364
|
-
resourceUrl: "https://tb-
|
|
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=
|
|
3705
|
+
expect(requestLogs).toContain("controllerAction=failover_next");
|
|
3424
3706
|
expect(requestLogs).not.toContain("event=route.failover.retry_scheduled");
|
|
3425
3707
|
});
|
|
3426
3708
|
});
|