@tokenbuddy/tokenbuddy 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.
- package/dist/src/buyer-store.d.ts +7 -2
- package/dist/src/buyer-store.js +46 -7
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +15 -7
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/dist/src/workdir.d.ts +10 -0
- package/dist/src/workdir.js +26 -0
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- package/tsconfig.json +0 -8
package/tests/e2e.test.ts
DELETED
|
@@ -1,366 +0,0 @@
|
|
|
1
|
-
import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
|
|
2
|
-
import { buildSellerApp } from "../../seller/src/server.js";
|
|
3
|
-
import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
4
|
-
import { BuyerStore } from "../src/buyer-store.js";
|
|
5
|
-
import { AdminClient } from "../../admin-cli/src/client.js";
|
|
6
|
-
import { PROVIDER_MODE_CONFIG_KEY } from "../src/provider-routing-config.js";
|
|
7
|
-
import * as path from "path";
|
|
8
|
-
import * as fs from "fs";
|
|
9
|
-
import http from "http";
|
|
10
|
-
import { AddressInfo } from "net";
|
|
11
|
-
|
|
12
|
-
const TEMP_DIR = path.resolve(__dirname, "../../data-test");
|
|
13
|
-
const TEMP_BOOTSTRAP_JSON = path.join(TEMP_DIR, "sellers-e2e.json");
|
|
14
|
-
const TEMP_SELLER_DB = path.join(TEMP_DIR, "seller-e2e.db");
|
|
15
|
-
const TEMP_BUYER_DB = path.join(TEMP_DIR, "buyer-e2e.db");
|
|
16
|
-
|
|
17
|
-
describe("TokenBuddy Full End-to-End Integration Flow Tests", () => {
|
|
18
|
-
let bootstrapServer: http.Server;
|
|
19
|
-
let sellerServer: http.Server;
|
|
20
|
-
let upstreamServer: http.Server;
|
|
21
|
-
let daemon: TokenbuddyDaemon;
|
|
22
|
-
|
|
23
|
-
let bootstrapPort: number;
|
|
24
|
-
let sellerPort: number;
|
|
25
|
-
let upstreamPort: number;
|
|
26
|
-
let daemonControlPort: number;
|
|
27
|
-
let daemonProxyPort: number;
|
|
28
|
-
|
|
29
|
-
let sellerCloseFn: () => void;
|
|
30
|
-
|
|
31
|
-
beforeAll(async () => {
|
|
32
|
-
// Ensure clean temp dir
|
|
33
|
-
if (!fs.existsSync(TEMP_DIR)) {
|
|
34
|
-
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
35
|
-
}
|
|
36
|
-
fs.writeFileSync(TEMP_BOOTSTRAP_JSON, JSON.stringify({
|
|
37
|
-
version: 1,
|
|
38
|
-
sellers: [
|
|
39
|
-
{
|
|
40
|
-
id: "seller-e2e-seed",
|
|
41
|
-
name: "Seller E2E Seed",
|
|
42
|
-
url: "http://127.0.0.1:1",
|
|
43
|
-
supportedProtocols: ["chat_completions"],
|
|
44
|
-
paymentMethods: ["mock"],
|
|
45
|
-
models: ["gpt-4"]
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
}), "utf8");
|
|
49
|
-
|
|
50
|
-
if (fs.existsSync(TEMP_SELLER_DB)) {
|
|
51
|
-
try { fs.unlinkSync(TEMP_SELLER_DB); } catch (e) {}
|
|
52
|
-
}
|
|
53
|
-
if (fs.existsSync(TEMP_BUYER_DB)) {
|
|
54
|
-
try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
|
|
55
|
-
}
|
|
56
|
-
const buyerStore = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
57
|
-
buyerStore.savePayment({
|
|
58
|
-
method: "mock",
|
|
59
|
-
enabled: true,
|
|
60
|
-
isDefault: true,
|
|
61
|
-
config: { channel: "e2e-test" }
|
|
62
|
-
});
|
|
63
|
-
buyerStore.saveDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY, {
|
|
64
|
-
mode: "auto",
|
|
65
|
-
updatedAt: new Date().toISOString()
|
|
66
|
-
});
|
|
67
|
-
buyerStore.close();
|
|
68
|
-
|
|
69
|
-
// 1. Launch Mock Upstream OpenAI completions server
|
|
70
|
-
upstreamServer = http.createServer((req, res) => {
|
|
71
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
72
|
-
if (req.url === "/v1/chat/completions") {
|
|
73
|
-
res.end(JSON.stringify({
|
|
74
|
-
id: "chatcmpl-e2e-ok",
|
|
75
|
-
usage: { prompt_tokens: 20, completion_tokens: 30 },
|
|
76
|
-
choices: [{ message: { role: "assistant", content: "Hello back!" } }]
|
|
77
|
-
}));
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
res.end("{}");
|
|
81
|
-
});
|
|
82
|
-
await new Promise<void>((resolve) => upstreamServer.listen(0, "127.0.0.1", () => resolve()));
|
|
83
|
-
upstreamPort = (upstreamServer.address() as AddressInfo).port;
|
|
84
|
-
|
|
85
|
-
// 2. Launch Wallet Bootstrap server
|
|
86
|
-
const bootstrapApp = buildBootstrapApp({
|
|
87
|
-
payTo: "test-activation-pay-to",
|
|
88
|
-
sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
|
|
89
|
-
skillSlug: "test-activation-slug",
|
|
90
|
-
skillId: "test-activation-id",
|
|
91
|
-
description: "Activate registration",
|
|
92
|
-
resourceUrl: "http://127.0.0.1/verify",
|
|
93
|
-
activationFeeFen: 1,
|
|
94
|
-
microsPerFen: 1000000,
|
|
95
|
-
sellerRegistryPath: TEMP_BOOTSTRAP_JSON,
|
|
96
|
-
operatorSecret: "op-secret",
|
|
97
|
-
allowLocalSellerUrls: true
|
|
98
|
-
});
|
|
99
|
-
bootstrapServer = http.createServer(bootstrapApp);
|
|
100
|
-
await new Promise<void>((resolve) => bootstrapServer.listen(0, "127.0.0.1", () => resolve()));
|
|
101
|
-
bootstrapPort = (bootstrapServer.address() as AddressInfo).port;
|
|
102
|
-
|
|
103
|
-
// 3. Launch Seller server
|
|
104
|
-
const { app: sellerApp, close: closeDb } = buildSellerApp(TEMP_SELLER_DB, {
|
|
105
|
-
allowMock: true,
|
|
106
|
-
publicMockPayments: true,
|
|
107
|
-
upstreamUrl: `http://127.0.0.1:${upstreamPort}`,
|
|
108
|
-
upstreamApiKey: "upstream-mock-key",
|
|
109
|
-
operatorSecret: "op-secret"
|
|
110
|
-
});
|
|
111
|
-
sellerCloseFn = closeDb;
|
|
112
|
-
sellerServer = http.createServer(sellerApp);
|
|
113
|
-
await new Promise<void>((resolve) => sellerServer.listen(0, "127.0.0.1", () => resolve()));
|
|
114
|
-
sellerPort = (sellerServer.address() as AddressInfo).port;
|
|
115
|
-
|
|
116
|
-
// 4. Launch TokenBuddy Buyer Proxy daemon
|
|
117
|
-
// Configure process.env to redirect to this mock seller port in the test environment
|
|
118
|
-
process.env.TOKENBUDDY_TEST_SELLER_URL = `http://127.0.0.1:${sellerPort}`;
|
|
119
|
-
|
|
120
|
-
daemon = new TokenbuddyDaemon({
|
|
121
|
-
controlPort: 0, // dynamic port for control plane
|
|
122
|
-
proxyPort: 0, // dynamic port for proxy plane
|
|
123
|
-
dbPath: TEMP_BUYER_DB,
|
|
124
|
-
sellerRegistryUrl: `http://127.0.0.1:${bootstrapPort}/registry/sellers`
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
daemon.start();
|
|
128
|
-
|
|
129
|
-
// Retrieve active control plane & proxy ports
|
|
130
|
-
// Since we pass 0, the active listeners inside daemon have been dynamically assigned
|
|
131
|
-
// We fetch ports by inspecting process listener fields, or directly reading properties if exposed.
|
|
132
|
-
// In packages/tokenbuddy-cli/src/daemon.ts:
|
|
133
|
-
// this.controlServer = controlApp.listen(this.config.controlPort);
|
|
134
|
-
// this.proxyServer = proxyApp.listen(this.config.proxyPort);
|
|
135
|
-
const daemonCtrl = (daemon as any).controlServer.address() as AddressInfo;
|
|
136
|
-
const daemonProxy = (daemon as any).proxyServer.address() as AddressInfo;
|
|
137
|
-
daemonControlPort = daemonCtrl.port;
|
|
138
|
-
daemonProxyPort = daemonProxy.port;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
afterAll(async () => {
|
|
142
|
-
// Cleanup servers
|
|
143
|
-
daemon.stop();
|
|
144
|
-
await new Promise<void>((resolve) => sellerServer.close(() => resolve()));
|
|
145
|
-
sellerCloseFn();
|
|
146
|
-
await new Promise<void>((resolve) => bootstrapServer.close(() => resolve()));
|
|
147
|
-
await new Promise<void>((resolve) => upstreamServer.close(() => resolve()));
|
|
148
|
-
|
|
149
|
-
// Delete temporary databases
|
|
150
|
-
if (fs.existsSync(TEMP_BOOTSTRAP_JSON)) {
|
|
151
|
-
try { fs.unlinkSync(TEMP_BOOTSTRAP_JSON); } catch (e) {}
|
|
152
|
-
}
|
|
153
|
-
if (fs.existsSync(TEMP_SELLER_DB)) {
|
|
154
|
-
try { fs.unlinkSync(TEMP_SELLER_DB); } catch (e) {}
|
|
155
|
-
}
|
|
156
|
-
if (fs.existsSync(TEMP_BUYER_DB)) {
|
|
157
|
-
try { fs.unlinkSync(TEMP_BUYER_DB); } catch (e) {}
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("💡 Verify Full E2E Loop: operator configure -> bootstrap list -> buyer daemon init -> concurrent proxy inference -> precise ledger check", async () => {
|
|
162
|
-
// Step 1: Initialize Admin Client & Verify operator connection
|
|
163
|
-
const admin = new AdminClient(`http://127.0.0.1:${sellerPort}`, "op-secret");
|
|
164
|
-
|
|
165
|
-
const status = await admin.get("/operator/status");
|
|
166
|
-
expect(status.status).toBe("healthy");
|
|
167
|
-
|
|
168
|
-
// Step 2: Operator configures dynamically payment methods
|
|
169
|
-
const clawtipConfig = {
|
|
170
|
-
payTo: "test-merchant-acct",
|
|
171
|
-
sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
|
|
172
|
-
skillSlug: "test-slug",
|
|
173
|
-
skillId: "test-id",
|
|
174
|
-
description: "Token purchase",
|
|
175
|
-
resourceUrl: "http://127.0.0.1/verify"
|
|
176
|
-
};
|
|
177
|
-
const paymentSetRes = await admin.put("/operator/admin/payments/clawtip", clawtipConfig);
|
|
178
|
-
expect(paymentSetRes.status).toBe("success");
|
|
179
|
-
|
|
180
|
-
// Verify payments listed
|
|
181
|
-
const serviceInfo = await admin.get("/operator/admin/service");
|
|
182
|
-
expect(serviceInfo.clawtipConfigured).toBe(true);
|
|
183
|
-
|
|
184
|
-
// Step 3: Register this seller in the wallet bootstrap registry
|
|
185
|
-
const bootstrapAdmin = new AdminClient(`http://127.0.0.1:${bootstrapPort}`, "op-secret");
|
|
186
|
-
const testRegistry = {
|
|
187
|
-
version: 1,
|
|
188
|
-
sellers: [
|
|
189
|
-
{
|
|
190
|
-
id: "seller-e2e-node",
|
|
191
|
-
name: "Seller E2E Node",
|
|
192
|
-
url: `http://127.0.0.1:${sellerPort}`,
|
|
193
|
-
supportedProtocols: ["chat_completions"],
|
|
194
|
-
paymentMethods: ["mock"],
|
|
195
|
-
models: ["gpt-4", "gpt-4o"]
|
|
196
|
-
}
|
|
197
|
-
]
|
|
198
|
-
};
|
|
199
|
-
const registered = await bootstrapAdmin.put("/operator/registry/sellers", testRegistry);
|
|
200
|
-
expect(registered.sellers.length).toBe(1);
|
|
201
|
-
|
|
202
|
-
// Verify registry list publicly available
|
|
203
|
-
const sellersPublicRes = await fetch(`http://127.0.0.1:${bootstrapPort}/registry/sellers`);
|
|
204
|
-
const sellersPublic = await sellersPublicRes.json() as any;
|
|
205
|
-
expect(sellersPublic.sellers[0].id).toBe("seller-e2e-node");
|
|
206
|
-
|
|
207
|
-
// Step 4: Validate daemon connection and check doctor
|
|
208
|
-
const daemonStatusRes = await fetch(`http://127.0.0.1:${daemonControlPort}/status`);
|
|
209
|
-
const daemonStatus = await daemonStatusRes.json() as any;
|
|
210
|
-
expect(daemonStatus.status).toBe("running");
|
|
211
|
-
|
|
212
|
-
// Step 5: Send concurrent inference chat requests to the buyer proxy plane (Port 17821 proxy equivalent)
|
|
213
|
-
const chatBody = {
|
|
214
|
-
model: "gpt-4",
|
|
215
|
-
messages: [{ role: "user", content: "How is the weather?" }],
|
|
216
|
-
requestId: "e2e_concurrency_req_1"
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const sendProxyRequest = (reqId: string) => fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
220
|
-
method: "POST",
|
|
221
|
-
headers: { "Content-Type": "application/json" },
|
|
222
|
-
body: JSON.stringify({ ...chatBody, requestId: reqId })
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Fire 5 concurrent requests representing intense parallel LLM execution by coding terminal (like Claude Code)
|
|
226
|
-
const parallelResponses = await Promise.all([
|
|
227
|
-
sendProxyRequest("req_p_1"),
|
|
228
|
-
sendProxyRequest("req_p_2"),
|
|
229
|
-
sendProxyRequest("req_p_3"),
|
|
230
|
-
sendProxyRequest("req_p_4"),
|
|
231
|
-
sendProxyRequest("req_p_5")
|
|
232
|
-
]);
|
|
233
|
-
|
|
234
|
-
for (const res of parallelResponses) {
|
|
235
|
-
expect(res.ok).toBe(true);
|
|
236
|
-
const data = await res.json() as any;
|
|
237
|
-
expect(data.id).toBe("chatcmpl-e2e-ok");
|
|
238
|
-
expect(data.choices[0].message.content).toBe("Hello back!");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Step 6: Verify precise seller billing ledger
|
|
242
|
-
// A single token should have been purchased of amount 2,000,000 micros.
|
|
243
|
-
// 5 concurrent requests each used: prompt 20 * 1.0 + completion 30 * 3.0 = 110 micros.
|
|
244
|
-
// Total cost: 5 * 110 = 550 micros.
|
|
245
|
-
// Outstanding remaining balance should be: 2,000,000 - 550 = 1,999,450 micros!
|
|
246
|
-
// Let's connect directly to the seller DB using manual sync query or operator billing list
|
|
247
|
-
const { DatabaseSync } = require("node:sqlite");
|
|
248
|
-
const verifyDb = new DatabaseSync(TEMP_SELLER_DB);
|
|
249
|
-
const credRow = verifyDb.prepare("SELECT credit_balance_micros, reserved_micros, spent_micros FROM credentials").get() as any;
|
|
250
|
-
|
|
251
|
-
expect(credRow.credit_balance_micros).toBe(1999450);
|
|
252
|
-
expect(credRow.reserved_micros).toBe(0);
|
|
253
|
-
expect(credRow.spent_micros).toBe(550);
|
|
254
|
-
|
|
255
|
-
// Also assert 5 distinct request settlement entries exist in ledger
|
|
256
|
-
const requestRows = verifyDb.prepare("SELECT state, settled_micros FROM requests").all() as any[];
|
|
257
|
-
expect(requestRows.length).toBe(5);
|
|
258
|
-
for (const r of requestRows) {
|
|
259
|
-
expect(r.state).toBe("settled");
|
|
260
|
-
expect(r.settled_micros).toBe(110);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
verifyDb.close();
|
|
264
|
-
|
|
265
|
-
const buyerAfterConcurrent = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
266
|
-
try {
|
|
267
|
-
const tokenCache = buyerAfterConcurrent.getToken("seller-e2e-node");
|
|
268
|
-
expect(tokenCache).toMatchObject({
|
|
269
|
-
balanceMicros: 1999450,
|
|
270
|
-
reservedMicros: 0,
|
|
271
|
-
spentMicros: 550,
|
|
272
|
-
balanceSource: "seller_settlement_summary"
|
|
273
|
-
});
|
|
274
|
-
const inferenceLedger = buyerAfterConcurrent.listInferenceLedger();
|
|
275
|
-
expect(inferenceLedger.filter((entry) => entry.requestId.startsWith("req_p_"))).toHaveLength(5);
|
|
276
|
-
expect(inferenceLedger).toEqual(expect.arrayContaining([
|
|
277
|
-
expect.objectContaining({
|
|
278
|
-
requestId: "req_p_1",
|
|
279
|
-
billedMicros: 110,
|
|
280
|
-
estimatedMicros: 200,
|
|
281
|
-
settledMicros: 110,
|
|
282
|
-
settledUsdMicros: 110,
|
|
283
|
-
priceVersion: "openrouter_usd.v1",
|
|
284
|
-
balanceSource: "seller_authoritative"
|
|
285
|
-
})
|
|
286
|
-
]));
|
|
287
|
-
} finally {
|
|
288
|
-
buyerAfterConcurrent.close();
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Step 7: Fire subsequent inference request and assert it HITS local daemon token cache immediately (Zero-Purchase)
|
|
292
|
-
// We capture time elapsed to verify cache speed
|
|
293
|
-
const start = Date.now();
|
|
294
|
-
const hitResponse = await sendProxyRequest("req_subsequent_hit");
|
|
295
|
-
const duration = Date.now() - start;
|
|
296
|
-
|
|
297
|
-
expect(hitResponse.ok).toBe(true);
|
|
298
|
-
const hitData = await hitResponse.json() as any;
|
|
299
|
-
expect(hitData.id).toBe("chatcmpl-e2e-ok");
|
|
300
|
-
|
|
301
|
-
// Total purchases should remain intact, meaning no extra purchases occurred!
|
|
302
|
-
const verifyDb2 = new DatabaseSync(TEMP_SELLER_DB);
|
|
303
|
-
const purchaseCount = (verifyDb2.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
|
|
304
|
-
// 1 original purchase coalesce from PromiseLocks
|
|
305
|
-
expect(purchaseCount).toBe(1);
|
|
306
|
-
verifyDb2.close();
|
|
307
|
-
|
|
308
|
-
// Step 8: Force seller-authoritative insufficiency while buyer cache still looks funded.
|
|
309
|
-
// The daemon must refresh /v1/balance, auto-purchase once, and retry the same request idempotently.
|
|
310
|
-
const forcedDb = new DatabaseSync(TEMP_SELLER_DB);
|
|
311
|
-
const oldCredential = forcedDb.prepare("SELECT credential_id FROM credentials ORDER BY created_at ASC LIMIT 1").get() as any;
|
|
312
|
-
forcedDb.prepare("UPDATE credentials SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
|
|
313
|
-
forcedDb.prepare("UPDATE tokens SET credit_balance_micros = 1, reserved_micros = 0 WHERE credential_id = ?").run(oldCredential.credential_id);
|
|
314
|
-
forcedDb.close();
|
|
315
|
-
const buyerBefore402 = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
316
|
-
try {
|
|
317
|
-
const cached = buyerBefore402.getToken("seller-e2e-node");
|
|
318
|
-
expect(cached?.balanceMicros).toBeGreaterThan(200000);
|
|
319
|
-
} finally {
|
|
320
|
-
buyerBefore402.close();
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const retryResponse = await sendProxyRequest("req_402_rebuy_retry");
|
|
324
|
-
expect(retryResponse.ok).toBe(true);
|
|
325
|
-
const retryData = await retryResponse.json() as any;
|
|
326
|
-
expect(retryData.id).toBe("chatcmpl-e2e-ok");
|
|
327
|
-
|
|
328
|
-
const verifyDb3 = new DatabaseSync(TEMP_SELLER_DB);
|
|
329
|
-
const purchaseCountAfterRetry = (verifyDb3.prepare("SELECT COUNT(*) as count FROM purchases").get() as any).count;
|
|
330
|
-
const retryRows = verifyDb3.prepare("SELECT state, settled_micros FROM requests WHERE request_id = ?").all("req_402_rebuy_retry") as any[];
|
|
331
|
-
expect(purchaseCountAfterRetry).toBe(2);
|
|
332
|
-
expect(retryRows).toEqual([
|
|
333
|
-
expect.objectContaining({ state: "settled", settled_micros: 110 })
|
|
334
|
-
]);
|
|
335
|
-
verifyDb3.close();
|
|
336
|
-
|
|
337
|
-
const buyerAfterRetry = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
338
|
-
try {
|
|
339
|
-
expect(buyerAfterRetry.listInferenceLedger()).toEqual(expect.arrayContaining([
|
|
340
|
-
expect.objectContaining({
|
|
341
|
-
requestId: "req_402_rebuy_retry",
|
|
342
|
-
billedMicros: 110,
|
|
343
|
-
estimatedMicros: 200,
|
|
344
|
-
settledMicros: 110,
|
|
345
|
-
balanceSource: "seller_authoritative"
|
|
346
|
-
})
|
|
347
|
-
]));
|
|
348
|
-
expect(buyerAfterRetry.getToken("seller-e2e-node")).toMatchObject({
|
|
349
|
-
balanceMicros: 1999890,
|
|
350
|
-
balanceSource: "seller_settlement_summary"
|
|
351
|
-
});
|
|
352
|
-
} finally {
|
|
353
|
-
buyerAfterRetry.close();
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Step 9: Verify newly implemented admin query billing APIs (purchases & requests)
|
|
357
|
-
const billingPurchases = await admin.get("/operator/admin/purchases");
|
|
358
|
-
expect(billingPurchases.purchases.length).toBe(2);
|
|
359
|
-
expect(billingPurchases.purchases.every((purchase: any) => purchase.state === "funded")).toBe(true);
|
|
360
|
-
|
|
361
|
-
const billingRequests = await admin.get("/operator/admin/requests");
|
|
362
|
-
// 5 concurrent requests + 1 cache hit + 1 retry-after-402 request = 7 settled inference requests.
|
|
363
|
-
expect(billingRequests.requests.length).toBe(7);
|
|
364
|
-
expect(billingRequests.requests.every((r: any) => r.state === "settled")).toBe(true);
|
|
365
|
-
});
|
|
366
|
-
});
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { buildApp as buildBootstrapApp } from "../../wallet-bootstrap/src/server.js";
|
|
2
|
-
import { buildSellerApp } from "../../seller/src/server.js";
|
|
3
|
-
import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
4
|
-
import { BuyerStore } from "../src/buyer-store.js";
|
|
5
|
-
import { PROVIDER_MODE_CONFIG_KEY } from "../src/provider-routing-config.js";
|
|
6
|
-
import * as fs from "fs";
|
|
7
|
-
import * as http from "http";
|
|
8
|
-
import * as os from "os";
|
|
9
|
-
import * as path from "path";
|
|
10
|
-
import type { AddressInfo } from "net";
|
|
11
|
-
|
|
12
|
-
describe("TokenBuddy image generation full chain", () => {
|
|
13
|
-
let tempDir: string;
|
|
14
|
-
let upstreamServer: http.Server;
|
|
15
|
-
let sellerServer: http.Server;
|
|
16
|
-
let bootstrapServer: http.Server;
|
|
17
|
-
let daemon: TokenbuddyDaemon;
|
|
18
|
-
let sellerClose: () => void;
|
|
19
|
-
let buyerDbPath: string;
|
|
20
|
-
let sellerDbPath: string;
|
|
21
|
-
let registryPath: string;
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
daemon?.stop();
|
|
25
|
-
await closeServer(sellerServer);
|
|
26
|
-
sellerClose?.();
|
|
27
|
-
await closeServer(bootstrapServer);
|
|
28
|
-
await closeServer(upstreamServer);
|
|
29
|
-
if (tempDir) {
|
|
30
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("proxies /v1/images/generations through seller and records authoritative billing", async () => {
|
|
35
|
-
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tb-image-e2e-"));
|
|
36
|
-
buyerDbPath = path.join(tempDir, "buyer.db");
|
|
37
|
-
sellerDbPath = path.join(tempDir, "seller.db");
|
|
38
|
-
registryPath = path.join(tempDir, "registry.json");
|
|
39
|
-
|
|
40
|
-
upstreamServer = http.createServer((req, res) => {
|
|
41
|
-
if (req.url !== "/v1/images/generations" || req.method !== "POST") {
|
|
42
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
43
|
-
res.end(JSON.stringify({ error: "unexpected endpoint" }));
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
let body = "";
|
|
47
|
-
req.setEncoding("utf8");
|
|
48
|
-
req.on("data", (chunk) => {
|
|
49
|
-
body += chunk;
|
|
50
|
-
});
|
|
51
|
-
req.on("end", () => {
|
|
52
|
-
const parsed = JSON.parse(body) as {
|
|
53
|
-
model?: string;
|
|
54
|
-
prompt?: string;
|
|
55
|
-
requestId?: string;
|
|
56
|
-
request_id?: string;
|
|
57
|
-
size?: string;
|
|
58
|
-
quality?: string;
|
|
59
|
-
output_format?: string;
|
|
60
|
-
};
|
|
61
|
-
expect(parsed).toMatchObject({
|
|
62
|
-
model: "gpt-image-2",
|
|
63
|
-
prompt: "draw a tiny smoke test",
|
|
64
|
-
size: "1024x1024",
|
|
65
|
-
quality: "high",
|
|
66
|
-
output_format: "png"
|
|
67
|
-
});
|
|
68
|
-
expect(parsed.requestId).toBeUndefined();
|
|
69
|
-
expect(parsed.request_id).toBeUndefined();
|
|
70
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
71
|
-
res.end(JSON.stringify({
|
|
72
|
-
created: 123,
|
|
73
|
-
data: [{ b64_json: "aW1hZ2U=" }],
|
|
74
|
-
usage: {
|
|
75
|
-
input_tokens: 25,
|
|
76
|
-
output_tokens: 75,
|
|
77
|
-
total_tokens: 100
|
|
78
|
-
}
|
|
79
|
-
}));
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
await listen(upstreamServer);
|
|
83
|
-
const upstreamPort = portOf(upstreamServer);
|
|
84
|
-
|
|
85
|
-
const sellerHandle = buildSellerApp(sellerDbPath, {
|
|
86
|
-
allowMock: true,
|
|
87
|
-
publicMockPayments: true,
|
|
88
|
-
upstreamUrl: `http://127.0.0.1:${upstreamPort}`,
|
|
89
|
-
upstreamApiKey: "upstream-test-key",
|
|
90
|
-
upstreamCapabilities: {
|
|
91
|
-
chatCompletions: "unsupported",
|
|
92
|
-
responses: "unsupported",
|
|
93
|
-
messages: "unsupported",
|
|
94
|
-
imagesGenerations: "supported"
|
|
95
|
-
},
|
|
96
|
-
models: [
|
|
97
|
-
{
|
|
98
|
-
id: "gpt-image-2",
|
|
99
|
-
inputPriceMicrosPer1m: 1000000,
|
|
100
|
-
outputPriceMicrosPer1m: 3000000,
|
|
101
|
-
streaming: false
|
|
102
|
-
}
|
|
103
|
-
]
|
|
104
|
-
});
|
|
105
|
-
sellerClose = sellerHandle.close;
|
|
106
|
-
sellerServer = http.createServer(sellerHandle.app);
|
|
107
|
-
await listen(sellerServer);
|
|
108
|
-
const sellerPort = portOf(sellerServer);
|
|
109
|
-
|
|
110
|
-
fs.writeFileSync(registryPath, JSON.stringify({
|
|
111
|
-
version: 1,
|
|
112
|
-
defaultSeller: "seller-image-e2e",
|
|
113
|
-
sellers: [
|
|
114
|
-
{
|
|
115
|
-
id: "seller-image-e2e",
|
|
116
|
-
name: "Seller Image E2E",
|
|
117
|
-
url: `http://127.0.0.1:${sellerPort}`,
|
|
118
|
-
supportedProtocols: ["images_generations"],
|
|
119
|
-
paymentMethods: ["mock"],
|
|
120
|
-
models: ["gpt-image-2"]
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
}), "utf8");
|
|
124
|
-
|
|
125
|
-
const bootstrapApp = buildBootstrapApp({
|
|
126
|
-
payTo: "test-activation-pay-to",
|
|
127
|
-
sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
|
|
128
|
-
skillSlug: "test-activation-slug",
|
|
129
|
-
skillId: "test-activation-id",
|
|
130
|
-
description: "Activate registration",
|
|
131
|
-
resourceUrl: "http://127.0.0.1/verify",
|
|
132
|
-
activationFeeFen: 1,
|
|
133
|
-
microsPerFen: 1000000,
|
|
134
|
-
sellerRegistryPath: registryPath,
|
|
135
|
-
operatorSecret: "op-secret",
|
|
136
|
-
allowLocalSellerUrls: true
|
|
137
|
-
});
|
|
138
|
-
bootstrapServer = http.createServer(bootstrapApp);
|
|
139
|
-
await listen(bootstrapServer);
|
|
140
|
-
const bootstrapPort = portOf(bootstrapServer);
|
|
141
|
-
|
|
142
|
-
const buyerStore = new BuyerStore({ dbPath: buyerDbPath });
|
|
143
|
-
buyerStore.savePayment({
|
|
144
|
-
method: "mock",
|
|
145
|
-
enabled: true,
|
|
146
|
-
isDefault: true,
|
|
147
|
-
config: { channel: "image-e2e-test" }
|
|
148
|
-
});
|
|
149
|
-
buyerStore.saveDaemonRuntimeConfig(PROVIDER_MODE_CONFIG_KEY, {
|
|
150
|
-
mode: "auto",
|
|
151
|
-
updatedAt: new Date().toISOString()
|
|
152
|
-
});
|
|
153
|
-
buyerStore.close();
|
|
154
|
-
|
|
155
|
-
daemon = new TokenbuddyDaemon({
|
|
156
|
-
controlPort: 0,
|
|
157
|
-
proxyPort: 0,
|
|
158
|
-
dbPath: buyerDbPath,
|
|
159
|
-
sellerRegistryUrl: `http://127.0.0.1:${bootstrapPort}/registry/sellers`
|
|
160
|
-
});
|
|
161
|
-
daemon.start();
|
|
162
|
-
const daemonProxyPort = portOf((daemon as any).proxyServer);
|
|
163
|
-
|
|
164
|
-
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/images/generations`, {
|
|
165
|
-
method: "POST",
|
|
166
|
-
headers: {
|
|
167
|
-
"Content-Type": "application/json",
|
|
168
|
-
"X-Request-Id": "image_full_chain"
|
|
169
|
-
},
|
|
170
|
-
body: JSON.stringify({
|
|
171
|
-
model: "gpt-image-2",
|
|
172
|
-
prompt: "draw a tiny smoke test",
|
|
173
|
-
size: "1024x1024",
|
|
174
|
-
quality: "high",
|
|
175
|
-
output_format: "png"
|
|
176
|
-
})
|
|
177
|
-
});
|
|
178
|
-
expect(response.ok).toBe(true);
|
|
179
|
-
const body = await response.json() as any;
|
|
180
|
-
expect(body.data[0].b64_json).toBe("aW1hZ2U=");
|
|
181
|
-
|
|
182
|
-
const after = new BuyerStore({ dbPath: buyerDbPath });
|
|
183
|
-
try {
|
|
184
|
-
expect(after.listInferenceLedger()).toEqual(expect.arrayContaining([
|
|
185
|
-
expect.objectContaining({
|
|
186
|
-
requestId: "image_full_chain",
|
|
187
|
-
sellerKey: "seller-image-e2e",
|
|
188
|
-
modelId: "gpt-image-2",
|
|
189
|
-
endpoint: "/v1/images/generations",
|
|
190
|
-
billedMicros: 250,
|
|
191
|
-
estimatedMicros: 400,
|
|
192
|
-
settledMicros: 250,
|
|
193
|
-
settledUsdMicros: 250,
|
|
194
|
-
billingUnit: "images",
|
|
195
|
-
imageCount: 1,
|
|
196
|
-
imageSize: "1024x1024",
|
|
197
|
-
imageQuality: "high",
|
|
198
|
-
imageOutputFormat: "png",
|
|
199
|
-
imageOutputTokens: 75,
|
|
200
|
-
imageOutputCostMicros: 225,
|
|
201
|
-
imageCostMicrosPerImage: 250,
|
|
202
|
-
balanceSource: "seller_authoritative"
|
|
203
|
-
})
|
|
204
|
-
]));
|
|
205
|
-
expect(after.getToken("seller-image-e2e")).toMatchObject({
|
|
206
|
-
balanceMicros: 1999750,
|
|
207
|
-
reservedMicros: 0,
|
|
208
|
-
spentMicros: 250,
|
|
209
|
-
balanceSource: "seller_settlement_summary"
|
|
210
|
-
});
|
|
211
|
-
} finally {
|
|
212
|
-
after.close();
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
function listen(server: http.Server): Promise<void> {
|
|
218
|
-
return new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function closeServer(server: http.Server | undefined): Promise<void> {
|
|
222
|
-
if (!server?.listening) {
|
|
223
|
-
return Promise.resolve();
|
|
224
|
-
}
|
|
225
|
-
return new Promise((resolve) => server.close(() => resolve()));
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
function portOf(server: http.Server): number {
|
|
229
|
-
return (server.address() as AddressInfo).port;
|
|
230
|
-
}
|