@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
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
import * as fs from "fs";
|
|
2
|
-
import * as os from "os";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
5
|
-
|
|
6
|
-
describe("TokenbuddyDaemon classifyFailureStatus", () => {
|
|
7
|
-
let daemon: TokenbuddyDaemon;
|
|
8
|
-
|
|
9
|
-
beforeEach(() => {
|
|
10
|
-
daemon = new TokenbuddyDaemon({
|
|
11
|
-
controlPort: 0,
|
|
12
|
-
proxyPort: 0,
|
|
13
|
-
dbPath: ":memory:",
|
|
14
|
-
sellerRegistryUrl: "http://127.0.0.1:1/registry",
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
daemon.stop();
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("classifies auth failures", () => {
|
|
23
|
-
expect((daemon as any).classifyFailureStatus(401)).toBe("auth_invalid");
|
|
24
|
-
expect((daemon as any).classifyFailureStatus(403)).toBe("auth_invalid");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("classifies insufficient funds", () => {
|
|
28
|
-
expect((daemon as any).classifyFailureStatus(402)).toBe("insufficient_funds");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("classifies hard request 4xx failures", () => {
|
|
32
|
-
expect((daemon as any).classifyFailureStatus(400)).toBe("hard_4xx");
|
|
33
|
-
expect((daemon as any).classifyFailureStatus(404)).toBe("hard_4xx");
|
|
34
|
-
expect((daemon as any).classifyFailureStatus(422)).toBe("hard_4xx");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("classifies retryable upstream failures", () => {
|
|
38
|
-
expect((daemon as any).classifyFailureStatus(429)).toBe("soft_5xx");
|
|
39
|
-
expect((daemon as any).classifyFailureStatus(500)).toBe("soft_5xx");
|
|
40
|
-
expect((daemon as any).classifyFailureStatus(502)).toBe("soft_5xx");
|
|
41
|
-
expect((daemon as any).classifyFailureStatus(503)).toBe("soft_5xx");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("classifies seller capacity exhaustion separately from generic 429", () => {
|
|
45
|
-
expect((daemon as any).classifyFailureStatus(429, JSON.stringify({
|
|
46
|
-
error: { code: "busy_capacity" }
|
|
47
|
-
}))).toBe("busy_capacity");
|
|
48
|
-
expect((daemon as any).classifyFailureStatus(429, JSON.stringify({
|
|
49
|
-
error: { code: "rate_limited" }
|
|
50
|
-
}))).toBe("soft_5xx");
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("recognizes supported proxy endpoint protocols", () => {
|
|
54
|
-
expect((daemon as any).endpointProtocol("/v1/chat/completions")).toBe("chat_completions");
|
|
55
|
-
expect((daemon as any).endpointProtocol("/v1/responses")).toBe("responses");
|
|
56
|
-
expect((daemon as any).endpointProtocol("/v1/messages")).toBe("messages");
|
|
57
|
-
expect((daemon as any).endpointProtocol("/messages")).toBe("messages");
|
|
58
|
-
expect((daemon as any).endpointProtocol("/v1/images/generations")).toBe("images_generations");
|
|
59
|
-
expect((daemon as any).endpointProtocol("/v1/unknown")).toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("extracts model ids from supported body shapes", () => {
|
|
63
|
-
expect((daemon as any).extractModelId("/v1/chat/completions", { model: " gpt-5.4 " })).toBe("gpt-5.4");
|
|
64
|
-
expect((daemon as any).extractModelId("/v1/chat/completions", { model: " " })).toBeUndefined();
|
|
65
|
-
expect((daemon as any).extractModelId("/v1/responses", { model_id: " resp-model " })).toBe("resp-model");
|
|
66
|
-
expect((daemon as any).extractModelId("/v1/responses", { model_id: 42 })).toBeUndefined();
|
|
67
|
-
expect((daemon as any).extractModelId("/v1/images/generations", { model: " gpt-image-2 " })).toBe("gpt-image-2");
|
|
68
|
-
expect((daemon as any).extractModelId("/v1/responses", null)).toBeUndefined();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("resolves and applies route model ids", () => {
|
|
72
|
-
expect((daemon as any).resolveRouteModelId("/v1/chat/completions", { model: "gpt-5.4" })).toEqual({
|
|
73
|
-
requestedModelId: "gpt-5.4",
|
|
74
|
-
resolvedModelId: "gpt-5.4",
|
|
75
|
-
});
|
|
76
|
-
expect((daemon as any).resolveRouteModelId("/v1/chat/completions", {})).toEqual({});
|
|
77
|
-
expect((daemon as any).applyResolvedModelToBody("/v1/chat/completions", { model: "old" }, "new")).toEqual({
|
|
78
|
-
model: "new",
|
|
79
|
-
});
|
|
80
|
-
expect((daemon as any).applyResolvedModelToBody("/v1/responses", { model_id: "old" }, "new")).toEqual({
|
|
81
|
-
model_id: "new",
|
|
82
|
-
});
|
|
83
|
-
expect((daemon as any).applyResolvedModelToBody("/v1/responses", { input: "hi" }, "new")).toEqual({
|
|
84
|
-
input: "hi",
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("chooses the default enabled payment method", () => {
|
|
89
|
-
expect((daemon as any).defaultPaymentMethod()).toBeUndefined();
|
|
90
|
-
(daemon as any).tokenStore.savePayment({ method: "mock", enabled: true, isDefault: false });
|
|
91
|
-
expect((daemon as any).defaultPaymentMethod()).toBe("mock");
|
|
92
|
-
(daemon as any).tokenStore.savePayment({ method: "clawtip", enabled: true, isDefault: true });
|
|
93
|
-
expect((daemon as any).defaultPaymentMethod()).toBe("clawtip");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("reads usage from chat and responses style bodies", () => {
|
|
97
|
-
expect((daemon as any).readUsage("")).toEqual({
|
|
98
|
-
promptTokens: 0,
|
|
99
|
-
completionTokens: 0,
|
|
100
|
-
cacheReadTokens: 0,
|
|
101
|
-
billedMicros: 0,
|
|
102
|
-
});
|
|
103
|
-
expect((daemon as any).readUsage("not-json")).toEqual({
|
|
104
|
-
promptTokens: 0,
|
|
105
|
-
completionTokens: 0,
|
|
106
|
-
cacheReadTokens: 0,
|
|
107
|
-
billedMicros: 0,
|
|
108
|
-
});
|
|
109
|
-
expect((daemon as any).readUsage(JSON.stringify({ usage: { prompt_tokens: 2, completion_tokens: 3 } }))).toEqual({
|
|
110
|
-
promptTokens: 2,
|
|
111
|
-
completionTokens: 3,
|
|
112
|
-
cacheReadTokens: 0,
|
|
113
|
-
billedMicros: 20,
|
|
114
|
-
});
|
|
115
|
-
expect((daemon as any).readUsage(JSON.stringify({ usage: { input_tokens: 5, output_tokens: 7, prompt_tokens_details: { cached_tokens: 4 } } }))).toEqual({
|
|
116
|
-
promptTokens: 5,
|
|
117
|
-
completionTokens: 7,
|
|
118
|
-
cacheReadTokens: 4,
|
|
119
|
-
billedMicros: 48,
|
|
120
|
-
});
|
|
121
|
-
expect((daemon as any).readUsage(JSON.stringify({ usage: { input_tokens: 25, output_tokens: 75, total_tokens: 100 } }))).toEqual({
|
|
122
|
-
promptTokens: 25,
|
|
123
|
-
completionTokens: 75,
|
|
124
|
-
cacheReadTokens: 0,
|
|
125
|
-
billedMicros: 400,
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("parses seller settlement headers", () => {
|
|
130
|
-
expect((daemon as any).parseSellerSettlementSummary(new Headers())).toBeUndefined();
|
|
131
|
-
expect((daemon as any).parseSellerSettlementSummary(new Headers({
|
|
132
|
-
"x-tokenbuddy-settlement": JSON.stringify({
|
|
133
|
-
request_id: "req-1",
|
|
134
|
-
settled_micros: "20",
|
|
135
|
-
remaining_credit_micros: "980",
|
|
136
|
-
reserved_balance_micros: 1,
|
|
137
|
-
spent_micros: 19,
|
|
138
|
-
price_version: "v1",
|
|
139
|
-
}),
|
|
140
|
-
}))).toEqual({
|
|
141
|
-
requestId: "req-1",
|
|
142
|
-
settledMicros: 20,
|
|
143
|
-
settledUsdMicros: undefined,
|
|
144
|
-
remainingCreditMicros: 980,
|
|
145
|
-
reservedBalanceMicros: 1,
|
|
146
|
-
spentMicros: 19,
|
|
147
|
-
priceVersion: "v1",
|
|
148
|
-
});
|
|
149
|
-
expect((daemon as any).parseSellerSettlementSummary(new Headers({
|
|
150
|
-
"x-tokenbuddy-settlement": "{bad",
|
|
151
|
-
}))).toBeUndefined();
|
|
152
|
-
expect((daemon as any).parseSellerSettlementSummary(new Headers({
|
|
153
|
-
"x-tokenbuddy-settlement": JSON.stringify({ requestId: "missing-numbers" }),
|
|
154
|
-
}))).toBeUndefined();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("detects insufficient funds responses", () => {
|
|
158
|
-
expect((daemon as any).isInsufficientFundsResponse(500, "insufficient funds")).toBe(false);
|
|
159
|
-
expect((daemon as any).isInsufficientFundsResponse(402, JSON.stringify({ error: { code: "insufficient_funds" } }))).toBe(true);
|
|
160
|
-
expect((daemon as any).isInsufficientFundsResponse(402, JSON.stringify({ error: { message: "Insufficient funds available" } }))).toBe(true);
|
|
161
|
-
expect((daemon as any).isInsufficientFundsResponse(402, "{not-json insufficient funds")).toBe(true);
|
|
162
|
-
expect((daemon as any).isInsufficientFundsResponse(402, JSON.stringify({ error: { code: "other" } }))).toBe(false);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test("uses validated numeric environment defaults", () => {
|
|
166
|
-
const previousPurchase = process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS;
|
|
167
|
-
const previousRebuy = process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS;
|
|
168
|
-
const previousProof = process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS;
|
|
169
|
-
try {
|
|
170
|
-
delete process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS;
|
|
171
|
-
delete process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS;
|
|
172
|
-
delete process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS;
|
|
173
|
-
expect((daemon as any).autoPurchaseAmountUsdMicros()).toBe(2000000);
|
|
174
|
-
expect((daemon as any).tokenRebuyMinBalanceMicros()).toBe(200000);
|
|
175
|
-
expect((daemon as any).clawtipProofTimeoutMs()).toBe(120000);
|
|
176
|
-
|
|
177
|
-
process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS = "300";
|
|
178
|
-
process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS = "0";
|
|
179
|
-
process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS = "1000";
|
|
180
|
-
expect((daemon as any).autoPurchaseAmountUsdMicros()).toBe(300);
|
|
181
|
-
expect((daemon as any).tokenRebuyMinBalanceMicros()).toBe(0);
|
|
182
|
-
expect((daemon as any).clawtipProofTimeoutMs()).toBe(1000);
|
|
183
|
-
|
|
184
|
-
process.env.TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS = "0";
|
|
185
|
-
process.env.TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS = "-1";
|
|
186
|
-
process.env.TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS = "999";
|
|
187
|
-
expect((daemon as any).autoPurchaseAmountUsdMicros()).toBe(2000000);
|
|
188
|
-
expect((daemon as any).tokenRebuyMinBalanceMicros()).toBe(200000);
|
|
189
|
-
expect((daemon as any).clawtipProofTimeoutMs()).toBe(120000);
|
|
190
|
-
} finally {
|
|
191
|
-
restoreEnv("TB_PROXYD_AUTO_PURCHASE_AMOUNT_USD_MICROS", previousPurchase);
|
|
192
|
-
restoreEnv("TB_PROXYD_TOKEN_REBUY_MIN_BALANCE_MICROS", previousRebuy);
|
|
193
|
-
restoreEnv("TB_PROXYD_CLAWTIP_PROOF_TIMEOUT_MS", previousProof);
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
test("uses validated request deadline and token expiry margin defaults", () => {
|
|
198
|
-
const previousDeadline = process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
|
|
199
|
-
const previousMargin = process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS;
|
|
200
|
-
try {
|
|
201
|
-
delete process.env.TB_PROXYD_REQUEST_DEADLINE_MS;
|
|
202
|
-
delete process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS;
|
|
203
|
-
expect((daemon as any).requestDeadlineMs()).toBe(180000);
|
|
204
|
-
expect((daemon as any).tokenExpirySafetyMarginMs()).toBe(60000);
|
|
205
|
-
|
|
206
|
-
process.env.TB_PROXYD_REQUEST_DEADLINE_MS = "2500";
|
|
207
|
-
process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS = "0";
|
|
208
|
-
expect((daemon as any).requestDeadlineMs()).toBe(2500);
|
|
209
|
-
expect((daemon as any).tokenExpirySafetyMarginMs()).toBe(0);
|
|
210
|
-
|
|
211
|
-
process.env.TB_PROXYD_REQUEST_DEADLINE_MS = "999";
|
|
212
|
-
process.env.TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS = "-1";
|
|
213
|
-
expect((daemon as any).requestDeadlineMs()).toBe(180000);
|
|
214
|
-
expect((daemon as any).tokenExpirySafetyMarginMs()).toBe(60000);
|
|
215
|
-
} finally {
|
|
216
|
-
restoreEnv("TB_PROXYD_REQUEST_DEADLINE_MS", previousDeadline);
|
|
217
|
-
restoreEnv("TB_PROXYD_TOKEN_EXPIRY_SAFETY_MARGIN_MS", previousMargin);
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test("infers prompt hash input for object bodies only", () => {
|
|
222
|
-
expect((daemon as any).inferPromptForHash(null)).toBeUndefined();
|
|
223
|
-
expect((daemon as any).inferPromptForHash("text")).toBeUndefined();
|
|
224
|
-
expect((daemon as any).inferPromptForHash({ model: "gpt-5.4", messages: [] })).toBe(JSON.stringify({
|
|
225
|
-
model: "gpt-5.4",
|
|
226
|
-
messages: [],
|
|
227
|
-
}));
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("resolves payment proof from supported mock and ClawTip sources", async () => {
|
|
231
|
-
const previousProof = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
|
|
232
|
-
const previousOldProof = process.env.TOKENBUDDY_CLAWTIP_PROOF_FILE;
|
|
233
|
-
const previousProofFile = process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE;
|
|
234
|
-
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "tokenbuddy-proof-"));
|
|
235
|
-
const proofFile = path.join(tempDir, "proof.txt");
|
|
236
|
-
const route = routeFor("clawtip");
|
|
237
|
-
try {
|
|
238
|
-
expect(await (daemon as any).resolvePaymentProof(routeFor("mock"), {}, "req-1")).toBe("mock-proof-data");
|
|
239
|
-
await expect((daemon as any).resolvePaymentProof(routeFor("card"), {}, "req-1"))
|
|
240
|
-
.rejects.toThrow("unsupported payment method");
|
|
241
|
-
|
|
242
|
-
process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF = " proof-from-env ";
|
|
243
|
-
expect(await (daemon as any).resolvePaymentProof(route, {}, "req-1")).toBe("proof-from-env");
|
|
244
|
-
|
|
245
|
-
delete process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF;
|
|
246
|
-
fs.writeFileSync(proofFile, " proof-from-file\n");
|
|
247
|
-
process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE = proofFile;
|
|
248
|
-
expect(await (daemon as any).resolvePaymentProof(route, {}, "req-1")).toBe("proof-from-file");
|
|
249
|
-
|
|
250
|
-
delete process.env.TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE;
|
|
251
|
-
await expect((daemon as any).resolvePaymentProof(route, {}, "req-1"))
|
|
252
|
-
.rejects.toThrow("clawtip auto purchase requires");
|
|
253
|
-
} finally {
|
|
254
|
-
restoreEnv("TOKENBUDDY_CLAWTIP_PAYMENT_PROOF", previousProof);
|
|
255
|
-
restoreEnv("TOKENBUDDY_CLAWTIP_PROOF_FILE", previousOldProof);
|
|
256
|
-
restoreEnv("TOKENBUDDY_CLAWTIP_PAYMENT_PROOF_FILE", previousProofFile);
|
|
257
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("copies only safe upstream headers", () => {
|
|
262
|
-
const upstream = {
|
|
263
|
-
headers: new Headers({
|
|
264
|
-
"content-type": "application/json",
|
|
265
|
-
"content-length": "20",
|
|
266
|
-
connection: "keep-alive",
|
|
267
|
-
"x-tokenbuddy-test": "ok",
|
|
268
|
-
}),
|
|
269
|
-
} as Response;
|
|
270
|
-
const res = {
|
|
271
|
-
setHeader: jest.fn(),
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
(daemon as any).copyUpstreamHeaders(upstream, res);
|
|
275
|
-
|
|
276
|
-
expect(res.setHeader).toHaveBeenCalledWith("content-type", "application/json");
|
|
277
|
-
expect(res.setHeader).toHaveBeenCalledWith("x-tokenbuddy-test", "ok");
|
|
278
|
-
expect(res.setHeader).not.toHaveBeenCalledWith("content-length", expect.anything());
|
|
279
|
-
expect(res.setHeader).not.toHaveBeenCalledWith("connection", expect.anything());
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test("resolves focus sets from explicit config and environment", () => {
|
|
283
|
-
const explicitDaemon = new TokenbuddyDaemon({
|
|
284
|
-
controlPort: 0,
|
|
285
|
-
proxyPort: 0,
|
|
286
|
-
dbPath: ":memory:",
|
|
287
|
-
sellerRegistryUrl: "http://127.0.0.1:1/registry",
|
|
288
|
-
warmupModels: ["explicit-a", "explicit-b"],
|
|
289
|
-
});
|
|
290
|
-
const previousWarmup = process.env.TB_BUYER_WARMUP_MODELS;
|
|
291
|
-
try {
|
|
292
|
-
expect((explicitDaemon as any).resolveFocusSet()).toEqual(["explicit-a", "explicit-b"]);
|
|
293
|
-
|
|
294
|
-
process.env.TB_BUYER_WARMUP_MODELS = " env-a, ,env-b ";
|
|
295
|
-
expect((daemon as any).resolveFocusSet()).toEqual(["env-a", "env-b"]);
|
|
296
|
-
|
|
297
|
-
delete process.env.TB_BUYER_WARMUP_MODELS;
|
|
298
|
-
expect((daemon as any).resolveFocusSet()).toEqual([]);
|
|
299
|
-
} finally {
|
|
300
|
-
explicitDaemon.stop();
|
|
301
|
-
restoreEnv("TB_BUYER_WARMUP_MODELS", previousWarmup);
|
|
302
|
-
}
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
test("resolves prewarm protocol from the model index", () => {
|
|
306
|
-
(daemon as any).modelIndex.rebuild([
|
|
307
|
-
{
|
|
308
|
-
id: "seller-chat",
|
|
309
|
-
url: "https://seller-chat.example.com",
|
|
310
|
-
models: ["chat-model"],
|
|
311
|
-
supportedProtocols: ["chat_completions"],
|
|
312
|
-
paymentMethods: ["clawtip"],
|
|
313
|
-
},
|
|
314
|
-
{
|
|
315
|
-
id: "seller-messages",
|
|
316
|
-
url: "https://seller-messages.example.com",
|
|
317
|
-
models: ["message-model"],
|
|
318
|
-
supportedProtocols: ["messages"],
|
|
319
|
-
paymentMethods: ["clawtip"],
|
|
320
|
-
},
|
|
321
|
-
{
|
|
322
|
-
id: "seller-responses",
|
|
323
|
-
url: "https://seller-responses.example.com",
|
|
324
|
-
models: ["response-model"],
|
|
325
|
-
supportedProtocols: ["responses"],
|
|
326
|
-
paymentMethods: ["clawtip"],
|
|
327
|
-
},
|
|
328
|
-
]);
|
|
329
|
-
|
|
330
|
-
expect((daemon as any).resolvePrewarmProtocol("chat-model")).toBe("chat_completions");
|
|
331
|
-
expect((daemon as any).resolvePrewarmProtocol("message-model")).toBe("messages");
|
|
332
|
-
expect((daemon as any).resolvePrewarmProtocol("response-model")).toBe("responses");
|
|
333
|
-
expect((daemon as any).resolvePrewarmProtocol("missing-model")).toBeUndefined();
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("reuses fresh cached seller tokens", async () => {
|
|
337
|
-
(daemon as any).tokenStore.saveToken(
|
|
338
|
-
"seller-a",
|
|
339
|
-
"cached-token",
|
|
340
|
-
"model:gpt-5.4",
|
|
341
|
-
900000,
|
|
342
|
-
"2099-01-01T00:00:00.000Z"
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-cache"))
|
|
346
|
-
.resolves.toBe("cached-token");
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
test("purchases and persists a token when cache is missing", async () => {
|
|
350
|
-
const fetchMock = jest.spyOn(globalThis, "fetch").mockImplementation(async (url: string | URL | Request) => {
|
|
351
|
-
const href = String(url);
|
|
352
|
-
if (href === "https://seller-a.example.com/purchase/create") {
|
|
353
|
-
return response({
|
|
354
|
-
purchase_id: "purchase-1",
|
|
355
|
-
status: "pending",
|
|
356
|
-
payment_reference: "pay-ref",
|
|
357
|
-
expires_at: "2099-01-01T00:00:00.000Z",
|
|
358
|
-
credit_micros: 500000,
|
|
359
|
-
currency: "USD",
|
|
360
|
-
payment_instructions: { type: "mock" },
|
|
361
|
-
quote: { amount: 5 },
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
if (href === "https://seller-a.example.com/purchase/complete") {
|
|
365
|
-
return response({
|
|
366
|
-
access_token: "new-token",
|
|
367
|
-
token_class: "model:gpt-5.4",
|
|
368
|
-
credit_micros: 500000,
|
|
369
|
-
currency: "USD",
|
|
370
|
-
payment_reference: "pay-ref",
|
|
371
|
-
status: "funded",
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
throw new Error(`unexpected fetch ${href}`);
|
|
375
|
-
}) as jest.MockedFunction<typeof fetch>;
|
|
376
|
-
|
|
377
|
-
try {
|
|
378
|
-
await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-buy"))
|
|
379
|
-
.resolves.toBe("new-token");
|
|
380
|
-
expect((daemon as any).tokenStore.getToken("seller-a")).toMatchObject({
|
|
381
|
-
token: "new-token",
|
|
382
|
-
balanceMicros: 500000,
|
|
383
|
-
});
|
|
384
|
-
expect((daemon as any).tokenStore.listPurchaseLedger()).toHaveLength(1);
|
|
385
|
-
} finally {
|
|
386
|
-
fetchMock.mockRestore();
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("surfaces purchase create and complete failures", async () => {
|
|
391
|
-
const fetchMock = jest.spyOn(globalThis, "fetch");
|
|
392
|
-
try {
|
|
393
|
-
fetchMock.mockResolvedValueOnce(response({ error: { message: "create denied" } }, false, 402));
|
|
394
|
-
await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-create-fail"))
|
|
395
|
-
.rejects.toThrow("create denied");
|
|
396
|
-
|
|
397
|
-
fetchMock
|
|
398
|
-
.mockResolvedValueOnce(response({ purchaseId: "purchase-2" }))
|
|
399
|
-
.mockResolvedValueOnce(response({ error: { message: "complete denied" } }, false, 500));
|
|
400
|
-
await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-complete-fail"))
|
|
401
|
-
.rejects.toThrow("complete denied");
|
|
402
|
-
} finally {
|
|
403
|
-
fetchMock.mockRestore();
|
|
404
|
-
}
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
test("validates required purchase response fields", async () => {
|
|
408
|
-
const fetchMock = jest.spyOn(globalThis, "fetch");
|
|
409
|
-
try {
|
|
410
|
-
fetchMock.mockResolvedValueOnce(response({ status: "pending" }));
|
|
411
|
-
await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-no-purchase"))
|
|
412
|
-
.rejects.toThrow("purchase/create response missing purchaseId");
|
|
413
|
-
|
|
414
|
-
fetchMock
|
|
415
|
-
.mockResolvedValueOnce(response({ purchaseId: "purchase-3" }))
|
|
416
|
-
.mockResolvedValueOnce(response({ status: "funded" }));
|
|
417
|
-
await expect((daemon as any).getOrPurchaseToken(routeFor("mock"), "req-no-token"))
|
|
418
|
-
.rejects.toThrow("purchase/complete response missing accessToken");
|
|
419
|
-
} finally {
|
|
420
|
-
fetchMock.mockRestore();
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
function restoreEnv(name: string, value: string | undefined): void {
|
|
426
|
-
if (value === undefined) {
|
|
427
|
-
delete process.env[name];
|
|
428
|
-
} else {
|
|
429
|
-
process.env[name] = value;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
function routeFor(paymentMethod: string): unknown {
|
|
434
|
-
return {
|
|
435
|
-
seller: {
|
|
436
|
-
id: "seller-a",
|
|
437
|
-
url: "https://seller-a.example.com",
|
|
438
|
-
},
|
|
439
|
-
manifest: null,
|
|
440
|
-
protocol: "chat_completions",
|
|
441
|
-
modelId: "gpt-5.4",
|
|
442
|
-
paymentMethod,
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
function response(body: unknown, ok = true, status = 200): Response {
|
|
447
|
-
return {
|
|
448
|
-
ok,
|
|
449
|
-
status,
|
|
450
|
-
json: async () => body,
|
|
451
|
-
} as Response;
|
|
452
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
2
|
-
import type { ClaudeCodeModelMappingConfig, SingleModelProviderRuntimeConfig } from "../src/provider-install.js";
|
|
3
|
-
|
|
4
|
-
describe("TokenbuddyDaemon resolveClaudeRoleModel", () => {
|
|
5
|
-
let daemon: TokenbuddyDaemon;
|
|
6
|
-
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
daemon = new TokenbuddyDaemon({
|
|
9
|
-
controlPort: 0,
|
|
10
|
-
proxyPort: 0,
|
|
11
|
-
dbPath: ":memory:",
|
|
12
|
-
sellerRegistryUrl: "http://127.0.0.1:1/registry",
|
|
13
|
-
});
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(() => {
|
|
17
|
-
daemon.stop();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function saveConfig(config: ClaudeCodeModelMappingConfig | SingleModelProviderRuntimeConfig): void {
|
|
21
|
-
(daemon as any).tokenStore.saveProviderRuntimeConfig("claude-code", config);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
test("returns stripped model id when runtime config is absent", () => {
|
|
25
|
-
expect((daemon as any).resolveClaudeRoleModel("claude-sonnet-4-6 [1M]")).toBe("claude-sonnet-4-6");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test("returns stripped model id when selection kind is not role mapping", () => {
|
|
29
|
-
saveConfig({
|
|
30
|
-
selectionKind: "single-model",
|
|
31
|
-
defaultModel: "upstream-default",
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
expect((daemon as any).resolveClaudeRoleModel("sonnet [1M]")).toBe("sonnet");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
test("maps haiku aliases to haiku upstream model", () => {
|
|
38
|
-
saveConfig({
|
|
39
|
-
selectionKind: "claude-role-mapping",
|
|
40
|
-
roles: {
|
|
41
|
-
haiku: { upstreamModel: "upstream-haiku" },
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
expect((daemon as any).resolveClaudeRoleModel("haiku")).toBe("upstream-haiku");
|
|
46
|
-
expect((daemon as any).resolveClaudeRoleModel("claude-haiku-4-5 [1M]")).toBe("upstream-haiku");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("maps sonnet aliases to sonnet upstream model", () => {
|
|
50
|
-
saveConfig({
|
|
51
|
-
selectionKind: "claude-role-mapping",
|
|
52
|
-
roles: {
|
|
53
|
-
sonnet: { upstreamModel: "upstream-sonnet" },
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
expect((daemon as any).resolveClaudeRoleModel("sonnet")).toBe("upstream-sonnet");
|
|
58
|
-
expect((daemon as any).resolveClaudeRoleModel("claude-sonnet-4-6")).toBe("upstream-sonnet");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("maps opus aliases to opus upstream model", () => {
|
|
62
|
-
saveConfig({
|
|
63
|
-
selectionKind: "claude-role-mapping",
|
|
64
|
-
roles: {
|
|
65
|
-
opus: { upstreamModel: "upstream-opus" },
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
expect((daemon as any).resolveClaudeRoleModel("opus")).toBe("upstream-opus");
|
|
70
|
-
expect((daemon as any).resolveClaudeRoleModel("claude-opus-4-7")).toBe("upstream-opus");
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("uses fallback for unknown roles and missing role bindings", () => {
|
|
74
|
-
saveConfig({
|
|
75
|
-
selectionKind: "claude-role-mapping",
|
|
76
|
-
fallbackModel: "upstream-fallback",
|
|
77
|
-
roles: {},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
expect((daemon as any).resolveClaudeRoleModel("custom-role")).toBe("upstream-fallback");
|
|
81
|
-
expect((daemon as any).resolveClaudeRoleModel("haiku")).toBe("upstream-fallback");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("returns stripped model id when role mapping has no match or fallback", () => {
|
|
85
|
-
saveConfig({
|
|
86
|
-
selectionKind: "claude-role-mapping",
|
|
87
|
-
roles: {},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
expect((daemon as any).resolveClaudeRoleModel("custom-role [1M]")).toBe("custom-role");
|
|
91
|
-
});
|
|
92
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import * as http from "http";
|
|
2
|
-
import * as fs from "fs";
|
|
3
|
-
import * as path from "path";
|
|
4
|
-
import { AddressInfo } from "net";
|
|
5
|
-
import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
6
|
-
import { BuyerStore } from "../src/buyer-store.js";
|
|
7
|
-
|
|
8
|
-
describe("TokenbuddyDaemon trusted registry cache", () => {
|
|
9
|
-
const TEMP_DB = path.resolve(__dirname, "../../data-test/trusted-registry-cache-test.db");
|
|
10
|
-
let server: http.Server;
|
|
11
|
-
let serverPort: number;
|
|
12
|
-
let registryAvailable = true;
|
|
13
|
-
let daemon: TokenbuddyDaemon | undefined;
|
|
14
|
-
|
|
15
|
-
function rmDb(): void {
|
|
16
|
-
for (const suffix of ["", "-wal", "-shm"]) {
|
|
17
|
-
fs.rmSync(`${TEMP_DB}${suffix}`, { force: true });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function startDaemon(): { controlPort: number; proxyPort: number } {
|
|
22
|
-
daemon = new TokenbuddyDaemon({
|
|
23
|
-
controlPort: 0,
|
|
24
|
-
proxyPort: 0,
|
|
25
|
-
dbPath: TEMP_DB,
|
|
26
|
-
sellerRegistryUrl: `http://127.0.0.1:${serverPort}/registry/sellers`
|
|
27
|
-
});
|
|
28
|
-
daemon.start();
|
|
29
|
-
return {
|
|
30
|
-
controlPort: ((daemon as unknown as { controlServer: { address(): AddressInfo } }).controlServer.address()).port,
|
|
31
|
-
proxyPort: ((daemon as unknown as { proxyServer: { address(): AddressInfo } }).proxyServer.address()).port
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function stopDaemon(): Promise<void> {
|
|
36
|
-
daemon?.stop();
|
|
37
|
-
daemon = undefined;
|
|
38
|
-
await new Promise<void>((resolve) => setTimeout(resolve, 50));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
beforeAll((done) => {
|
|
42
|
-
server = http.createServer((req, res) => {
|
|
43
|
-
res.setHeader("Content-Type", "application/json");
|
|
44
|
-
if (req.url === "/registry/sellers") {
|
|
45
|
-
if (!registryAvailable) {
|
|
46
|
-
res.statusCode = 500;
|
|
47
|
-
res.end(JSON.stringify({ error: "registry unavailable" }));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
res.statusCode = 200;
|
|
51
|
-
res.end(JSON.stringify({
|
|
52
|
-
version: 7,
|
|
53
|
-
defaultSeller: "cached-seller",
|
|
54
|
-
sellers: [
|
|
55
|
-
{
|
|
56
|
-
id: "cached-seller",
|
|
57
|
-
name: "Cached Seller",
|
|
58
|
-
status: "active",
|
|
59
|
-
url: `http://127.0.0.1:${serverPort}/cached-seller`,
|
|
60
|
-
supportedProtocols: ["chat_completions"],
|
|
61
|
-
paymentMethods: ["mock"],
|
|
62
|
-
models: ["gpt-cache"]
|
|
63
|
-
}
|
|
64
|
-
]
|
|
65
|
-
}));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
if (req.url === "/cached-seller/manifest") {
|
|
69
|
-
res.statusCode = 200;
|
|
70
|
-
res.end(JSON.stringify({
|
|
71
|
-
sellerId: "cached-seller",
|
|
72
|
-
supportedProtocols: ["chat_completions"],
|
|
73
|
-
paymentMethods: ["mock"],
|
|
74
|
-
models: [{ id: "gpt-cache" }]
|
|
75
|
-
}));
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
res.statusCode = 404;
|
|
79
|
-
res.end(JSON.stringify({ error: "not found" }));
|
|
80
|
-
});
|
|
81
|
-
server.listen(0, "127.0.0.1", () => {
|
|
82
|
-
serverPort = (server.address() as AddressInfo).port;
|
|
83
|
-
done();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
afterAll((done) => {
|
|
88
|
-
server.close(() => done());
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
beforeEach(() => {
|
|
92
|
-
rmDb();
|
|
93
|
-
registryAvailable = true;
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
afterEach(async () => {
|
|
97
|
-
await stopDaemon();
|
|
98
|
-
rmDb();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test("persists a trusted registry snapshot and reuses it after daemon restart", async () => {
|
|
102
|
-
let ports = startDaemon();
|
|
103
|
-
const firstModels = await (await fetch(`http://127.0.0.1:${ports.proxyPort}/v1/models`)).json() as any;
|
|
104
|
-
expect(firstModels.data).toEqual(expect.arrayContaining([
|
|
105
|
-
expect.objectContaining({ id: "gpt-cache", sellerId: "cached-seller" })
|
|
106
|
-
]));
|
|
107
|
-
|
|
108
|
-
const store = new BuyerStore({ dbPath: TEMP_DB });
|
|
109
|
-
const cache = store.getDaemonRuntimeConfig<any>("trusted-registry-snapshot")?.config;
|
|
110
|
-
store.close();
|
|
111
|
-
expect(cache).toMatchObject({
|
|
112
|
-
schemaVersion: 1,
|
|
113
|
-
registryUrl: `http://127.0.0.1:${serverPort}/registry/sellers`,
|
|
114
|
-
version: 7,
|
|
115
|
-
trust: {
|
|
116
|
-
verified: false
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
expect(cache.registrySha256).toMatch(/^[a-f0-9]{64}$/);
|
|
120
|
-
|
|
121
|
-
await stopDaemon();
|
|
122
|
-
registryAvailable = false;
|
|
123
|
-
|
|
124
|
-
ports = startDaemon();
|
|
125
|
-
const secondModelsResponse = await fetch(`http://127.0.0.1:${ports.proxyPort}/v1/models`);
|
|
126
|
-
expect(secondModelsResponse.status).toBe(200);
|
|
127
|
-
const secondModels = await secondModelsResponse.json() as any;
|
|
128
|
-
expect(secondModels.data).toEqual([
|
|
129
|
-
expect.objectContaining({ id: "gpt-cache", sellerId: "cached-seller" })
|
|
130
|
-
]);
|
|
131
|
-
});
|
|
132
|
-
});
|