@tokenbuddy/tb-admin 1.0.31 → 1.0.32
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/cli.d.ts.map +1 -1
- package/dist/src/cli.js +280 -19
- package/dist/src/cli.js.map +1 -1
- package/dist/src/client.d.ts +82 -2
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +93 -0
- package/dist/src/client.js.map +1 -1
- package/dist/src/provider.d.ts +120 -0
- package/dist/src/provider.d.ts.map +1 -0
- package/dist/src/provider.js +73 -0
- package/dist/src/provider.js.map +1 -0
- package/dist/src/seller.d.ts +104 -0
- package/dist/src/seller.d.ts.map +1 -0
- package/dist/src/seller.js +283 -0
- package/dist/src/seller.js.map +1 -0
- package/dist/src/ui-actions.d.ts +25 -0
- package/dist/src/ui-actions.d.ts.map +1 -1
- package/dist/src/ui-actions.js +81 -11
- package/dist/src/ui-actions.js.map +1 -1
- package/dist/src/ui-server.js +9 -0
- package/dist/src/ui-server.js.map +1 -1
- package/dist/src/ui-state.d.ts +77 -2
- package/dist/src/ui-state.d.ts.map +1 -1
- package/dist/src/ui-state.js +242 -14
- package/dist/src/ui-state.js.map +1 -1
- package/dist/src/ui-static.d.ts.map +1 -1
- package/dist/src/ui-static.js +95 -17
- package/dist/src/ui-static.js.map +1 -1
- package/dist/src/vendor-client.d.ts +23 -0
- package/dist/src/vendor-client.d.ts.map +1 -0
- package/dist/src/vendor-client.js +2 -0
- package/dist/src/vendor-client.js.map +1 -0
- package/dist/src/vendor-commands.d.ts +35 -0
- package/dist/src/vendor-commands.d.ts.map +1 -0
- package/dist/src/vendor-commands.js +33 -0
- package/dist/src/vendor-commands.js.map +1 -0
- package/package.json +1 -1
- package/src/cli.ts +305 -31
- package/src/client.ts +119 -2
- package/src/provider.ts +150 -0
- package/src/seller.ts +362 -0
- package/src/ui-actions.ts +89 -11
- package/src/ui-server.ts +9 -0
- package/src/ui-state.ts +293 -15
- package/src/ui-static.ts +95 -17
- package/src/vendor-client.ts +23 -0
- package/src/vendor-commands.ts +65 -0
- package/tests/admin.test.ts +20 -1
- package/tests/seller.test.ts +307 -0
- package/tests/ui-state-fleet.test.ts +257 -0
- package/tests/ui-static-row.test.ts +202 -0
- package/tests/vendor-cli.test.ts +197 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step 13 v1.1: admin web UI seller row 渲染测试.
|
|
3
|
+
*
|
|
4
|
+
* 原则: 不引入 jsdom / linkedom. 直接 evaluate ui-static.ts bundle 的
|
|
5
|
+
* inline script 抽出 sellerRow 函数 (regex 匹配 function 声明) + 喂 mock
|
|
6
|
+
* window.__tbFmt, 然后 string match 验证 4 类行 (dataSource=both / fly /
|
|
7
|
+
* registry / 老 1.0.31 缺字段) 渲染出正确的 CSS class + 文案 + 按钮.
|
|
8
|
+
*
|
|
9
|
+
* 覆盖 (跟 docs/processes/seller-fleet-data-sources.md 一致):
|
|
10
|
+
* 1. dataSource="both" → "app-row" (无红/灰 class), 绿点 active, "Both" chip
|
|
11
|
+
* 2. dataSource="fly" → "app-row app-row-fly-only" (灰), "未发布" chip
|
|
12
|
+
* + publish-hint-btn 文案
|
|
13
|
+
* 3. dataSource="registry" → "app-row app-row-registry-only" (整行红边)
|
|
14
|
+
* + "严重事故" + alert-reason + remove-hint-btn
|
|
15
|
+
* 4. dataSource 老 1.0.31 缺失 → 兜底 "both" (默认行, 不报红)
|
|
16
|
+
*
|
|
17
|
+
* 验证 grep 锚点 (跟 spec 一致):
|
|
18
|
+
* - "立即下线 (registry-only)" 必须在 remove-hint-btn 出现
|
|
19
|
+
* - "registry 收录了但 fly app 失踪" 必须在 alert-reason 出现
|
|
20
|
+
* - "未发布 — 可申请发布" 必须在 publish-hint-btn 出现
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { adminUiHtml } from "../src/ui-static.js";
|
|
24
|
+
|
|
25
|
+
interface MockFmt {
|
|
26
|
+
UNKNOWN_VALUE: string;
|
|
27
|
+
formatDuration: (v: unknown) => string;
|
|
28
|
+
formatSpeed: (v: unknown) => string;
|
|
29
|
+
formatBalanceAmount: () => string;
|
|
30
|
+
formatSellerCapacity: () => string;
|
|
31
|
+
formatDiscountRatio: (v: unknown) => string;
|
|
32
|
+
formatSellerStatus: (s: unknown) => string;
|
|
33
|
+
sellerStatusTone: (s: unknown) => string;
|
|
34
|
+
normalizeStatusLabel: (s: unknown) => string;
|
|
35
|
+
formatTimeCompact: (s: unknown) => string;
|
|
36
|
+
formatCount: (n: unknown) => string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MOCK_FMT: MockFmt = {
|
|
40
|
+
UNKNOWN_VALUE: "—",
|
|
41
|
+
formatDuration: () => "123ms",
|
|
42
|
+
formatSpeed: () => "12.3 tok/s",
|
|
43
|
+
formatBalanceAmount: () => "0.00",
|
|
44
|
+
formatSellerCapacity: () => "3/8",
|
|
45
|
+
formatDiscountRatio: () => "1.0x",
|
|
46
|
+
formatSellerStatus: (s) => String(s || "unknown"),
|
|
47
|
+
sellerStatusTone: (s) => {
|
|
48
|
+
const k = String(s || "unknown").toLowerCase();
|
|
49
|
+
if (k === "active" || k === "healthy") return "green";
|
|
50
|
+
if (k === "draining" || k === "degraded") return "amber";
|
|
51
|
+
if (k === "offline" || k === "unhealthy") return "red";
|
|
52
|
+
if (k === "busy_capacity") return "blue";
|
|
53
|
+
return "gray";
|
|
54
|
+
},
|
|
55
|
+
normalizeStatusLabel: (s) => String(s || "—"),
|
|
56
|
+
formatTimeCompact: () => "12:34",
|
|
57
|
+
formatCount: (n) => String(n ?? 0)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Step 13 v1.1: 不引 jsdom, 用 `new Function` 跑抽出 sellerRow 函数体.
|
|
62
|
+
* ui-static.ts 整个文件是 `<script>...</script>` 模板字符串, 我们用
|
|
63
|
+
* 简单 regex 提取 inline script 内容, 加上 mock window, 跑 sellerRow 拿 HTML.
|
|
64
|
+
*
|
|
65
|
+
* 限制: 不模拟 DOM 树. 验证方式是直接看 sellerRow() 返回的 HTML 字符串
|
|
66
|
+
* 是否含预期的 class + 文案 + 按钮 (跟 spec grep 锚点一一对应).
|
|
67
|
+
*/
|
|
68
|
+
function renderRow(row: Record<string, unknown>): string {
|
|
69
|
+
const html = adminUiHtml();
|
|
70
|
+
// 抽 <script>...</script>
|
|
71
|
+
const scriptMatch = html.match(/<script>([\s\S]*?)<\/script>/);
|
|
72
|
+
if (!scriptMatch) {
|
|
73
|
+
throw new Error("adminUiHtml() did not include a <script> tag");
|
|
74
|
+
}
|
|
75
|
+
const scriptBody = scriptMatch[1];
|
|
76
|
+
// mock window + 把 sellerRow 暴露
|
|
77
|
+
const code = `
|
|
78
|
+
var noopArr = () => [];
|
|
79
|
+
var fakeEl = { classList: { toggle: () => undefined, add: () => undefined, remove: () => undefined }, innerHTML: "", textContent: "" };
|
|
80
|
+
var window = { __tbFmt: ${JSON.stringify(MOCK_FMT)}, addEventListener: () => undefined };
|
|
81
|
+
var document = {
|
|
82
|
+
createElement: () => fakeEl,
|
|
83
|
+
addEventListener: () => undefined,
|
|
84
|
+
querySelectorAll: noopArr,
|
|
85
|
+
querySelector: () => fakeEl,
|
|
86
|
+
getElementById: () => fakeEl
|
|
87
|
+
};
|
|
88
|
+
var setTimeout = () => 0;
|
|
89
|
+
var setInterval = () => 0;
|
|
90
|
+
var clearTimeout = () => undefined;
|
|
91
|
+
var clearInterval = () => undefined;
|
|
92
|
+
${scriptBody}
|
|
93
|
+
return sellerRow(${JSON.stringify(row)});
|
|
94
|
+
`;
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
|
|
96
|
+
const fn = new Function(code);
|
|
97
|
+
return fn();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe("admin web seller row 渲染 (Step 13 v1.1 双源 spec)", () => {
|
|
101
|
+
test("dataSource=both → 正常行 (无红/灰 class), 绿点, Both chip", () => {
|
|
102
|
+
const out = renderRow({
|
|
103
|
+
id: "tbs-alpha",
|
|
104
|
+
name: "tbs-alpha",
|
|
105
|
+
app: "tbs-alpha",
|
|
106
|
+
url: "https://tbs-alpha.fly.dev",
|
|
107
|
+
registryStatus: "active",
|
|
108
|
+
nodeStatus: "active",
|
|
109
|
+
upstreamDomain: "openrouter.ai",
|
|
110
|
+
upstreamStatus: "healthy",
|
|
111
|
+
modelsCount: 1,
|
|
112
|
+
dataSource: "both",
|
|
113
|
+
flyApp: { name: "tbs-alpha", status: "running" }
|
|
114
|
+
});
|
|
115
|
+
// 行 class 不带 registry-only / fly-only
|
|
116
|
+
expect(out).toContain('class="app-row"');
|
|
117
|
+
expect(out).not.toContain("app-row-registry-only");
|
|
118
|
+
expect(out).not.toContain("app-row-fly-only");
|
|
119
|
+
// 绿点 active → tone-green
|
|
120
|
+
expect(out).toMatch(/app-dot tone-green/);
|
|
121
|
+
// Both chip
|
|
122
|
+
expect(out).toMatch(/datasource-chip both/);
|
|
123
|
+
expect(out).toContain("Both");
|
|
124
|
+
// 无 alert-reason / remove-hint-btn / publish-hint-btn
|
|
125
|
+
expect(out).not.toContain("alert-reason");
|
|
126
|
+
expect(out).not.toContain("remove-hint-btn");
|
|
127
|
+
expect(out).not.toContain("publish-hint-btn");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("dataSource=fly → 灰底 + 未发布 chip + publish-hint-btn", () => {
|
|
131
|
+
const out = renderRow({
|
|
132
|
+
id: "tbs-flyonly",
|
|
133
|
+
name: "tbs-flyonly",
|
|
134
|
+
app: "tbs-flyonly",
|
|
135
|
+
url: "https://tbs-flyonly.fly.dev",
|
|
136
|
+
registryStatus: "unknown",
|
|
137
|
+
nodeStatus: "unknown",
|
|
138
|
+
upstreamDomain: "tbs-flyonly.fly.dev",
|
|
139
|
+
upstreamStatus: "unknown",
|
|
140
|
+
modelsCount: 0,
|
|
141
|
+
dataSource: "fly",
|
|
142
|
+
publishHint: "未发布 — 可申请发布 (走 vendor-bootstrap stage)",
|
|
143
|
+
flyApp: { name: "tbs-flyonly", status: "running" }
|
|
144
|
+
});
|
|
145
|
+
expect(out).toContain("app-row-fly-only");
|
|
146
|
+
expect(out).not.toContain("app-row-registry-only");
|
|
147
|
+
expect(out).toMatch(/datasource-chip fly/);
|
|
148
|
+
expect(out).toContain("未发布");
|
|
149
|
+
// publish-hint-btn 出现 + 文案含 spec 关键词
|
|
150
|
+
expect(out).toContain("publish-hint-btn");
|
|
151
|
+
expect(out).toContain("未发布 — 可申请发布");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("dataSource=registry → 整行红边 + 严重事故 + alert-reason + 立即下线 (registry-only) 按钮", () => {
|
|
155
|
+
const out = renderRow({
|
|
156
|
+
id: "tbs-ghost",
|
|
157
|
+
name: "tbs-ghost",
|
|
158
|
+
app: "tbs-ghost",
|
|
159
|
+
url: "https://tbs-ghost.fly.dev",
|
|
160
|
+
registryStatus: "active",
|
|
161
|
+
nodeStatus: "unknown",
|
|
162
|
+
upstreamDomain: "tbs-ghost.fly.dev",
|
|
163
|
+
upstreamStatus: "unknown",
|
|
164
|
+
modelsCount: 1,
|
|
165
|
+
dataSource: "registry",
|
|
166
|
+
registryAlert: true,
|
|
167
|
+
alertReason: "registry 收录了但 fly app 失踪 — 严重事故, 立即下线",
|
|
168
|
+
removeHint: "立即下线 (registry-only)"
|
|
169
|
+
});
|
|
170
|
+
expect(out).toContain("app-row-registry-only");
|
|
171
|
+
// 严重事故 文案
|
|
172
|
+
expect(out).toContain("严重事故");
|
|
173
|
+
// alert-reason
|
|
174
|
+
expect(out).toContain("alert-reason");
|
|
175
|
+
expect(out).toContain("registry 收录了但 fly app 失踪");
|
|
176
|
+
// 立即下线 (registry-only) 按钮
|
|
177
|
+
expect(out).toContain("remove-hint-btn");
|
|
178
|
+
expect(out).toContain("立即下线 (registry-only)");
|
|
179
|
+
expect(out).toMatch(/data-action="remove"/);
|
|
180
|
+
expect(out).toMatch(/data-seller-id="tbs-ghost"/);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("dataSource 缺失 (老 1.0.31 行) → 兜底 'both', 不报错不标红", () => {
|
|
184
|
+
const out = renderRow({
|
|
185
|
+
id: "legacy",
|
|
186
|
+
name: "legacy",
|
|
187
|
+
app: "legacy",
|
|
188
|
+
url: "https://legacy.fly.dev",
|
|
189
|
+
registryStatus: "active",
|
|
190
|
+
nodeStatus: "active",
|
|
191
|
+
upstreamDomain: "legacy.fly.dev",
|
|
192
|
+
upstreamStatus: "healthy",
|
|
193
|
+
modelsCount: 1
|
|
194
|
+
// 故意没 dataSource 字段, 模拟 1.0.31 老 row
|
|
195
|
+
});
|
|
196
|
+
expect(out).toContain('class="app-row"');
|
|
197
|
+
expect(out).not.toContain("app-row-registry-only");
|
|
198
|
+
expect(out).not.toContain("app-row-fly-only");
|
|
199
|
+
// 兜底 chip = both
|
|
200
|
+
expect(out).toMatch(/datasource-chip both/);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import request from "supertest";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { buildAdminCli } from "../src/cli.js";
|
|
5
|
+
import { ConfigManager } from "../src/config.js";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
|
|
8
|
+
// Use the workspace-linked package. ts-jest will resolve the
|
|
9
|
+
// `.js` to the compiled `.ts` via the root moduleNameMapper.
|
|
10
|
+
import { buildApp, BootstrapConfig } from "@tokenbuddy/wallet-bootstrap";
|
|
11
|
+
|
|
12
|
+
const WALLET_TEST_DIR = path.resolve(__dirname, "../../data-vendor-cli-test");
|
|
13
|
+
const TEMP_REGISTRY_PATH = path.join(WALLET_TEST_DIR, "sellers.json");
|
|
14
|
+
const TEMP_REGISTRY_DB_PATH = path.join(WALLET_TEST_DIR, "bootstrap.sqlite");
|
|
15
|
+
const TEMP_CONFIG_PATH = path.join(WALLET_TEST_DIR, "tb-registry.yaml");
|
|
16
|
+
const TEMP_ADMIN_CONF_PATH = path.join(WALLET_TEST_DIR, "admin-config.json");
|
|
17
|
+
const SUPER_ADMIN_KEY = "platform-super-admin-key-please-rotate-me-1234567890";
|
|
18
|
+
const SESSION_SECRET = "session-secret-32-chars-min-please";
|
|
19
|
+
|
|
20
|
+
const baseRegistry = {
|
|
21
|
+
version: 1,
|
|
22
|
+
defaultSeller: "platform-seed",
|
|
23
|
+
sellers: [
|
|
24
|
+
{
|
|
25
|
+
id: "platform-seed",
|
|
26
|
+
name: "Platform Seed",
|
|
27
|
+
url: "https://platform-seed.example.com",
|
|
28
|
+
status: "active",
|
|
29
|
+
models: ["seed-model"],
|
|
30
|
+
supportedProtocols: ["chat_completions"],
|
|
31
|
+
paymentMethods: ["clawtip"]
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const baseConfig: BootstrapConfig = {
|
|
37
|
+
configPath: TEMP_CONFIG_PATH,
|
|
38
|
+
payTo: "pay-to-x",
|
|
39
|
+
sm4KeyBase64: "MDEyMzQ1Njc4OUFCQ0RFRg==",
|
|
40
|
+
skillSlug: "tb-registry",
|
|
41
|
+
skillId: "si-tb-registry",
|
|
42
|
+
description: "Activate",
|
|
43
|
+
resourceUrl: "https://example.com",
|
|
44
|
+
activationFeeFen: 1,
|
|
45
|
+
microsPerFen: 1000000,
|
|
46
|
+
sellerRegistryPath: TEMP_REGISTRY_PATH,
|
|
47
|
+
operatorSecret: "op-secret",
|
|
48
|
+
superAdminKey: SUPER_ADMIN_KEY,
|
|
49
|
+
superAdminLabel: "ops",
|
|
50
|
+
sessionSecret: SESSION_SECRET
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function buildServer() {
|
|
54
|
+
fs.mkdirSync(WALLET_TEST_DIR, { recursive: true });
|
|
55
|
+
fs.writeFileSync(TEMP_REGISTRY_PATH, JSON.stringify(baseRegistry), "utf8");
|
|
56
|
+
for (const f of [TEMP_REGISTRY_DB_PATH, `${TEMP_REGISTRY_DB_PATH}-wal`, `${TEMP_REGISTRY_DB_PATH}-shm`]) {
|
|
57
|
+
try { fs.rmSync(f, { force: true }); } catch { /* ignore */ }
|
|
58
|
+
}
|
|
59
|
+
return buildApp(baseConfig);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function createVendorToken(app: ReturnType<typeof buildServer>, name: string): Promise<string> {
|
|
63
|
+
const response = await request(app)
|
|
64
|
+
.post("/platform/vendors")
|
|
65
|
+
.set("X-Registry-Admin-Key", SUPER_ADMIN_KEY)
|
|
66
|
+
.send({ name });
|
|
67
|
+
expect(response.status).toBe(200);
|
|
68
|
+
return response.body.token;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runCli(args: string[], env: NodeJS.ProcessEnv): Promise<{ stdout: string; stderr: string; status: number }> {
|
|
72
|
+
// We invoke the CLI as a child process so the `program` state does
|
|
73
|
+
// not leak between tests. The package's `dist/` may not exist in
|
|
74
|
+
// worktrees, so we run from TypeScript source via `tsx` if
|
|
75
|
+
// available; fall back to requiring the source.
|
|
76
|
+
const cliEntry = path.resolve(__dirname, "../src/cli.ts");
|
|
77
|
+
// Try `npx tsx` first, then `node --import tsx`, then fallback
|
|
78
|
+
// to `node -r ts-node/register`.
|
|
79
|
+
let cmd: string;
|
|
80
|
+
let finalArgs: string[];
|
|
81
|
+
try {
|
|
82
|
+
execSync("which tsx", { stdio: "ignore" });
|
|
83
|
+
cmd = "npx";
|
|
84
|
+
finalArgs = ["tsx", cliEntry, ...args];
|
|
85
|
+
} catch {
|
|
86
|
+
cmd = "node";
|
|
87
|
+
finalArgs = ["--import", "tsx", cliEntry, ...args];
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const stdout = execSync(`${cmd} ${finalArgs.map((a) => JSON.stringify(a)).join(" ")}`, {
|
|
91
|
+
env: { ...process.env, ...env },
|
|
92
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
93
|
+
encoding: "utf8"
|
|
94
|
+
});
|
|
95
|
+
return { stdout, stderr: "", status: 0 };
|
|
96
|
+
} catch (err: any) {
|
|
97
|
+
return {
|
|
98
|
+
stdout: err.stdout?.toString() || "",
|
|
99
|
+
stderr: err.stderr?.toString() || err.message,
|
|
100
|
+
status: err.status || 1
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe("Vendor CLI integration (Step 5)", () => {
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
fs.mkdirSync(WALLET_TEST_DIR, { recursive: true });
|
|
108
|
+
for (const f of [TEMP_REGISTRY_PATH, TEMP_REGISTRY_DB_PATH, `${TEMP_REGISTRY_DB_PATH}-wal`, `${TEMP_REGISTRY_DB_PATH}-shm`, TEMP_CONFIG_PATH, TEMP_ADMIN_CONF_PATH]) {
|
|
109
|
+
try { fs.rmSync(f, { force: true }); } catch { /* ignore */ }
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
afterAll(() => {
|
|
114
|
+
fs.rmSync(WALLET_TEST_DIR, { recursive: true, force: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("buildAdminCli still constructs without errors after the registry redesign", () => {
|
|
118
|
+
const config = new ConfigManager(TEMP_ADMIN_CONF_PATH);
|
|
119
|
+
const cli = buildAdminCli(config);
|
|
120
|
+
expect(cli.commands.find((c: any) => c.name() === "vendor-bootstrap")).toBeDefined();
|
|
121
|
+
// The legacy `bootstrap` command is still there for backward compat.
|
|
122
|
+
expect(cli.commands.find((c: any) => c.name() === "bootstrap")).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("RegistryVendorClient can authenticate against a live wallet-bootstrap app and stage a seller", async () => {
|
|
126
|
+
const app = buildServer();
|
|
127
|
+
const token = await createVendorToken(app, "Acme");
|
|
128
|
+
// Direct HTTP call to the vendor API as the CLI would do.
|
|
129
|
+
const stage = await request(app)
|
|
130
|
+
.post("/platform/sellers/stage")
|
|
131
|
+
.set("Authorization", `Bearer ${token}`)
|
|
132
|
+
.send({ seller: {
|
|
133
|
+
id: "acme-seller-1",
|
|
134
|
+
name: "Acme Seller 1",
|
|
135
|
+
url: "https://acme.example.com",
|
|
136
|
+
status: "active",
|
|
137
|
+
models: ["m1"],
|
|
138
|
+
supportedProtocols: ["chat_completions"],
|
|
139
|
+
paymentMethods: ["clawtip"]
|
|
140
|
+
} });
|
|
141
|
+
expect(stage.status).toBe(200);
|
|
142
|
+
expect(stage.body.pendingSeller.id).toBe("acme-seller-1");
|
|
143
|
+
expect(stage.body.pendingSeller.status).toBe("staged");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("RegistryVendorClient listPendingSellers only returns this vendor's rows", async () => {
|
|
147
|
+
const app = buildServer();
|
|
148
|
+
const tokenA = await createVendorToken(app, "VendorA");
|
|
149
|
+
const tokenB = await createVendorToken(app, "VendorB");
|
|
150
|
+
await request(app)
|
|
151
|
+
.post("/platform/sellers/stage")
|
|
152
|
+
.set("Authorization", `Bearer ${tokenA}`)
|
|
153
|
+
.send({ seller: {
|
|
154
|
+
id: "va-1", name: "VA1", url: "https://va1.example.com", status: "active",
|
|
155
|
+
models: ["m1"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"]
|
|
156
|
+
} });
|
|
157
|
+
await request(app)
|
|
158
|
+
.post("/platform/sellers/stage")
|
|
159
|
+
.set("Authorization", `Bearer ${tokenB}`)
|
|
160
|
+
.send({ seller: {
|
|
161
|
+
id: "vb-1", name: "VB1", url: "https://vb1.example.com", status: "active",
|
|
162
|
+
models: ["m1"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"]
|
|
163
|
+
} });
|
|
164
|
+
const aList = await request(app)
|
|
165
|
+
.get("/platform/sellers/pending?status=staged")
|
|
166
|
+
.set("Authorization", `Bearer ${tokenA}`);
|
|
167
|
+
const bList = await request(app)
|
|
168
|
+
.get("/platform/sellers/pending?status=staged")
|
|
169
|
+
.set("Authorization", `Bearer ${tokenB}`);
|
|
170
|
+
expect(aList.body.pendingSellers).toHaveLength(1);
|
|
171
|
+
expect(aList.body.pendingSellers[0].id).toBe("va-1");
|
|
172
|
+
expect(bList.body.pendingSellers).toHaveLength(1);
|
|
173
|
+
expect(bList.body.pendingSellers[0].id).toBe("vb-1");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("RegistryVendorClient submitRelease -> listReleaseRequests round-trips", async () => {
|
|
177
|
+
const app = buildServer();
|
|
178
|
+
const token = await createVendorToken(app, "VendorC");
|
|
179
|
+
await request(app)
|
|
180
|
+
.post("/platform/sellers/stage")
|
|
181
|
+
.set("Authorization", `Bearer ${token}`)
|
|
182
|
+
.send({ seller: {
|
|
183
|
+
id: "vc-1", name: "VC1", url: "https://vc1.example.com", status: "active",
|
|
184
|
+
models: ["m1"], supportedProtocols: ["chat_completions"], paymentMethods: ["clawtip"]
|
|
185
|
+
} });
|
|
186
|
+
const submit = await request(app)
|
|
187
|
+
.post("/platform/release-requests")
|
|
188
|
+
.set("Authorization", `Bearer ${token}`)
|
|
189
|
+
.send({ stagedSellerIds: ["vc-1"], note: "test" });
|
|
190
|
+
expect(submit.status).toBe(200);
|
|
191
|
+
const list = await request(app)
|
|
192
|
+
.get("/platform/release-requests?limit=10")
|
|
193
|
+
.set("Authorization", `Bearer ${token}`);
|
|
194
|
+
expect(list.body.releaseRequests).toHaveLength(1);
|
|
195
|
+
expect(list.body.releaseRequests[0].id).toBe(submit.body.releaseRequest.id);
|
|
196
|
+
});
|
|
197
|
+
});
|