@tokenbuddy/tokenbuddy 1.0.6 → 1.0.8
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 +28 -1
- package/dist/src/buyer-store.d.ts.map +1 -1
- package/dist/src/buyer-store.js +71 -16
- package/dist/src/buyer-store.js.map +1 -1
- package/dist/src/cli.d.ts +17 -0
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +201 -32
- package/dist/src/cli.js.map +1 -1
- package/dist/src/daemon.d.ts +5 -0
- package/dist/src/daemon.d.ts.map +1 -1
- package/dist/src/daemon.js +279 -72
- package/dist/src/daemon.js.map +1 -1
- package/dist/src/doctor-clawtip-wallet.d.ts +14 -0
- package/dist/src/doctor-clawtip-wallet.d.ts.map +1 -0
- package/dist/src/doctor-clawtip-wallet.js +54 -0
- package/dist/src/doctor-clawtip-wallet.js.map +1 -0
- package/dist/src/doctor-diagnostics.d.ts +2 -0
- package/dist/src/doctor-diagnostics.d.ts.map +1 -1
- package/dist/src/doctor-diagnostics.js +5 -0
- package/dist/src/doctor-diagnostics.js.map +1 -1
- package/dist/src/init-clawtip-activation.d.ts +48 -0
- package/dist/src/init-clawtip-activation.d.ts.map +1 -0
- package/dist/src/init-clawtip-activation.js +395 -0
- package/dist/src/init-clawtip-activation.js.map +1 -0
- package/dist/src/init-payment-options.d.ts +23 -1
- package/dist/src/init-payment-options.d.ts.map +1 -1
- package/dist/src/init-payment-options.js +97 -22
- package/dist/src/init-payment-options.js.map +1 -1
- package/dist/src/terminal-image.d.ts +22 -0
- package/dist/src/terminal-image.d.ts.map +1 -0
- package/dist/src/terminal-image.js +135 -0
- package/dist/src/terminal-image.js.map +1 -0
- package/package.json +1 -1
- package/src/buyer-store.ts +140 -17
- package/src/cli.ts +251 -33
- package/src/daemon.ts +308 -53
- package/src/doctor-clawtip-wallet.ts +70 -0
- package/src/doctor-diagnostics.ts +11 -0
- package/src/init-clawtip-activation.ts +487 -0
- package/src/init-payment-options.ts +140 -22
- package/src/terminal-image.ts +187 -0
- package/tests/e2e.test.ts +79 -5
- package/tests/tokenbuddy.test.ts +745 -19
package/tests/tokenbuddy.test.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { TokenbuddyDaemon } from "../src/daemon.js";
|
|
2
|
-
import { BuyerStore, resolveBuyerStorePath } from "../src/buyer-store.js";
|
|
3
|
-
import {
|
|
2
|
+
import { BuyerStore, resolveBuyerStorePath, type PaymentConfig } from "../src/buyer-store.js";
|
|
3
|
+
import {
|
|
4
|
+
buildCli,
|
|
5
|
+
fetchClawtipBootstrap,
|
|
6
|
+
normalizeClawtipBootstrapResourceUrl,
|
|
7
|
+
} from "../src/cli.js";
|
|
8
|
+
import {
|
|
9
|
+
checkOpenClawRuntime,
|
|
10
|
+
parseClawtipCliOutput,
|
|
11
|
+
readClawtipPayCredential,
|
|
12
|
+
resolveClawtipQrMediaPath,
|
|
13
|
+
startClawtipWalletBootstrap,
|
|
14
|
+
waitForClawtipActivationConfirmation,
|
|
15
|
+
writeClawtipOrderFile,
|
|
16
|
+
} from "../src/init-clawtip-activation.js";
|
|
4
17
|
import {
|
|
5
18
|
applyProviderInstall,
|
|
6
19
|
detectProviders,
|
|
@@ -10,14 +23,21 @@ import {
|
|
|
10
23
|
import { detectTerminals } from "../src/terminal-detect.js";
|
|
11
24
|
import {
|
|
12
25
|
buildInitSuccessMessage,
|
|
13
|
-
|
|
26
|
+
buildInitTerminalSelectionState,
|
|
14
27
|
detectExistingClawtipBinding,
|
|
28
|
+
inspectClawtipWalletReadiness,
|
|
29
|
+
inspectOpenClawWalletConfig,
|
|
15
30
|
INIT_COMING_SOON_PAYMENT_OPTIONS,
|
|
16
31
|
INIT_PAYMENT_OPTIONS,
|
|
17
32
|
noteInitComingSoonPayments,
|
|
18
33
|
OTHER_TERMINAL_OPTION,
|
|
19
34
|
validateInitTerminalSelection,
|
|
20
35
|
} from "../src/init-payment-options.js";
|
|
36
|
+
import { printDoctorClawtipWallet } from "../src/doctor-clawtip-wallet.js";
|
|
37
|
+
import {
|
|
38
|
+
detectTerminalImageDisplay,
|
|
39
|
+
displayTerminalImage,
|
|
40
|
+
} from "../src/terminal-image.js";
|
|
21
41
|
import * as path from "path";
|
|
22
42
|
import * as fs from "fs";
|
|
23
43
|
import http from "http";
|
|
@@ -26,6 +46,8 @@ import zlib from "zlib";
|
|
|
26
46
|
|
|
27
47
|
const TEMP_BUYER_DB = path.resolve(__dirname, "../../data-test/buyer-cache-test.db");
|
|
28
48
|
const TEMP_STORE_ROOT = path.resolve(__dirname, "../../data-test/buyer-store-test");
|
|
49
|
+
const INSPECTION_STORE_ROOT = path.resolve(__dirname, "../../data-test/json-inspection-store");
|
|
50
|
+
const INSPECTION_HOME = path.resolve(__dirname, "../../data-test/json-inspection-home");
|
|
29
51
|
const PACKAGE_JSON = path.resolve(__dirname, "../package.json");
|
|
30
52
|
|
|
31
53
|
function rmSqliteFiles(dbPath: string): void {
|
|
@@ -119,15 +141,27 @@ describe("BuyerStore safe SQLite persistence", () => {
|
|
|
119
141
|
expect(store.getToken("seller-a")).toBeUndefined();
|
|
120
142
|
|
|
121
143
|
store.saveToken("seller-a", "raw-token-secret", "model:gpt-4", 500000, "2030-01-01T00:00:00.000Z");
|
|
122
|
-
expect(store.getToken("seller-a")).
|
|
144
|
+
expect(store.getToken("seller-a")).toMatchObject({
|
|
123
145
|
token: "raw-token-secret",
|
|
124
|
-
balanceMicros: 500000
|
|
146
|
+
balanceMicros: 500000,
|
|
147
|
+
reservedMicros: 0,
|
|
148
|
+
spentMicros: 0,
|
|
149
|
+
balanceSource: "purchase_complete"
|
|
125
150
|
});
|
|
126
151
|
|
|
127
|
-
store.
|
|
128
|
-
|
|
152
|
+
store.reconcileTokenBalance({
|
|
153
|
+
sellerKey: "seller-a",
|
|
154
|
+
balanceMicros: 499890,
|
|
155
|
+
reservedMicros: 0,
|
|
156
|
+
spentMicros: 110,
|
|
157
|
+
balanceSource: "seller_settlement_summary"
|
|
158
|
+
});
|
|
159
|
+
expect(store.getToken("seller-a")).toMatchObject({
|
|
129
160
|
token: "raw-token-secret",
|
|
130
|
-
balanceMicros:
|
|
161
|
+
balanceMicros: 499890,
|
|
162
|
+
reservedMicros: 0,
|
|
163
|
+
spentMicros: 110,
|
|
164
|
+
balanceSource: "seller_settlement_summary"
|
|
131
165
|
});
|
|
132
166
|
});
|
|
133
167
|
|
|
@@ -471,6 +505,55 @@ describe("TokenBuddy init payment options", () => {
|
|
|
471
505
|
}));
|
|
472
506
|
});
|
|
473
507
|
|
|
508
|
+
test("requires the OpenClaw wallet config before reusing ClawTip metadata", () => {
|
|
509
|
+
const payment: PaymentConfig = {
|
|
510
|
+
method: "clawtip",
|
|
511
|
+
enabled: true,
|
|
512
|
+
isDefault: true,
|
|
513
|
+
updatedAt: "2026-05-30T00:00:00.000Z",
|
|
514
|
+
config: {
|
|
515
|
+
orderNo: "order_123",
|
|
516
|
+
resourceUrl: "https://example.test/pay"
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const missingWallet = inspectClawtipWalletReadiness(payment, {
|
|
521
|
+
expectedPath: "/tmp/home/.openclaw/configs/config.json",
|
|
522
|
+
configsDirExists: true,
|
|
523
|
+
exists: false,
|
|
524
|
+
alternatePaths: ["/tmp/home/.openclaw/configs/config.json.bak"]
|
|
525
|
+
});
|
|
526
|
+
expect(missingWallet.status).toBe("metadata_missing_wallet");
|
|
527
|
+
expect(missingWallet.savedBinding?.orderNo).toBe("order_123");
|
|
528
|
+
expect(missingWallet.reusableBinding).toBeUndefined();
|
|
529
|
+
|
|
530
|
+
const readyWallet = inspectClawtipWalletReadiness(payment, {
|
|
531
|
+
expectedPath: "/tmp/home/.openclaw/configs/config.json",
|
|
532
|
+
configsDirExists: true,
|
|
533
|
+
exists: true,
|
|
534
|
+
alternatePaths: []
|
|
535
|
+
});
|
|
536
|
+
expect(readyWallet.status).toBe("ready");
|
|
537
|
+
expect(readyWallet.reusableBinding?.orderNo).toBe("order_123");
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("detects renamed OpenClaw wallet config files as nearby files", () => {
|
|
541
|
+
const home = path.join(TEMP_STORE_ROOT, "openclaw-home");
|
|
542
|
+
const configsDir = path.join(home, ".openclaw", "configs");
|
|
543
|
+
rmDir(home);
|
|
544
|
+
fs.mkdirSync(configsDir, { recursive: true });
|
|
545
|
+
fs.writeFileSync(path.join(configsDir, "config.json.bak"), "{}", "utf8");
|
|
546
|
+
|
|
547
|
+
const wallet = inspectOpenClawWalletConfig(home);
|
|
548
|
+
|
|
549
|
+
expect(wallet.exists).toBe(false);
|
|
550
|
+
expect(wallet.configsDirExists).toBe(true);
|
|
551
|
+
expect(wallet.expectedPath).toBe(path.join(configsDir, "config.json"));
|
|
552
|
+
expect(wallet.alternatePaths).toEqual([
|
|
553
|
+
path.join(configsDir, "config.json.bak")
|
|
554
|
+
]);
|
|
555
|
+
});
|
|
556
|
+
|
|
474
557
|
test("builds a clear init success message with summary lines", () => {
|
|
475
558
|
const message = buildInitSuccessMessage([
|
|
476
559
|
"2 programming terminals configured for TokenBuddy.",
|
|
@@ -483,8 +566,8 @@ describe("TokenBuddy init payment options", () => {
|
|
|
483
566
|
expect(message).toContain("Run `tb doctor` to audit status anytime.");
|
|
484
567
|
});
|
|
485
568
|
|
|
486
|
-
test("builds terminal
|
|
487
|
-
const
|
|
569
|
+
test("builds terminal selection with configured entries separated from selectable options", () => {
|
|
570
|
+
const selection = buildInitTerminalSelectionState([
|
|
488
571
|
{
|
|
489
572
|
id: "codex",
|
|
490
573
|
name: "Codex CLI",
|
|
@@ -505,11 +588,13 @@ describe("TokenBuddy init payment options", () => {
|
|
|
505
588
|
}
|
|
506
589
|
]);
|
|
507
590
|
|
|
508
|
-
expect(
|
|
591
|
+
expect(selection.installed).toEqual([
|
|
509
592
|
expect.objectContaining({
|
|
510
|
-
value: "codex
|
|
593
|
+
value: "codex",
|
|
511
594
|
label: "Codex CLI(已安装)"
|
|
512
|
-
})
|
|
595
|
+
})
|
|
596
|
+
]);
|
|
597
|
+
expect(selection.options).toEqual([
|
|
513
598
|
expect.objectContaining({
|
|
514
599
|
value: "hermes",
|
|
515
600
|
label: "Hermes Terminal"
|
|
@@ -521,14 +606,467 @@ describe("TokenBuddy init payment options", () => {
|
|
|
521
606
|
]);
|
|
522
607
|
});
|
|
523
608
|
|
|
524
|
-
test("requires at least one
|
|
609
|
+
test("requires at least one terminal choice", () => {
|
|
525
610
|
expect(validateInitTerminalSelection([])).toBe("Select at least one terminal or choose Other.");
|
|
526
|
-
expect(validateInitTerminalSelection(["codex:installed"])).toBe(
|
|
527
|
-
"Installed terminals are already configured. Select another terminal or choose Other."
|
|
528
|
-
);
|
|
529
611
|
expect(validateInitTerminalSelection(["other"])).toBeUndefined();
|
|
530
612
|
expect(validateInitTerminalSelection(["hermes"])).toBeUndefined();
|
|
531
613
|
});
|
|
614
|
+
|
|
615
|
+
test("waits for ClawTip scan confirmation until the wallet config appears", async () => {
|
|
616
|
+
let attempts = 0;
|
|
617
|
+
const inspectWalletConfig = jest.fn(() => {
|
|
618
|
+
attempts += 1;
|
|
619
|
+
return {
|
|
620
|
+
expectedPath: "/tmp/home/.openclaw/configs/config.json",
|
|
621
|
+
configsDirExists: true,
|
|
622
|
+
exists: attempts >= 3,
|
|
623
|
+
alternatePaths: []
|
|
624
|
+
};
|
|
625
|
+
});
|
|
626
|
+
const sleep = jest.fn(async () => undefined);
|
|
627
|
+
|
|
628
|
+
await expect(waitForClawtipActivationConfirmation({
|
|
629
|
+
inspectWalletConfig,
|
|
630
|
+
pollIntervalMs: 10,
|
|
631
|
+
sleep,
|
|
632
|
+
})).resolves.toBe(true);
|
|
633
|
+
|
|
634
|
+
expect(inspectWalletConfig).toHaveBeenCalledTimes(3);
|
|
635
|
+
expect(sleep).toHaveBeenCalledTimes(2);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("cancels ClawTip scan confirmation on Ctrl+C", async () => {
|
|
639
|
+
const cancelled = jest.fn();
|
|
640
|
+
await expect(
|
|
641
|
+
waitForClawtipActivationConfirmation({
|
|
642
|
+
inspectWalletConfig: () => ({
|
|
643
|
+
expectedPath: "/tmp/home/.openclaw/configs/config.json",
|
|
644
|
+
configsDirExists: true,
|
|
645
|
+
exists: false,
|
|
646
|
+
alternatePaths: []
|
|
647
|
+
}),
|
|
648
|
+
isCancelled: () => true,
|
|
649
|
+
cancel: cancelled,
|
|
650
|
+
pollIntervalMs: 10,
|
|
651
|
+
sleep: async () => undefined,
|
|
652
|
+
})
|
|
653
|
+
).resolves.toBe(false);
|
|
654
|
+
|
|
655
|
+
expect(cancelled).toHaveBeenCalledWith("ClawTip activation cancelled.");
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("parses ClawTip auth URL and clawtipId from pay CLI output", () => {
|
|
659
|
+
const parsed = parseClawtipCliOutput(
|
|
660
|
+
"请扫码 https://clawtip.jd.com/qrcode?foo=1&clawtipId=device-789 完成授权"
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(parsed.requiresWalletAuth).toBe(true);
|
|
664
|
+
expect(parsed.authUrl).toBe("https://clawtip.jd.com/qrcode?foo=1&clawtipId=device-789");
|
|
665
|
+
expect(parsed.clawtipId).toBe("device-789");
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("ignores bootstrap env endpoint noise when parsing the real ClawTip auth URL", () => {
|
|
669
|
+
const parsed = parseClawtipCliOutput([
|
|
670
|
+
"process.env.CLAWTIP_PAY https://ms.jr.jd.com/gw2/generic/hyqy/h5/m/clawtipPay",
|
|
671
|
+
"请扫码 https://idt.jd.com/unifiedAuthM/plat/safeMonitor/?authFlowUid=abc 完成授权",
|
|
672
|
+
].join("\n"));
|
|
673
|
+
|
|
674
|
+
expect(parsed.authUrl).toBe(
|
|
675
|
+
"https://idt.jd.com/unifiedAuthM/plat/safeMonitor/?authFlowUid=abc"
|
|
676
|
+
);
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
test("parses the official ClawTip wallet QR image path", () => {
|
|
680
|
+
const parsed = parseClawtipCliOutput([
|
|
681
|
+
"从 .env.prod 加载环境变量",
|
|
682
|
+
"/Users/test/.openclaw/workspace/clawtip/qrcode/clawtip-index-code.png",
|
|
683
|
+
].join("\n"));
|
|
684
|
+
|
|
685
|
+
expect(parsed.mediaPath).toBe("/Users/test/.openclaw/workspace/clawtip/qrcode/clawtip-index-code.png");
|
|
686
|
+
expect(parsed.requiresWalletAuth).toBe(true);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("surfaces ClawTip upstream payment errors directly", async () => {
|
|
690
|
+
const parsed = parseClawtipCliOutput("ClawTip 返回错误:商家信息有误");
|
|
691
|
+
|
|
692
|
+
expect(parsed.failureMessage).toContain("商家信息有误");
|
|
693
|
+
await expect(startClawtipWalletBootstrap({
|
|
694
|
+
orderNo: "order_error",
|
|
695
|
+
amountFen: 1,
|
|
696
|
+
payTo: "pay-to-test",
|
|
697
|
+
encryptedData: "ciphertext",
|
|
698
|
+
indicator: "indicator_error",
|
|
699
|
+
slug: "tb-wallet-bootstrap",
|
|
700
|
+
skillId: "si-tb-wallet-bootstrap",
|
|
701
|
+
description: "TokenBuddy ClawTip wallet activation",
|
|
702
|
+
resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
|
|
703
|
+
}, {
|
|
704
|
+
home: path.join(TEMP_STORE_ROOT, "clawtip-error-home"),
|
|
705
|
+
runClawtipCommand: async () => "ClawTip 返回错误:商家信息有误",
|
|
706
|
+
})).rejects.toThrow("ClawTip pay failed: ClawTip 返回错误:商家信息有误");
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("uses only ClawTip CLI media paths for QR resolution", () => {
|
|
710
|
+
expect(resolveClawtipQrMediaPath({
|
|
711
|
+
authUrl: "https://clawtip.jd.com/qrcode?clawtipId=device-789",
|
|
712
|
+
clawtipId: "device-789",
|
|
713
|
+
mediaPath: "/tmp/clawtip/qrcode-1.png",
|
|
714
|
+
requiresWalletAuth: true,
|
|
715
|
+
walletReady: false,
|
|
716
|
+
}, "/tmp/orders/order-1.json")).toBe("/tmp/clawtip/qrcode-1.png");
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
test("fails ClawTip QR resolution when pay CLI emits no QR source", () => {
|
|
720
|
+
expect(() => resolveClawtipQrMediaPath({
|
|
721
|
+
authUrl: undefined,
|
|
722
|
+
clawtipId: undefined,
|
|
723
|
+
mediaPath: undefined,
|
|
724
|
+
requiresWalletAuth: false,
|
|
725
|
+
walletReady: false,
|
|
726
|
+
}, "/tmp/orders/order-1.json")).toThrow("ClawTip pay did not return a QR media file.");
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test("checks OpenClaw before ClawTip wallet bootstrap", async () => {
|
|
730
|
+
const calls: string[][] = [];
|
|
731
|
+
const openClawVersion = await checkOpenClawRuntime({
|
|
732
|
+
runOpenClawCommand: async (args) => {
|
|
733
|
+
calls.push(["openclaw", ...args]);
|
|
734
|
+
return "OpenClaw 2026.5.18";
|
|
735
|
+
},
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
expect(openClawVersion).toBe("OpenClaw 2026.5.18");
|
|
739
|
+
expect(calls).toEqual([
|
|
740
|
+
["openclaw", "--version"],
|
|
741
|
+
]);
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("normalizes the bootstrap resource URL away from the public registry endpoint", () => {
|
|
745
|
+
expect(normalizeClawtipBootstrapResourceUrl(
|
|
746
|
+
"https://tb-wallet-bootstrap.fly.dev",
|
|
747
|
+
"https://tb-wallet-bootstrap.fly.dev/registry/sellers"
|
|
748
|
+
)).toBe("https://tb-wallet-bootstrap.fly.dev");
|
|
749
|
+
|
|
750
|
+
expect(normalizeClawtipBootstrapResourceUrl(
|
|
751
|
+
"https://tb-wallet-bootstrap.fly.dev/base",
|
|
752
|
+
"https://tb-wallet-bootstrap.fly.dev/registry/sellers"
|
|
753
|
+
)).toBe("https://tb-wallet-bootstrap.fly.dev/base");
|
|
754
|
+
|
|
755
|
+
expect(normalizeClawtipBootstrapResourceUrl(
|
|
756
|
+
"https://tb-wallet-bootstrap.fly.dev",
|
|
757
|
+
"https://example.test/pay"
|
|
758
|
+
)).toBe("https://example.test/pay");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
test("rejects a bootstrap response that still uses the placeholder ClawTip payTo", async () => {
|
|
762
|
+
const originalFetch = global.fetch;
|
|
763
|
+
global.fetch = jest.fn(async () => new Response(JSON.stringify({
|
|
764
|
+
activationFeeFen: 1,
|
|
765
|
+
payment: {
|
|
766
|
+
orderNo: "order_placeholder",
|
|
767
|
+
indicator: "indicator_placeholder",
|
|
768
|
+
payTo: "bootstrap-pay-to",
|
|
769
|
+
resourceUrl: "https://tb-wallet-bootstrap.fly.dev/registry/sellers",
|
|
770
|
+
}
|
|
771
|
+
}), {
|
|
772
|
+
status: 200,
|
|
773
|
+
headers: { "Content-Type": "application/json" }
|
|
774
|
+
})) as typeof fetch;
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
await expect(fetchClawtipBootstrap("https://tb-wallet-bootstrap.fly.dev")).rejects.toThrow(
|
|
778
|
+
"ClawTip bootstrap service is misconfigured: payTo is still the placeholder"
|
|
779
|
+
);
|
|
780
|
+
} finally {
|
|
781
|
+
global.fetch = originalFetch;
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
test("writes the Rust-compatible ClawTip order file shape", () => {
|
|
786
|
+
const home = path.join(TEMP_STORE_ROOT, "clawtip-order-home");
|
|
787
|
+
rmDir(home);
|
|
788
|
+
|
|
789
|
+
const orderFile = writeClawtipOrderFile({
|
|
790
|
+
orderNo: "order_123",
|
|
791
|
+
amountFen: 1,
|
|
792
|
+
payTo: "pay-to-test",
|
|
793
|
+
encryptedData: "ciphertext",
|
|
794
|
+
indicator: "indicator_123",
|
|
795
|
+
slug: "tb-wallet-bootstrap",
|
|
796
|
+
skillId: "si-tb-wallet-bootstrap",
|
|
797
|
+
description: "TokenBuddy ClawTip wallet activation",
|
|
798
|
+
resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
|
|
799
|
+
}, home);
|
|
800
|
+
|
|
801
|
+
expect(orderFile).toBe(path.join(
|
|
802
|
+
home,
|
|
803
|
+
".openclaw",
|
|
804
|
+
"skills",
|
|
805
|
+
"orders",
|
|
806
|
+
"indicator_123",
|
|
807
|
+
"order_123.json"
|
|
808
|
+
));
|
|
809
|
+
|
|
810
|
+
const saved = JSON.parse(fs.readFileSync(orderFile, "utf8"));
|
|
811
|
+
expect(saved).toEqual(expect.objectContaining({
|
|
812
|
+
"skill-id": "si-tb-wallet-bootstrap",
|
|
813
|
+
order_no: "order_123",
|
|
814
|
+
amount: 1,
|
|
815
|
+
encrypted_data: "ciphertext",
|
|
816
|
+
pay_to: "pay-to-test",
|
|
817
|
+
slug: "tb-wallet-bootstrap",
|
|
818
|
+
resource_url: "https://tb-wallet-bootstrap.fly.dev"
|
|
819
|
+
}));
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
test("starts ClawTip payment activation and reads payCredential from the order file", async () => {
|
|
823
|
+
const home = path.join(TEMP_STORE_ROOT, "clawtip-bootstrap-home");
|
|
824
|
+
rmDir(home);
|
|
825
|
+
|
|
826
|
+
const activation = await startClawtipWalletBootstrap({
|
|
827
|
+
orderNo: "order_456",
|
|
828
|
+
amountFen: 1,
|
|
829
|
+
payTo: "pay-to-test",
|
|
830
|
+
encryptedData: "ciphertext",
|
|
831
|
+
indicator: "indicator_456",
|
|
832
|
+
slug: "tb-wallet-bootstrap",
|
|
833
|
+
skillId: "si-tb-wallet-bootstrap",
|
|
834
|
+
description: "TokenBuddy ClawTip wallet activation",
|
|
835
|
+
resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
|
|
836
|
+
}, {
|
|
837
|
+
home,
|
|
838
|
+
runClawtipCommand: async () => {
|
|
839
|
+
const orderFile = path.join(
|
|
840
|
+
home,
|
|
841
|
+
".openclaw",
|
|
842
|
+
"skills",
|
|
843
|
+
"orders",
|
|
844
|
+
"indicator_456",
|
|
845
|
+
"order_456.json"
|
|
846
|
+
);
|
|
847
|
+
const order = JSON.parse(fs.readFileSync(orderFile, "utf8"));
|
|
848
|
+
order.payCredential = "credential_456";
|
|
849
|
+
fs.writeFileSync(orderFile, JSON.stringify(order, null, 2), "utf8");
|
|
850
|
+
return "已获取到支付凭证";
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
expect(activation.orderFile).toBe(path.join(
|
|
855
|
+
home,
|
|
856
|
+
".openclaw",
|
|
857
|
+
"skills",
|
|
858
|
+
"orders",
|
|
859
|
+
"indicator_456",
|
|
860
|
+
"order_456.json"
|
|
861
|
+
));
|
|
862
|
+
expect(activation.payCredential).toBe("credential_456");
|
|
863
|
+
expect(readClawtipPayCredential(activation.orderFile)).toBe("credential_456");
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
test("recovers the latest generated ClawTip QR media path when pay output omits MEDIA", async () => {
|
|
867
|
+
const home = path.join(TEMP_STORE_ROOT, "clawtip-bootstrap-qr-home");
|
|
868
|
+
rmDir(home);
|
|
869
|
+
|
|
870
|
+
const activation = await startClawtipWalletBootstrap({
|
|
871
|
+
orderNo: "order_789",
|
|
872
|
+
amountFen: 1,
|
|
873
|
+
payTo: "pay-to-test",
|
|
874
|
+
encryptedData: "ciphertext",
|
|
875
|
+
indicator: "indicator_789",
|
|
876
|
+
slug: "tb-wallet-bootstrap",
|
|
877
|
+
skillId: "si-tb-wallet-bootstrap",
|
|
878
|
+
description: "TokenBuddy ClawTip wallet activation",
|
|
879
|
+
resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
|
|
880
|
+
}, {
|
|
881
|
+
home,
|
|
882
|
+
runClawtipCommand: async () => {
|
|
883
|
+
const qrDir = path.join(home, ".openclaw", "workspace", "clawtip", "qrcode");
|
|
884
|
+
fs.mkdirSync(qrDir, { recursive: true });
|
|
885
|
+
const qrPath = path.join(qrDir, "qrcode-generated.png");
|
|
886
|
+
fs.writeFileSync(qrPath, "png", "utf8");
|
|
887
|
+
return [
|
|
888
|
+
"process.env.CLAWTIP_PAY https://ms.jr.jd.com/gw2/generic/hyqy/h5/m/clawtipPay",
|
|
889
|
+
"请扫码 https://idt.jd.com/unifiedAuthM/plat/safeMonitor/?authFlowUid=abc 完成授权",
|
|
890
|
+
].join("\n");
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
expect(activation.parsedOutput.authUrl).toBe(
|
|
895
|
+
"https://idt.jd.com/unifiedAuthM/plat/safeMonitor/?authFlowUid=abc"
|
|
896
|
+
);
|
|
897
|
+
expect(activation.parsedOutput.mediaPath).toBe(
|
|
898
|
+
path.join(home, ".openclaw", "workspace", "clawtip", "qrcode", "qrcode-generated.png")
|
|
899
|
+
);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("recovers ClawTip QR media even when pay output only writes payCredential", async () => {
|
|
903
|
+
const home = path.join(TEMP_STORE_ROOT, "clawtip-bootstrap-credential-and-qr-home");
|
|
904
|
+
rmDir(home);
|
|
905
|
+
|
|
906
|
+
const activation = await startClawtipWalletBootstrap({
|
|
907
|
+
orderNo: "order_credential_qr",
|
|
908
|
+
amountFen: 1,
|
|
909
|
+
payTo: "pay-to-test",
|
|
910
|
+
encryptedData: "ciphertext",
|
|
911
|
+
indicator: "indicator_credential_qr",
|
|
912
|
+
slug: "tb-wallet-bootstrap",
|
|
913
|
+
skillId: "si-tb-wallet-bootstrap",
|
|
914
|
+
description: "TokenBuddy ClawTip wallet activation",
|
|
915
|
+
resourceUrl: "https://tb-wallet-bootstrap.fly.dev"
|
|
916
|
+
}, {
|
|
917
|
+
home,
|
|
918
|
+
runClawtipCommand: async () => {
|
|
919
|
+
const orderFile = path.join(
|
|
920
|
+
home,
|
|
921
|
+
".openclaw",
|
|
922
|
+
"skills",
|
|
923
|
+
"orders",
|
|
924
|
+
"indicator_credential_qr",
|
|
925
|
+
"order_credential_qr.json"
|
|
926
|
+
);
|
|
927
|
+
const order = JSON.parse(fs.readFileSync(orderFile, "utf8"));
|
|
928
|
+
order.payCredential = "credential_without_wallet_config";
|
|
929
|
+
fs.writeFileSync(orderFile, JSON.stringify(order, null, 2), "utf8");
|
|
930
|
+
|
|
931
|
+
const qrDir = path.join(home, ".openclaw", "workspace", "clawtip", "qrcode");
|
|
932
|
+
fs.mkdirSync(qrDir, { recursive: true });
|
|
933
|
+
const qrPath = path.join(qrDir, "qrcode-credential.png");
|
|
934
|
+
fs.writeFileSync(qrPath, "png", "utf8");
|
|
935
|
+
return "已获取到支付凭证";
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
expect(activation.payCredential).toBe("credential_without_wallet_config");
|
|
940
|
+
expect(activation.parsedOutput.requiresWalletAuth).toBe(true);
|
|
941
|
+
expect(activation.parsedOutput.mediaPath).toBe(
|
|
942
|
+
path.join(home, ".openclaw", "workspace", "clawtip", "qrcode", "qrcode-credential.png")
|
|
943
|
+
);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("doctor prompts tb init when no ClawTip wallet is available", () => {
|
|
947
|
+
const lines: string[] = [];
|
|
948
|
+
|
|
949
|
+
printDoctorClawtipWallet({
|
|
950
|
+
status: "missing",
|
|
951
|
+
ready: false,
|
|
952
|
+
paymentMetadataPresent: false,
|
|
953
|
+
walletConfigPresent: false,
|
|
954
|
+
configsDirExists: false,
|
|
955
|
+
expectedPath: "/tmp/home/.openclaw/configs/config.json",
|
|
956
|
+
alternatePaths: [],
|
|
957
|
+
message: "ClawTip payment metadata and local OpenClaw wallet config are not configured.",
|
|
958
|
+
}, (line) => {
|
|
959
|
+
lines.push(line);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
expect(lines.join("\n")).toContain(
|
|
963
|
+
"Action: Run `tb init` and choose ClawTip to bind a wallet before using ClawTip-backed purchases."
|
|
964
|
+
);
|
|
965
|
+
});
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
describe("TokenBuddy terminal image display", () => {
|
|
969
|
+
const imagePath = "/Users/test/.openclaw/workspace/clawtip/qrcode/clawtip-index-code.png";
|
|
970
|
+
|
|
971
|
+
test("detects inline image capable terminals only when stdout is a TTY", () => {
|
|
972
|
+
expect(detectTerminalImageDisplay({
|
|
973
|
+
env: { TERM_PROGRAM: "iTerm.app" },
|
|
974
|
+
stdoutIsTTY: true,
|
|
975
|
+
})).toBe("iterm");
|
|
976
|
+
expect(detectTerminalImageDisplay({
|
|
977
|
+
env: { WEZTERM_EXECUTABLE: "/Applications/WezTerm.app/wezterm" },
|
|
978
|
+
stdoutIsTTY: true,
|
|
979
|
+
})).toBe("iterm");
|
|
980
|
+
expect(detectTerminalImageDisplay({
|
|
981
|
+
env: { KITTY_WINDOW_ID: "1" },
|
|
982
|
+
stdoutIsTTY: true,
|
|
983
|
+
})).toBe("kitty");
|
|
984
|
+
expect(detectTerminalImageDisplay({
|
|
985
|
+
env: { TERM_PROGRAM: "iTerm.app" },
|
|
986
|
+
stdoutIsTTY: false,
|
|
987
|
+
})).toBeUndefined();
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test("renders the ClawTip QR image inline for iTerm2-compatible terminals", async () => {
|
|
991
|
+
const writes: string[] = [];
|
|
992
|
+
const runCommand = jest.fn(async () => undefined);
|
|
993
|
+
|
|
994
|
+
const result = await displayTerminalImage(imagePath, {
|
|
995
|
+
env: { TERM_PROGRAM: "iTerm.app" },
|
|
996
|
+
stdoutIsTTY: true,
|
|
997
|
+
fileExists: () => true,
|
|
998
|
+
readFile: () => Buffer.from("png-bytes"),
|
|
999
|
+
write: (chunk) => {
|
|
1000
|
+
writes.push(chunk);
|
|
1001
|
+
},
|
|
1002
|
+
runCommand,
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
expect(result).toEqual(expect.objectContaining({
|
|
1006
|
+
method: "inline-iterm",
|
|
1007
|
+
displayed: true,
|
|
1008
|
+
}));
|
|
1009
|
+
expect(writes.join("")).toContain("\u001B]1337;File=");
|
|
1010
|
+
expect(writes.join("")).toContain("inline=1");
|
|
1011
|
+
expect(runCommand).not.toHaveBeenCalled();
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
test("renders the ClawTip QR image inline for Kitty terminals", async () => {
|
|
1015
|
+
const writes: string[] = [];
|
|
1016
|
+
|
|
1017
|
+
const result = await displayTerminalImage(imagePath, {
|
|
1018
|
+
env: { TERM: "xterm-kitty" },
|
|
1019
|
+
stdoutIsTTY: true,
|
|
1020
|
+
fileExists: () => true,
|
|
1021
|
+
write: (chunk) => {
|
|
1022
|
+
writes.push(chunk);
|
|
1023
|
+
},
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
expect(result).toEqual(expect.objectContaining({
|
|
1027
|
+
method: "inline-kitty",
|
|
1028
|
+
displayed: true,
|
|
1029
|
+
}));
|
|
1030
|
+
expect(writes.join("")).toContain("\u001B_Ga=T,t=f,f=100,c=60;");
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
test("opens the ClawTip QR image with the system viewer when inline images are unsupported", async () => {
|
|
1034
|
+
const runCommand = jest.fn(async () => undefined);
|
|
1035
|
+
|
|
1036
|
+
const result = await displayTerminalImage(imagePath, {
|
|
1037
|
+
env: { TERM_PROGRAM: "Apple_Terminal" },
|
|
1038
|
+
platform: "darwin",
|
|
1039
|
+
stdoutIsTTY: true,
|
|
1040
|
+
fileExists: () => true,
|
|
1041
|
+
runCommand,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
expect(result).toEqual(expect.objectContaining({
|
|
1045
|
+
method: "system-open",
|
|
1046
|
+
displayed: true,
|
|
1047
|
+
fallbackCommand: `open ${imagePath}`,
|
|
1048
|
+
}));
|
|
1049
|
+
expect(runCommand).toHaveBeenCalledWith("open", [imagePath]);
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
test("falls back to a manual open command when the system viewer fails", async () => {
|
|
1053
|
+
const result = await displayTerminalImage(imagePath, {
|
|
1054
|
+
env: {},
|
|
1055
|
+
platform: "darwin",
|
|
1056
|
+
stdoutIsTTY: true,
|
|
1057
|
+
fileExists: () => true,
|
|
1058
|
+
runCommand: async () => {
|
|
1059
|
+
throw new Error("no gui session");
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
expect(result).toEqual(expect.objectContaining({
|
|
1064
|
+
method: "manual",
|
|
1065
|
+
displayed: false,
|
|
1066
|
+
fallbackCommand: `open ${imagePath}`,
|
|
1067
|
+
}));
|
|
1068
|
+
expect(result.message).toContain("no gui session");
|
|
1069
|
+
});
|
|
532
1070
|
});
|
|
533
1071
|
|
|
534
1072
|
describe("TokenBuddy JSON inspection commands", () => {
|
|
@@ -538,10 +1076,31 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
538
1076
|
let proxyPort: number;
|
|
539
1077
|
let previousControlPort: string | undefined;
|
|
540
1078
|
let previousProxyPort: string | undefined;
|
|
1079
|
+
let previousBuyerStoreRoot: string | undefined;
|
|
1080
|
+
let previousHome: string | undefined;
|
|
541
1081
|
|
|
542
1082
|
beforeEach((done) => {
|
|
543
1083
|
previousControlPort = process.env.TB_PROXYD_CONTROL_PORT;
|
|
544
1084
|
previousProxyPort = process.env.TB_PROXYD_PROXY_PORT;
|
|
1085
|
+
previousBuyerStoreRoot = process.env.TOKENBUDDY_BUYER_STORE;
|
|
1086
|
+
previousHome = process.env.HOME;
|
|
1087
|
+
rmDir(INSPECTION_STORE_ROOT);
|
|
1088
|
+
rmDir(INSPECTION_HOME);
|
|
1089
|
+
process.env.TOKENBUDDY_BUYER_STORE = INSPECTION_STORE_ROOT;
|
|
1090
|
+
process.env.HOME = INSPECTION_HOME;
|
|
1091
|
+
fs.mkdirSync(path.join(INSPECTION_HOME, ".openclaw", "configs"), { recursive: true });
|
|
1092
|
+
fs.writeFileSync(path.join(INSPECTION_HOME, ".openclaw", "configs", "config.json.bak"), "{}", "utf8");
|
|
1093
|
+
const store = new BuyerStore();
|
|
1094
|
+
store.savePayment({
|
|
1095
|
+
method: "clawtip",
|
|
1096
|
+
enabled: true,
|
|
1097
|
+
isDefault: true,
|
|
1098
|
+
config: {
|
|
1099
|
+
orderNo: "order_json",
|
|
1100
|
+
resourceUrl: "https://example.test/pay"
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
store.close();
|
|
545
1104
|
proxyServer = http.createServer((req, res) => {
|
|
546
1105
|
res.setHeader("Content-Type", "application/json");
|
|
547
1106
|
if (req.url === "/v1/models") {
|
|
@@ -650,8 +1209,22 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
650
1209
|
} else {
|
|
651
1210
|
process.env.TB_PROXYD_PROXY_PORT = previousProxyPort;
|
|
652
1211
|
}
|
|
1212
|
+
if (previousBuyerStoreRoot === undefined) {
|
|
1213
|
+
delete process.env.TOKENBUDDY_BUYER_STORE;
|
|
1214
|
+
} else {
|
|
1215
|
+
process.env.TOKENBUDDY_BUYER_STORE = previousBuyerStoreRoot;
|
|
1216
|
+
}
|
|
1217
|
+
if (previousHome === undefined) {
|
|
1218
|
+
delete process.env.HOME;
|
|
1219
|
+
} else {
|
|
1220
|
+
process.env.HOME = previousHome;
|
|
1221
|
+
}
|
|
653
1222
|
jest.restoreAllMocks();
|
|
654
|
-
controlServer.close(() => proxyServer.close(
|
|
1223
|
+
controlServer.close(() => proxyServer.close(() => {
|
|
1224
|
+
rmDir(INSPECTION_STORE_ROOT);
|
|
1225
|
+
rmDir(INSPECTION_HOME);
|
|
1226
|
+
done();
|
|
1227
|
+
}));
|
|
655
1228
|
});
|
|
656
1229
|
|
|
657
1230
|
test("doctor --json reports daemon and provider state", async () => {
|
|
@@ -694,6 +1267,16 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
694
1267
|
count: 1,
|
|
695
1268
|
registryUrl: "https://example.test/registry/sellers",
|
|
696
1269
|
});
|
|
1270
|
+
expect(parsed.clawtipWallet).toMatchObject({
|
|
1271
|
+
status: "metadata_missing_wallet",
|
|
1272
|
+
ready: false,
|
|
1273
|
+
paymentMetadataPresent: true,
|
|
1274
|
+
walletConfigPresent: false,
|
|
1275
|
+
expectedPath: path.join(INSPECTION_HOME, ".openclaw", "configs", "config.json"),
|
|
1276
|
+
alternatePaths: [
|
|
1277
|
+
path.join(INSPECTION_HOME, ".openclaw", "configs", "config.json.bak")
|
|
1278
|
+
]
|
|
1279
|
+
});
|
|
697
1280
|
expect(parsed.providers).toEqual(expect.arrayContaining([
|
|
698
1281
|
expect.objectContaining({ id: "codex" }),
|
|
699
1282
|
expect.objectContaining({ id: "claude-code" })
|
|
@@ -709,6 +1292,10 @@ describe("TokenBuddy JSON inspection commands", () => {
|
|
|
709
1292
|
await buildCli().parseAsync(["node", "tb", "doctor"]);
|
|
710
1293
|
|
|
711
1294
|
const joined = output.join("\n");
|
|
1295
|
+
expect(joined).toContain("--- ClawTip Wallet ---");
|
|
1296
|
+
expect(joined).toContain("❌ ClawTip Wallet [metadata_missing_wallet]");
|
|
1297
|
+
expect(joined).toContain("Payment metadata: present");
|
|
1298
|
+
expect(joined).toContain("Wallet config: missing");
|
|
712
1299
|
expect(joined).toContain("Checking local control plane and proxy endpoints...");
|
|
713
1300
|
expect(joined).toContain("Refreshing seller registry...");
|
|
714
1301
|
expect(joined).toContain("Model catalog hidden in `tb doctor`. Run `tb models` for the current model summary.");
|
|
@@ -1003,9 +1590,11 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1003
1590
|
let mockSellerServer: http.Server;
|
|
1004
1591
|
let sellerReqCount = 0;
|
|
1005
1592
|
let completeReqCount = 0;
|
|
1593
|
+
let balanceReqCount = 0;
|
|
1006
1594
|
let mockSellerPort: number;
|
|
1007
1595
|
let daemonControlPort: number;
|
|
1008
1596
|
let daemonProxyPort: number;
|
|
1597
|
+
const insufficientFundsAttempts = new Map<string, number>();
|
|
1009
1598
|
const sellerRequests: Array<{
|
|
1010
1599
|
url?: string;
|
|
1011
1600
|
authorization?: string;
|
|
@@ -1024,6 +1613,25 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1024
1613
|
});
|
|
1025
1614
|
});
|
|
1026
1615
|
|
|
1616
|
+
function setSettlementHeader(res: http.ServerResponse, requestId: string, settledMicros: number, remainingCreditMicros: number): void {
|
|
1617
|
+
res.setHeader("X-TokenBuddy-Settlement", JSON.stringify({
|
|
1618
|
+
requestId,
|
|
1619
|
+
request_id: requestId,
|
|
1620
|
+
settledMicros,
|
|
1621
|
+
settled_micros: settledMicros,
|
|
1622
|
+
settledUsdMicros: settledMicros,
|
|
1623
|
+
settled_usd_micros: settledMicros,
|
|
1624
|
+
remainingCreditMicros,
|
|
1625
|
+
remaining_credit_micros: remainingCreditMicros,
|
|
1626
|
+
reservedBalanceMicros: 0,
|
|
1627
|
+
reserved_balance_micros: 0,
|
|
1628
|
+
spentMicros: settledMicros,
|
|
1629
|
+
spent_micros: settledMicros,
|
|
1630
|
+
priceVersion: "openrouter_usd.v1",
|
|
1631
|
+
price_version: "openrouter_usd.v1"
|
|
1632
|
+
}));
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1027
1635
|
beforeAll((done) => {
|
|
1028
1636
|
mockSellerServer = http.createServer(async (req, res) => {
|
|
1029
1637
|
res.setHeader("Content-Type", "application/json");
|
|
@@ -1107,6 +1715,22 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1107
1715
|
return;
|
|
1108
1716
|
}
|
|
1109
1717
|
|
|
1718
|
+
if (req.url === "/v1/balance") {
|
|
1719
|
+
balanceReqCount++;
|
|
1720
|
+
sellerRequests.push({
|
|
1721
|
+
url: req.url,
|
|
1722
|
+
authorization: req.headers.authorization
|
|
1723
|
+
});
|
|
1724
|
+
res.end(JSON.stringify({
|
|
1725
|
+
tokenId: "cred_mock",
|
|
1726
|
+
creditMicros: 1000,
|
|
1727
|
+
reservedMicros: 0,
|
|
1728
|
+
spentMicros: 1999000,
|
|
1729
|
+
currency: "Micros"
|
|
1730
|
+
}));
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1110
1734
|
if (req.url === "/v1/chat/completions") {
|
|
1111
1735
|
const body = await readJsonBody(req);
|
|
1112
1736
|
sellerRequests.push({
|
|
@@ -1115,12 +1739,37 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1115
1739
|
idempotencyKey: req.headers["idempotency-key"] as string | undefined,
|
|
1116
1740
|
body
|
|
1117
1741
|
});
|
|
1742
|
+
if (body.requestId === "chat_req_402_retry") {
|
|
1743
|
+
const attempts = insufficientFundsAttempts.get(body.requestId) || 0;
|
|
1744
|
+
insufficientFundsAttempts.set(body.requestId, attempts + 1);
|
|
1745
|
+
if (attempts === 0) {
|
|
1746
|
+
res.statusCode = 402;
|
|
1747
|
+
res.end(JSON.stringify({
|
|
1748
|
+
error: {
|
|
1749
|
+
code: "insufficient_funds",
|
|
1750
|
+
message: "Insufficient funds"
|
|
1751
|
+
}
|
|
1752
|
+
}));
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1118
1756
|
if (body.stream) {
|
|
1119
1757
|
res.writeHead(200, { "Content-Type": "text/event-stream" });
|
|
1120
1758
|
res.write("data: {\"id\":\"chatcmpl-stream\",\"choices\":[{\"delta\":{\"content\":\"hello\"}}]}\n\n");
|
|
1759
|
+
res.write("event: tokenbuddy.settlement\n");
|
|
1760
|
+
res.write(`data: ${JSON.stringify({
|
|
1761
|
+
requestId: body.requestId || "stream_req_mock",
|
|
1762
|
+
settledMicros: 110,
|
|
1763
|
+
settledUsdMicros: 110,
|
|
1764
|
+
remainingCreditMicros: 1999890,
|
|
1765
|
+
reservedBalanceMicros: 0,
|
|
1766
|
+
spentMicros: 110,
|
|
1767
|
+
priceVersion: "openrouter_usd.v1"
|
|
1768
|
+
})}\n\n`);
|
|
1121
1769
|
res.end("data: [DONE]\n\n");
|
|
1122
1770
|
return;
|
|
1123
1771
|
}
|
|
1772
|
+
setSettlementHeader(res, body.requestId || "chat_req_mock", 110, 1999890);
|
|
1124
1773
|
res.end(JSON.stringify({
|
|
1125
1774
|
id: "chatcmpl-mock",
|
|
1126
1775
|
usage: { prompt_tokens: 10, completion_tokens: 10 }
|
|
@@ -1159,6 +1808,7 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1159
1808
|
res.end(compressed);
|
|
1160
1809
|
return;
|
|
1161
1810
|
}
|
|
1811
|
+
setSettlementHeader(res, body.requestId || "responses_req_mock", 64, 1999936);
|
|
1162
1812
|
res.end(JSON.stringify({
|
|
1163
1813
|
id: "resp-mock",
|
|
1164
1814
|
usage: { input_tokens: 7, output_tokens: 9 }
|
|
@@ -1174,6 +1824,7 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1174
1824
|
idempotencyKey: req.headers["idempotency-key"] as string | undefined,
|
|
1175
1825
|
body
|
|
1176
1826
|
});
|
|
1827
|
+
setSettlementHeader(res, body.requestId || "messages_req_mock", 44, 1999956);
|
|
1177
1828
|
res.end(JSON.stringify({
|
|
1178
1829
|
id: "msg-mock",
|
|
1179
1830
|
usage: { input_tokens: 5, output_tokens: 6 }
|
|
@@ -1198,6 +1849,8 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1198
1849
|
rmSqliteFiles(TEMP_BUYER_DB);
|
|
1199
1850
|
sellerReqCount = 0;
|
|
1200
1851
|
completeReqCount = 0;
|
|
1852
|
+
balanceReqCount = 0;
|
|
1853
|
+
insufficientFundsAttempts.clear();
|
|
1201
1854
|
sellerRequests.length = 0;
|
|
1202
1855
|
const seedStore = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
1203
1856
|
seedStore.savePayment({
|
|
@@ -1385,11 +2038,75 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1385
2038
|
const inferences = await (await fetch(`${controlUrl}/ledger/inferences`)).json() as any;
|
|
1386
2039
|
expect(purchases.purchases.some((entry: any) => entry.purchaseId === "pur_mock_123" && entry.paymentMethod === "mock")).toBe(true);
|
|
1387
2040
|
expect(inferences.inferences.filter((entry: any) => entry.endpoint === "/v1/chat/completions")).toHaveLength(11);
|
|
2041
|
+
const chatLedgers = inferences.inferences.filter((entry: any) => entry.requestId === "chat_req_parallel");
|
|
2042
|
+
expect(chatLedgers).toHaveLength(10);
|
|
2043
|
+
expect(chatLedgers).toEqual(expect.arrayContaining([
|
|
2044
|
+
expect.objectContaining({
|
|
2045
|
+
billedMicros: 110,
|
|
2046
|
+
estimatedMicros: 80,
|
|
2047
|
+
settledMicros: 110,
|
|
2048
|
+
settledUsdMicros: 110,
|
|
2049
|
+
priceVersion: "openrouter_usd.v1",
|
|
2050
|
+
balanceSnapshotMicros: 1999890,
|
|
2051
|
+
balanceSource: "seller_authoritative"
|
|
2052
|
+
})
|
|
2053
|
+
]));
|
|
2054
|
+
expect(balanceReqCount).toBe(0);
|
|
2055
|
+
const store = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
2056
|
+
try {
|
|
2057
|
+
expect(store.getToken("mock-seller")).toMatchObject({
|
|
2058
|
+
balanceMicros: 1999890,
|
|
2059
|
+
reservedMicros: 0,
|
|
2060
|
+
spentMicros: 110,
|
|
2061
|
+
balanceSource: "seller_settlement_summary"
|
|
2062
|
+
});
|
|
2063
|
+
} finally {
|
|
2064
|
+
store.close();
|
|
2065
|
+
}
|
|
1388
2066
|
const publicOutput = JSON.stringify({ purchases, inferences });
|
|
1389
2067
|
expect(publicOutput).not.toContain("raw concurrent prompt secret");
|
|
1390
2068
|
expect(publicOutput).not.toContain("tok_mock_token_abc");
|
|
1391
2069
|
});
|
|
1392
2070
|
|
|
2071
|
+
test("refreshes balance, auto-purchases, and retries once after seller 402 insufficient funds", async () => {
|
|
2072
|
+
const store = new BuyerStore({ dbPath: TEMP_BUYER_DB });
|
|
2073
|
+
store.saveToken("mock-seller", "tok_existing_high_cache", "model:gpt-4", 900000, "2030-01-01T00:00:00.000Z");
|
|
2074
|
+
store.close();
|
|
2075
|
+
|
|
2076
|
+
const response = await fetch(`http://127.0.0.1:${daemonProxyPort}/v1/chat/completions`, {
|
|
2077
|
+
method: "POST",
|
|
2078
|
+
headers: {
|
|
2079
|
+
"Content-Type": "application/json",
|
|
2080
|
+
"Idempotency-Key": "idem-402-retry"
|
|
2081
|
+
},
|
|
2082
|
+
body: JSON.stringify({
|
|
2083
|
+
model: "gpt-4",
|
|
2084
|
+
messages: [{ role: "user", content: "trigger 402" }],
|
|
2085
|
+
requestId: "chat_req_402_retry"
|
|
2086
|
+
})
|
|
2087
|
+
});
|
|
2088
|
+
|
|
2089
|
+
expect(response.ok).toBe(true);
|
|
2090
|
+
expect((await response.json() as any).id).toBe("chatcmpl-mock");
|
|
2091
|
+
expect(balanceReqCount).toBe(1);
|
|
2092
|
+
expect(sellerRequests.filter((request) => request.url === "/v1/chat/completions" && request.body?.requestId === "chat_req_402_retry")).toHaveLength(2);
|
|
2093
|
+
expect(sellerRequests.filter((request) => request.url === "/purchase/create")).toHaveLength(1);
|
|
2094
|
+
expect(sellerRequests.filter((request) => request.url === "/purchase/complete")).toHaveLength(1);
|
|
2095
|
+
expect(sellerRequests.filter((request) => request.url === "/v1/chat/completions" && request.body?.requestId === "chat_req_402_retry")
|
|
2096
|
+
.every((request) => request.idempotencyKey === "idem-402-retry")).toBe(true);
|
|
2097
|
+
|
|
2098
|
+
const inferences = await (await fetch(`http://127.0.0.1:${daemonControlPort}/ledger/inferences`)).json() as any;
|
|
2099
|
+
expect(inferences.inferences).toEqual(expect.arrayContaining([
|
|
2100
|
+
expect.objectContaining({
|
|
2101
|
+
requestId: "chat_req_402_retry",
|
|
2102
|
+
billedMicros: 110,
|
|
2103
|
+
estimatedMicros: 80,
|
|
2104
|
+
settledMicros: 110,
|
|
2105
|
+
balanceSource: "seller_authoritative"
|
|
2106
|
+
})
|
|
2107
|
+
]));
|
|
2108
|
+
});
|
|
2109
|
+
|
|
1393
2110
|
test("proxies models, responses, and anthropic message endpoints through compatible seller manifests", async () => {
|
|
1394
2111
|
const proxyUrl = `http://127.0.0.1:${daemonProxyPort}`;
|
|
1395
2112
|
|
|
@@ -1520,11 +2237,20 @@ describe("TokenBuddy CLI and Daemon Integration Tests", () => {
|
|
|
1520
2237
|
const body = await response.text();
|
|
1521
2238
|
expect(body).toContain("chatcmpl-stream");
|
|
1522
2239
|
expect(body).toContain("[DONE]");
|
|
2240
|
+
expect(body).not.toContain("tokenbuddy.settlement");
|
|
1523
2241
|
expect(sellerRequests.find((request) => request.url === "/v1/chat/completions" && request.body?.stream)?.idempotencyKey).toBe("idem-stream-preserved");
|
|
1524
2242
|
|
|
1525
2243
|
const inferences = await (await fetch(`http://127.0.0.1:${daemonControlPort}/ledger/inferences`)).json() as any;
|
|
1526
2244
|
expect(inferences.inferences).toEqual(expect.arrayContaining([
|
|
1527
|
-
expect.objectContaining({
|
|
2245
|
+
expect.objectContaining({
|
|
2246
|
+
endpoint: "/v1/chat/completions",
|
|
2247
|
+
requestId: "stream_req_1",
|
|
2248
|
+
status: "settled",
|
|
2249
|
+
billedMicros: 110,
|
|
2250
|
+
settledMicros: 110,
|
|
2251
|
+
settledUsdMicros: 110,
|
|
2252
|
+
balanceSource: "seller_authoritative"
|
|
2253
|
+
})
|
|
1528
2254
|
]));
|
|
1529
2255
|
const publicOutput = JSON.stringify(inferences);
|
|
1530
2256
|
expect(publicOutput).not.toContain("raw stream prompt secret");
|