@tokenbuddy/tokenbuddy 1.0.11 → 1.0.13
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 +61 -0
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +12 -0
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +47 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +287 -63
- package/dist/src/cli.js.map +1 -1
- package/dist/src/credit-tracker.d.ts +26 -0
- package/dist/src/credit-tracker.d.ts.map +1 -1
- package/dist/src/credit-tracker.js +8 -0
- package/dist/src/credit-tracker.js.map +1 -1
- package/dist/src/daemon.d.ts +29 -3
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +292 -65
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +25 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -1
- package/dist/src/doctor-clawtip-wallet.js +13 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -1
- package/dist/src/doctor-diagnostics.d.ts +63 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +39 -1
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +103 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -1
- package/dist/src/init-clawtip-activation.js +60 -0
- package/dist/src/init-clawtip-activation.js.map +1 -1
- package/dist/src/init-payment-options.d.ts +124 -0
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +68 -0
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/model-index.d.ts +9 -0
- package/dist/src/model-index.d.ts.map +1 -1
- package/dist/src/model-index.js.map +1 -1
- package/dist/src/prewarm-cache.d.ts +89 -0
- package/dist/src/prewarm-cache.d.ts.map +1 -1
- package/dist/src/prewarm-cache.js +14 -1
- package/dist/src/prewarm-cache.js.map +1 -1
- package/dist/src/prewarm-scheduler.d.ts +62 -3
- package/dist/src/prewarm-scheduler.d.ts.map +1 -1
- package/dist/src/prewarm-scheduler.js +39 -8
- package/dist/src/prewarm-scheduler.js.map +1 -1
- package/dist/src/provider-install.d.ts +89 -3
- package/dist/src/provider-install.d.ts.map +1 -1
- package/dist/src/provider-install.js +77 -17
- package/dist/src/provider-install.js.map +1 -1
- package/dist/src/route-failover.d.ts +48 -0
- package/dist/src/route-failover.d.ts.map +1 -1
- package/dist/src/route-failover.js.map +1 -1
- package/dist/src/seller-catalog.d.ts +158 -10
- package/dist/src/seller-catalog.d.ts.map +1 -1
- package/dist/src/seller-catalog.js +79 -5
- package/dist/src/seller-catalog.js.map +1 -1
- package/dist/src/seller-metadata-cache.d.ts +29 -0
- package/dist/src/seller-metadata-cache.d.ts.map +1 -0
- package/dist/src/seller-metadata-cache.js +71 -0
- package/dist/src/seller-metadata-cache.js.map +1 -0
- package/dist/src/seller-pool.d.ts +71 -0
- package/dist/src/seller-pool.d.ts.map +1 -1
- package/dist/src/seller-pool.js +6 -1
- package/dist/src/seller-pool.js.map +1 -1
- package/dist/src/seller-route-planner.d.ts +118 -0
- package/dist/src/seller-route-planner.d.ts.map +1 -0
- package/dist/src/seller-route-planner.js +160 -0
- package/dist/src/seller-route-planner.js.map +1 -0
- package/dist/src/seller-routing-config.d.ts +69 -0
- package/dist/src/seller-routing-config.d.ts.map +1 -0
- package/dist/src/seller-routing-config.js +164 -0
- package/dist/src/seller-routing-config.js.map +1 -0
- package/dist/src/seller-routing-strategy.d.ts +118 -0
- package/dist/src/seller-routing-strategy.d.ts.map +1 -0
- package/dist/src/seller-routing-strategy.js +183 -0
- package/dist/src/seller-routing-strategy.js.map +1 -0
- package/dist/src/stream-failover.d.ts +23 -0
- package/dist/src/stream-failover.d.ts.map +1 -1
- package/dist/src/stream-failover.js +4 -0
- package/dist/src/stream-failover.js.map +1 -1
- package/dist/src/tb-proxyd.js +7 -21
- package/dist/src/tb-proxyd.js.map +1 -1
- package/dist/src/terminal-detect.d.ts +51 -0
- package/dist/src/terminal-detect.d.ts.map +1 -1
- package/dist/src/terminal-detect.js +42 -0
- package/dist/src/terminal-detect.js.map +1 -1
- package/dist/src/terminal-image.d.ts +41 -0
- package/dist/src/terminal-image.d.ts.map +1 -1
- package/dist/src/terminal-image.js +15 -0
- package/dist/src/terminal-image.js.map +1 -1
- package/package.json +1 -1
- package/src/buyer-store.ts +61 -0
- package/src/cli.ts +330 -68
- package/src/credit-tracker.ts +26 -0
- package/src/daemon.ts +363 -72
- package/src/doctor-clawtip-wallet.ts +25 -0
- package/src/doctor-diagnostics.ts +63 -1
- package/src/index.ts +4 -0
- package/src/init-clawtip-activation.ts +103 -0
- package/src/init-payment-options.ts +124 -0
- package/src/model-index.ts +9 -0
- package/src/prewarm-cache.ts +99 -1
- package/src/prewarm-scheduler.ts +97 -12
- package/src/provider-install.ts +125 -25
- package/src/route-failover.ts +48 -0
- package/src/seller-catalog.ts +158 -12
- package/src/seller-metadata-cache.ts +91 -0
- package/src/seller-pool.ts +77 -1
- package/src/seller-route-planner.ts +323 -0
- package/src/seller-routing-config.ts +198 -0
- package/src/seller-routing-strategy.ts +316 -0
- package/src/stream-failover.ts +23 -0
- package/src/tb-proxyd.ts +7 -23
- package/src/terminal-detect.ts +51 -0
- package/src/terminal-image.ts +41 -0
- package/tests/cli-routing.test.ts +287 -0
- package/tests/daemon-classify.test.ts +431 -0
- package/tests/daemon-roles.test.ts +92 -0
- package/tests/seller-catalog-utilities.test.ts +70 -0
- package/tests/seller-metadata-cache.test.ts +89 -0
- package/tests/seller-route-planner.test.ts +150 -0
- package/tests/seller-routing-config.test.ts +111 -0
- package/tests/seller-routing-strategy.test.ts +166 -0
- package/tests/tokenbuddy.test.ts +447 -33
- /package/{src → tests}/credit-tracker.test.ts +0 -0
- /package/{src → tests}/model-index.test.ts +0 -0
- /package/{src → tests}/prewarm-cache.test.ts +0 -0
- /package/{src → tests}/prewarm-scheduler.test.ts +0 -0
- /package/{src → tests}/route-failover.test.ts +0 -0
- /package/{src → tests}/seller-catalog-413.test.ts +0 -0
- /package/{src → tests}/seller-pool.test.ts +0 -0
- /package/{src → tests}/stream-failover.test.ts +0 -0
- /package/{src → tests}/thousand-seller.test.ts +0 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as os from "os";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { BuyerStore } from "../src/buyer-store.js";
|
|
5
|
+
import { buildCli } from "../src/cli.js";
|
|
6
|
+
|
|
7
|
+
describe("tb routing set", () => {
|
|
8
|
+
let storeRoot: string;
|
|
9
|
+
let previousStoreRoot: string | undefined;
|
|
10
|
+
let logSpy: jest.SpyInstance;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
storeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "tokenbuddy-cli-routing-"));
|
|
14
|
+
previousStoreRoot = process.env.TOKENBUDDY_BUYER_STORE;
|
|
15
|
+
process.env.TOKENBUDDY_BUYER_STORE = storeRoot;
|
|
16
|
+
logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
logSpy.mockRestore();
|
|
21
|
+
if (previousStoreRoot === undefined) {
|
|
22
|
+
delete process.env.TOKENBUDDY_BUYER_STORE;
|
|
23
|
+
} else {
|
|
24
|
+
process.env.TOKENBUDDY_BUYER_STORE = previousStoreRoot;
|
|
25
|
+
}
|
|
26
|
+
fs.rmSync(storeRoot, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("fixed mode requires --seller", async () => {
|
|
30
|
+
await expect(buildCli().parseAsync(["node", "tb", "routing", "set", "fixed"]))
|
|
31
|
+
.rejects.toThrow("fixed routing requires --seller");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("fixedSet mode requires --seller-set", async () => {
|
|
35
|
+
await expect(buildCli().parseAsync(["node", "tb", "routing", "set", "fixedSet", "--seller-set", ""]))
|
|
36
|
+
.rejects.toThrow("fixedSet routing requires --seller-set");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("rejects unsupported routing mode", async () => {
|
|
40
|
+
await expect(buildCli().parseAsync(["node", "tb", "routing", "set", "bogus"]))
|
|
41
|
+
.rejects.toThrow("routing mode must be fixed, fixedSet, or fullAuto");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("fullAuto accepts empty seller options and writes balanced config", async () => {
|
|
45
|
+
await buildCli().parseAsync(["node", "tb", "routing", "set", "fullAuto"]);
|
|
46
|
+
|
|
47
|
+
const store = new BuyerStore({ root: storeRoot });
|
|
48
|
+
try {
|
|
49
|
+
expect(store.getDaemonRuntimeConfig("routing")).toMatchObject({
|
|
50
|
+
config: {
|
|
51
|
+
mode: "fullAuto",
|
|
52
|
+
scorer: "balanced",
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
} finally {
|
|
56
|
+
store.close();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("routing show prints fixed and fixedSet details in text mode", async () => {
|
|
61
|
+
const output: string[] = [];
|
|
62
|
+
logSpy.mockImplementation((message?: unknown) => {
|
|
63
|
+
output.push(String(message));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await buildCli().parseAsync(["node", "tb", "routing", "set", "fixed", "--seller", "seller-a"]);
|
|
67
|
+
output.length = 0;
|
|
68
|
+
await buildCli().parseAsync(["node", "tb", "routing", "show"]);
|
|
69
|
+
expect(output.join("\n")).toContain("Seller: seller-a");
|
|
70
|
+
expect(output.join("\n")).toContain("Updated:");
|
|
71
|
+
|
|
72
|
+
await buildCli().parseAsync(["node", "tb", "routing", "set", "fixedSet", "--seller-set", "seller-a,seller-b"]);
|
|
73
|
+
output.length = 0;
|
|
74
|
+
await buildCli().parseAsync(["node", "tb", "routing", "show"]);
|
|
75
|
+
expect(output.join("\n")).toContain("Seller Set: seller-a,seller-b");
|
|
76
|
+
expect(output.join("\n")).toContain("Updated:");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("doctor rejects invalid configured control port", async () => {
|
|
80
|
+
const previousControlPort = process.env.TB_PROXYD_CONTROL_PORT;
|
|
81
|
+
try {
|
|
82
|
+
process.env.TB_PROXYD_CONTROL_PORT = "not-a-port";
|
|
83
|
+
await expect(buildCli().parseAsync(["node", "tb", "doctor", "--json"]))
|
|
84
|
+
.rejects.toThrow("TB_PROXYD_CONTROL_PORT must be an integer port");
|
|
85
|
+
} finally {
|
|
86
|
+
restoreEnv("TB_PROXYD_CONTROL_PORT", previousControlPort);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("doctor --json reports daemon status and v1.2 snapshot", async () => {
|
|
91
|
+
const output: string[] = [];
|
|
92
|
+
logSpy.mockImplementation((message?: unknown) => {
|
|
93
|
+
output.push(String(message));
|
|
94
|
+
});
|
|
95
|
+
const fetchMock = jest.spyOn(globalThis, "fetch").mockImplementation(async (url: string | URL | Request) => {
|
|
96
|
+
const href = String(url);
|
|
97
|
+
if (href.endsWith("/status")) {
|
|
98
|
+
return response({
|
|
99
|
+
pid: 123,
|
|
100
|
+
sellerRoutingMode: "fixed",
|
|
101
|
+
selectedSellerId: "seller-a",
|
|
102
|
+
sellerRegistryUrl: "https://registry.example.test/sellers",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (href.endsWith("/v1.2/prewarm")) {
|
|
106
|
+
return response({ pool: { entries: [] } });
|
|
107
|
+
}
|
|
108
|
+
if (href.endsWith("/health")) {
|
|
109
|
+
return response({ ok: true });
|
|
110
|
+
}
|
|
111
|
+
return response({}, false, 404);
|
|
112
|
+
}) as jest.MockedFunction<typeof fetch>;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await buildCli().parseAsync(["node", "tb", "doctor", "--json"]);
|
|
116
|
+
const parsed = JSON.parse(output[0]) as any;
|
|
117
|
+
expect(parsed.daemon.running).toBe(true);
|
|
118
|
+
expect(parsed.daemon.status.selectedSellerId).toBe("seller-a");
|
|
119
|
+
expect(parsed.v12).toEqual({ pool: { entries: [] } });
|
|
120
|
+
} finally {
|
|
121
|
+
fetchMock.mockRestore();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("doctor --json reports fetch failures as not running", async () => {
|
|
126
|
+
const output: string[] = [];
|
|
127
|
+
logSpy.mockImplementation((message?: unknown) => {
|
|
128
|
+
output.push(String(message));
|
|
129
|
+
});
|
|
130
|
+
const fetchMock = jest.spyOn(globalThis, "fetch").mockRejectedValue(new Error("offline"));
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await buildCli().parseAsync(["node", "tb", "doctor", "--json"]);
|
|
134
|
+
const parsed = JSON.parse(output[0]) as any;
|
|
135
|
+
expect(parsed.daemon.running).toBe(false);
|
|
136
|
+
expect(parsed.daemon.error).toContain("offline");
|
|
137
|
+
expect(parsed.v12).toBeNull();
|
|
138
|
+
} finally {
|
|
139
|
+
fetchMock.mockRestore();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("doctor text mode renders running daemon details and v1.2 section", async () => {
|
|
144
|
+
const output: string[] = [];
|
|
145
|
+
logSpy.mockImplementation((message?: unknown) => {
|
|
146
|
+
output.push(String(message));
|
|
147
|
+
});
|
|
148
|
+
const fetchMock = jest.spyOn(globalThis, "fetch").mockImplementation(async (url: string | URL | Request) => {
|
|
149
|
+
const href = String(url);
|
|
150
|
+
if (href.endsWith("/status")) {
|
|
151
|
+
return response({
|
|
152
|
+
pid: 123,
|
|
153
|
+
controlPort: 17820,
|
|
154
|
+
proxyPort: 17821,
|
|
155
|
+
sellerRoutingMode: "fixed",
|
|
156
|
+
selectedSellerId: "seller-a",
|
|
157
|
+
sellerRegistryUrl: "https://registry.example.test/sellers",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (href.endsWith("/v1.2/prewarm")) {
|
|
161
|
+
return response({
|
|
162
|
+
focusSet: ["gpt-5.4"],
|
|
163
|
+
prewarm: {
|
|
164
|
+
size: 1,
|
|
165
|
+
entries: [{
|
|
166
|
+
modelId: "gpt-5.4",
|
|
167
|
+
state: "ready",
|
|
168
|
+
candidateCount: 2,
|
|
169
|
+
warmedAt: Date.now(),
|
|
170
|
+
ttlMs: 60000,
|
|
171
|
+
consecutiveWarmingFailures: 0,
|
|
172
|
+
}],
|
|
173
|
+
},
|
|
174
|
+
pool: {
|
|
175
|
+
size: 1,
|
|
176
|
+
entries: [{ sellerId: "seller-a", circuit: "open", healthScore: 10 }],
|
|
177
|
+
},
|
|
178
|
+
credit: {
|
|
179
|
+
totalWastedMicros: 1,
|
|
180
|
+
wastedSinceLastDoctorRun: 2,
|
|
181
|
+
purchasesInLastMinute: 0,
|
|
182
|
+
purchaseBudgetPerMinute: 5,
|
|
183
|
+
perSeller: [{ sellerId: "seller-a", currentBalanceMicros: 10, leftoverCreditMicros: 1 }],
|
|
184
|
+
},
|
|
185
|
+
scheduler: {
|
|
186
|
+
inFlight: 0,
|
|
187
|
+
queueDepth: 0,
|
|
188
|
+
totalSucceeded: 1,
|
|
189
|
+
totalFailed: 0,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return response({}, false, 404);
|
|
194
|
+
}) as jest.MockedFunction<typeof fetch>;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
await buildCli().parseAsync(["node", "tb", "doctor"]);
|
|
198
|
+
const text = output.join("\n");
|
|
199
|
+
expect(text).toContain("Daemon tb-proxyd is running");
|
|
200
|
+
expect(text).toContain("Routing Mode: fixed");
|
|
201
|
+
expect(text).toContain("Selected Seller: seller-a");
|
|
202
|
+
expect(text).toContain("=== v1.2 Fallback Pipeline ===");
|
|
203
|
+
expect(text).toContain("seller-a [open]");
|
|
204
|
+
} finally {
|
|
205
|
+
fetchMock.mockRestore();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("doctor text mode reports offline daemon and repair hint", async () => {
|
|
210
|
+
const output: string[] = [];
|
|
211
|
+
logSpy.mockImplementation((message?: unknown) => {
|
|
212
|
+
output.push(String(message));
|
|
213
|
+
});
|
|
214
|
+
const fetchMock = jest.spyOn(globalThis, "fetch").mockRejectedValue(new Error("offline"));
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await buildCli().parseAsync(["node", "tb", "doctor"]);
|
|
218
|
+
const text = output.join("\n");
|
|
219
|
+
expect(text).toContain("Daemon tb-proxyd is NOT running");
|
|
220
|
+
expect(text).toContain("tb doctor --fix");
|
|
221
|
+
} finally {
|
|
222
|
+
fetchMock.mockRestore();
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("payment commands reject unsupported methods after daemon gate", async () => {
|
|
227
|
+
const errors: string[] = [];
|
|
228
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation((message?: unknown) => {
|
|
229
|
+
errors.push(String(message));
|
|
230
|
+
});
|
|
231
|
+
const fetchMock = jest.spyOn(globalThis, "fetch").mockResolvedValue(response({ ok: true }));
|
|
232
|
+
const previousExitCode = process.exitCode;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
process.exitCode = undefined;
|
|
236
|
+
await buildCli().parseAsync(["node", "tb", "payment", "add", "card"]);
|
|
237
|
+
await buildCli().parseAsync(["node", "tb", "payment", "remove", "card"]);
|
|
238
|
+
|
|
239
|
+
expect(process.exitCode).toBe(1);
|
|
240
|
+
expect(errors.join("\n")).toContain("Unsupported payment method: card");
|
|
241
|
+
} finally {
|
|
242
|
+
process.exitCode = previousExitCode;
|
|
243
|
+
fetchMock.mockRestore();
|
|
244
|
+
errorSpy.mockRestore();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("payment add clawtip requires a bootstrap url", async () => {
|
|
249
|
+
const errors: string[] = [];
|
|
250
|
+
const errorSpy = jest.spyOn(console, "error").mockImplementation((message?: unknown) => {
|
|
251
|
+
errors.push(String(message));
|
|
252
|
+
});
|
|
253
|
+
const fetchMock = jest.spyOn(globalThis, "fetch").mockResolvedValue(response({ ok: true }));
|
|
254
|
+
const previousExitCode = process.exitCode;
|
|
255
|
+
const previousBootstrapUrl = process.env.TOKENBUDDY_BOOTSTRAP_URL;
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
process.exitCode = undefined;
|
|
259
|
+
delete process.env.TOKENBUDDY_BOOTSTRAP_URL;
|
|
260
|
+
await buildCli().parseAsync(["node", "tb", "payment", "add", "clawtip"]);
|
|
261
|
+
|
|
262
|
+
expect(process.exitCode).toBe(1);
|
|
263
|
+
expect(errors.join("\n")).toContain("ClawTip bootstrap URL is required");
|
|
264
|
+
} finally {
|
|
265
|
+
process.exitCode = previousExitCode;
|
|
266
|
+
restoreEnv("TOKENBUDDY_BOOTSTRAP_URL", previousBootstrapUrl);
|
|
267
|
+
fetchMock.mockRestore();
|
|
268
|
+
errorSpy.mockRestore();
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
function restoreEnv(name: string, value: string | undefined): void {
|
|
274
|
+
if (value === undefined) {
|
|
275
|
+
delete process.env[name];
|
|
276
|
+
} else {
|
|
277
|
+
process.env[name] = value;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function response(body: unknown, ok = true, status = 200): Response {
|
|
282
|
+
return {
|
|
283
|
+
ok,
|
|
284
|
+
status,
|
|
285
|
+
json: async () => body,
|
|
286
|
+
} as Response;
|
|
287
|
+
}
|