@tokenbuddy/tb-admin 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/cli.js +98 -25
- package/dist/src/config.d.ts +8 -2
- package/dist/src/config.js +17 -5
- package/dist/src/display-format.js +6 -14
- package/dist/src/init-command.d.ts +50 -0
- package/dist/src/init-command.js +347 -0
- package/dist/src/providers/fly-io.d.ts +3 -0
- package/dist/src/providers/fly-io.js +137 -0
- package/dist/src/providers/provider-definition.d.ts +38 -0
- package/dist/src/providers/provider-definition.js +2 -0
- package/dist/src/seller.d.ts +2 -0
- package/dist/src/seller.js +30 -13
- package/dist/src/server-cmd.d.ts +1 -0
- package/dist/src/server-cmd.js +9 -2
- package/dist/src/ui-actions.d.ts +3 -0
- package/dist/src/ui-actions.js +199 -27
- package/dist/src/ui-command.js +3 -2
- package/dist/src/ui-state.d.ts +1 -3
- package/dist/src/ui-state.js +4 -8
- package/dist/src/ui-static.js +43 -15
- package/dist/src/workdir.d.ts +21 -0
- package/dist/src/workdir.js +50 -0
- package/package.json +9 -3
- package/templates/providers/fly.io/admin.toml.example +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/README.md +18 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/admin-web.example.env +3 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/cloudflare-r2.example.env +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry-signing-key.example.json +6 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/registry.example.json +14 -0
- package/templates/providers/fly.io/deploy-secrets/bootstrap/tb-registry.example.yaml +14 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/README.md +13 -0
- package/templates/providers/fly.io/deploy-secrets/seller-configs/seller.example.yaml +35 -0
- package/templates/providers/fly.io/env/deploy.env.example +12 -0
- package/templates/providers/fly.io/fly/fly.tb-registry.toml +31 -0
- package/templates/providers/fly.io/fly/fly.tb-seller.toml +25 -0
- package/templates/providers/fly.io/provider.toml.example +10 -0
- package/dist/src/bootstrap-registry.d.ts.map +0 -1
- package/dist/src/bootstrap-registry.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/client.d.ts.map +0 -1
- package/dist/src/client.js.map +0 -1
- package/dist/src/config.d.ts.map +0 -1
- package/dist/src/config.js.map +0 -1
- package/dist/src/display-format.d.ts.map +0 -1
- package/dist/src/display-format.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/provider.d.ts.map +0 -1
- package/dist/src/provider.js.map +0 -1
- package/dist/src/seller.d.ts.map +0 -1
- package/dist/src/seller.js.map +0 -1
- package/dist/src/server-cmd.d.ts.map +0 -1
- package/dist/src/server-cmd.js.map +0 -1
- package/dist/src/ui-actions.d.ts.map +0 -1
- package/dist/src/ui-actions.js.map +0 -1
- package/dist/src/ui-command.d.ts.map +0 -1
- package/dist/src/ui-command.js.map +0 -1
- package/dist/src/ui-server.d.ts.map +0 -1
- package/dist/src/ui-server.js.map +0 -1
- package/dist/src/ui-state.d.ts.map +0 -1
- package/dist/src/ui-state.js.map +0 -1
- package/dist/src/ui-static.d.ts.map +0 -1
- package/dist/src/ui-static.js.map +0 -1
- package/dist/src/upstream-balance-probe.d.ts.map +0 -1
- package/dist/src/upstream-balance-probe.js.map +0 -1
- package/dist/src/vendor-client.d.ts.map +0 -1
- package/dist/src/vendor-client.js.map +0 -1
- package/dist/src/vendor-commands.d.ts.map +0 -1
- package/dist/src/vendor-commands.js.map +0 -1
- package/src/bootstrap-registry.ts +0 -90
- package/src/cli.ts +0 -1614
- package/src/client.ts +0 -179
- package/src/config.ts +0 -194
- package/src/display-format.ts +0 -411
- package/src/index.ts +0 -11
- package/src/provider.ts +0 -150
- package/src/seller.ts +0 -538
- package/src/server-cmd.ts +0 -362
- package/src/ui-actions.ts +0 -1040
- package/src/ui-command.ts +0 -44
- package/src/ui-server.ts +0 -353
- package/src/ui-state.ts +0 -1318
- package/src/ui-static.ts +0 -673
- package/src/upstream-balance-probe.ts +0 -13
- package/src/vendor-client.ts +0 -23
- package/src/vendor-commands.ts +0 -65
- package/tests/admin.test.ts +0 -2162
- package/tests/seller.test.ts +0 -388
- package/tests/ui-state-fleet.test.ts +0 -526
- package/tests/ui-static-row.test.ts +0 -467
- package/tests/vendor-cli.test.ts +0 -241
- package/tsconfig.json +0 -8
package/tests/seller.test.ts
DELETED
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Step 12 v1.1: tb-admin seller 子命令真实 CLI 测试.
|
|
3
|
-
*
|
|
4
|
-
* 原则: 不 mock. 全部跑真 `tb-admin` CLI (它内部调真 flyctl). 通过 --dry-run 模式
|
|
5
|
-
* 避免真改 fly 资源.
|
|
6
|
-
*
|
|
7
|
-
* 覆盖:
|
|
8
|
-
* - ls/status/create/deploy/remove 5 op 都有 文本路径 + --json 路径
|
|
9
|
-
* - --json 输出可被 JSON.parse
|
|
10
|
-
* - 文本路径不破坏 1.0.31 行为 (跟 flyctl 原始 stdout 一致)
|
|
11
|
-
* - dry-run 模式返回 commands 数组
|
|
12
|
-
*
|
|
13
|
-
* 依赖:
|
|
14
|
-
* - 真 flyctl 在 PATH (本机已装 v0.4.53)
|
|
15
|
-
* - tb-admin CLI 是 source-built 1:1 (跟 git checkout 一致), bin/tb-admin.js 可执行
|
|
16
|
-
*
|
|
17
|
-
* 失败模式:
|
|
18
|
-
* - 没装 flyctl: 大部分 test skip (it.skip / describe.skip)
|
|
19
|
-
* - 没 token: 1.0.31 行为不变, CLI 仍然跑 flyctl, 但 flyctl 鉴权失败的部分不影响 dry-run
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { execFileSync } from "node:child_process";
|
|
23
|
-
import { existsSync, mkdtempSync } from "node:fs";
|
|
24
|
-
import { tmpdir } from "node:os";
|
|
25
|
-
import { join } from "node:path";
|
|
26
|
-
import { ConfigManager } from "../src/config.js";
|
|
27
|
-
import { SellerCommandRunner, parseFlyListJson, parseFlyStatusJson } from "../src/seller.js";
|
|
28
|
-
import { FlyProvider, type FlyProviderRuntime } from "../src/server-cmd.js";
|
|
29
|
-
|
|
30
|
-
const TB_ADMIN_BIN = join(__dirname, "..", "bin", "tb-admin.js");
|
|
31
|
-
const FLYCTL = "flyctl";
|
|
32
|
-
const RUN_LIVE_FLY_TESTS = process.env.TOKENBUDDY_ADMIN_LIVE_FLY_TESTS === "1";
|
|
33
|
-
|
|
34
|
-
function flyctlInstalled(): boolean {
|
|
35
|
-
try {
|
|
36
|
-
execFileSync("which", [FLYCTL], { stdio: "ignore" });
|
|
37
|
-
return true;
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function makeEmptyConfigManager(): ConfigManager {
|
|
44
|
-
const dir = mkdtempSync(join(tmpdir(), "tb-admin-test-"));
|
|
45
|
-
return new ConfigManager(join(dir, "admin.toml"));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function makeMissingFlyctlConfigManager(): ConfigManager {
|
|
49
|
-
const manager = makeEmptyConfigManager();
|
|
50
|
-
manager.setSellerProvider("fly", { flyctl_path: "/tmp/tokenbuddy-missing-flyctl" });
|
|
51
|
-
return manager;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function tbAdminBinInstalled(): boolean {
|
|
55
|
-
return existsSync(TB_ADMIN_BIN);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function runTbAdminReal(args: string[]): { ok: boolean; stdout: string; stderr: string } {
|
|
59
|
-
try {
|
|
60
|
-
const stdout = execFileSync("node", [TB_ADMIN_BIN, ...args], {
|
|
61
|
-
encoding: "utf8",
|
|
62
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
-
env: { ...process.env, NO_COLOR: "1" }
|
|
64
|
-
});
|
|
65
|
-
return { ok: true, stdout, stderr: "" };
|
|
66
|
-
} catch (err) {
|
|
67
|
-
const e = err as { stdout?: Buffer | string; stderr?: Buffer | string };
|
|
68
|
-
return {
|
|
69
|
-
ok: false,
|
|
70
|
-
stdout: typeof e.stdout === "string" ? e.stdout : e.stdout ? e.stdout.toString() : "",
|
|
71
|
-
stderr: typeof e.stderr === "string" ? e.stderr : e.stderr ? e.stderr.toString() : ""
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
describe("seller parser helpers (pure, no IO)", () => {
|
|
77
|
-
test("parseFlyListJson normalizes flyctl apps list --json", () => {
|
|
78
|
-
const jsonText = JSON.stringify([
|
|
79
|
-
{ Name: "tbs-aaa", Status: "deployed", Owner: "personal", Version: 7, Deployed: true },
|
|
80
|
-
{ Name: "tbs-bbb", Status: "suspended", Owner: "personal", Version: 7, Deployed: false }
|
|
81
|
-
]);
|
|
82
|
-
const apps = parseFlyListJson(jsonText);
|
|
83
|
-
expect(apps).toHaveLength(2);
|
|
84
|
-
expect(apps[0].name).toBe("tbs-aaa");
|
|
85
|
-
expect(apps[0].status).toBe("deployed");
|
|
86
|
-
expect(apps[1].status).toBe("suspended");
|
|
87
|
-
expect(apps[0].raw).toMatchObject({ Name: "tbs-aaa" });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("parseFlyListJson throws on invalid JSON", () => {
|
|
91
|
-
expect(() => parseFlyListJson("not-json")).toThrow(/invalid JSON/);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("parseFlyListJson throws on non-array", () => {
|
|
95
|
-
expect(() => parseFlyListJson('{"foo":"bar"}')).toThrow(/did not return an array/);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test("parseFlyStatusJson parses single app status", () => {
|
|
99
|
-
const jsonText = JSON.stringify({ Name: "tbs-aaa", Status: "deployed", Deployed: true });
|
|
100
|
-
const status = parseFlyStatusJson(jsonText);
|
|
101
|
-
expect(status).toMatchObject({ Name: "tbs-aaa" });
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test("parseFlyStatusJson throws on array input", () => {
|
|
105
|
-
expect(() => parseFlyStatusJson("[]")).toThrow(/did not return an object/);
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("tb-admin seller CLI real spawn (no mock)", () => {
|
|
110
|
-
// 没用 beforeAll: 一些 jest sandbox 下 beforeAll 抛错会让整个 describe fail.
|
|
111
|
-
// 每个 test 进来时现场探测.
|
|
112
|
-
const itRequires = (name: string, fn: () => void | Promise<void>) => {
|
|
113
|
-
test(name, async () => {
|
|
114
|
-
if (!RUN_LIVE_FLY_TESTS) {
|
|
115
|
-
console.warn(`[skipped] ${name}: set TOKENBUDDY_ADMIN_LIVE_FLY_TESTS=1 to run live Fly.io checks`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const hasFlyctl = flyctlInstalled();
|
|
119
|
-
const hasBin = tbAdminBinInstalled();
|
|
120
|
-
if (!hasFlyctl || !hasBin) {
|
|
121
|
-
const reason = !hasFlyctl ? "flyctl not in PATH" : "tb-admin bin not built";
|
|
122
|
-
console.warn(`[skipped] ${name}: ${reason}`);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
await fn();
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
itRequires("ls text path returns flyctl human-readable table (1.0.31 behavior preserved)", () => {
|
|
130
|
-
const result = runTbAdminReal(["seller", "ls"]);
|
|
131
|
-
expect(result.ok).toBe(true);
|
|
132
|
-
// flyctl apps list 文本输出有 "│" 列分隔符
|
|
133
|
-
expect(result.stdout).toMatch(/│/);
|
|
134
|
-
expect(result.stdout).toMatch(/STATUS/);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
itRequires("ls --json returns valid JSON object with ok, provider, action, count, apps[]", () => {
|
|
138
|
-
const result = runTbAdminReal(["seller", "ls", "--json"]);
|
|
139
|
-
expect(result.ok).toBe(true);
|
|
140
|
-
const parsed = JSON.parse(result.stdout);
|
|
141
|
-
expect(parsed).toMatchObject({ ok: true, provider: "fly", action: "list" });
|
|
142
|
-
expect(typeof parsed.count).toBe("number");
|
|
143
|
-
expect(Array.isArray(parsed.apps)).toBe(true);
|
|
144
|
-
expect(parsed.apps.length).toBe(parsed.count);
|
|
145
|
-
if (parsed.apps.length > 0) {
|
|
146
|
-
expect(parsed.apps[0]).toHaveProperty("name");
|
|
147
|
-
expect(parsed.apps[0]).toHaveProperty("status");
|
|
148
|
-
expect(parsed.apps[0]).toHaveProperty("raw");
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
itRequires("status --json returns valid JSON object with ok, provider, action, app, status{}", () => {
|
|
153
|
-
// 用 live 真实存在的 tbs-86d81e app (1.0.31 live)
|
|
154
|
-
const result = runTbAdminReal(["seller", "status", "tbs-86d81e", "--json"]);
|
|
155
|
-
expect(result.ok).toBe(true);
|
|
156
|
-
const parsed = JSON.parse(result.stdout);
|
|
157
|
-
expect(parsed).toMatchObject({
|
|
158
|
-
ok: true,
|
|
159
|
-
provider: "fly",
|
|
160
|
-
action: "status",
|
|
161
|
-
app: "tbs-86d81e"
|
|
162
|
-
});
|
|
163
|
-
expect(parsed.status).toBeDefined();
|
|
164
|
-
expect(typeof parsed.status).toBe("object");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
itRequires("create --json --dry-run returns commands array, no real flyctl effect", () => {
|
|
168
|
-
const result = runTbAdminReal([
|
|
169
|
-
"seller", "create", "test-dry-run-app",
|
|
170
|
-
"--image", "registry.fly.io/tb-seller:1.0.31",
|
|
171
|
-
"--fly-config", "deploy/fly.io/fly.tb-seller.toml",
|
|
172
|
-
"--operator-secret", "test-secret",
|
|
173
|
-
"--region", "sin",
|
|
174
|
-
"--dry-run", "--json"
|
|
175
|
-
]);
|
|
176
|
-
expect(result.ok).toBe(true);
|
|
177
|
-
const parsed = JSON.parse(result.stdout);
|
|
178
|
-
expect(parsed).toMatchObject({
|
|
179
|
-
ok: true,
|
|
180
|
-
provider: "fly",
|
|
181
|
-
action: "create",
|
|
182
|
-
dryRun: true
|
|
183
|
-
});
|
|
184
|
-
expect(Array.isArray(parsed.commands)).toBe(true);
|
|
185
|
-
expect(parsed.commands.length).toBeGreaterThan(0);
|
|
186
|
-
// 包含 fly deploy + fly machine update 命令
|
|
187
|
-
const allCommands = parsed.commands.join(" ");
|
|
188
|
-
expect(allCommands).toMatch(/tb-seller/);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
itRequires("deploy --json --dry-run returns single machine update command", () => {
|
|
192
|
-
const result = runTbAdminReal([
|
|
193
|
-
"seller", "deploy", "tbs-86d81e",
|
|
194
|
-
"--image", "registry.fly.io/tb-seller:1.0.31",
|
|
195
|
-
"--dry-run", "--json"
|
|
196
|
-
]);
|
|
197
|
-
expect(result.ok).toBe(true);
|
|
198
|
-
const parsed = JSON.parse(result.stdout);
|
|
199
|
-
expect(parsed).toMatchObject({
|
|
200
|
-
ok: true,
|
|
201
|
-
provider: "fly",
|
|
202
|
-
action: "deploy",
|
|
203
|
-
app: "tbs-86d81e",
|
|
204
|
-
dryRun: true
|
|
205
|
-
});
|
|
206
|
-
expect(parsed.commands[0]).toContain("machine update");
|
|
207
|
-
expect(parsed.commands[0]).toContain("tbs-86d81e");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
itRequires("remove --json --dry-run returns apps destroy command", () => {
|
|
211
|
-
const result = runTbAdminReal([
|
|
212
|
-
"seller", "remove", "tbs-86d81e",
|
|
213
|
-
"--dry-run", "--json"
|
|
214
|
-
]);
|
|
215
|
-
expect(result.ok).toBe(true);
|
|
216
|
-
const parsed = JSON.parse(result.stdout);
|
|
217
|
-
expect(parsed).toMatchObject({
|
|
218
|
-
ok: true,
|
|
219
|
-
provider: "fly",
|
|
220
|
-
action: "remove",
|
|
221
|
-
dryRun: true
|
|
222
|
-
});
|
|
223
|
-
expect(parsed.commands[0]).toContain("apps destroy tbs-86d81e");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
itRequires("roll --dry-run text path lists all live tbs-* candidates from fly apps list", () => {
|
|
227
|
-
const result = runTbAdminReal([
|
|
228
|
-
"seller", "roll",
|
|
229
|
-
"--image", "registry.fly.io/tb-seller:1.0.33",
|
|
230
|
-
"--dry-run"
|
|
231
|
-
]);
|
|
232
|
-
expect(result.ok).toBe(true);
|
|
233
|
-
expect(result.stdout).toMatch(/\[Fly\.io\] roll candidates \(tbs-\*, excludes applied\): \d+/);
|
|
234
|
-
// 至少要有 tbs-86d81e (live 1.0.31 已知)
|
|
235
|
-
expect(result.stdout).toMatch(/tbs-86d81e/);
|
|
236
|
-
// dry-run 模式: 每台都是 [DRY-RUN] 而非真 flyctl
|
|
237
|
-
expect(result.stdout).toMatch(/\[DRY-RUN\] would update tbs-86d81e image to/);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
itRequires("roll --dry-run --json returns structured candidates/excluded/attempts", () => {
|
|
241
|
-
const result = runTbAdminReal([
|
|
242
|
-
"seller", "roll",
|
|
243
|
-
"--image", "registry.fly.io/tb-seller:1.0.33",
|
|
244
|
-
"--exclude", "tbs-anpin-ai-0d7517,tbs-openrouter-ai-06vry",
|
|
245
|
-
"--dry-run", "--json"
|
|
246
|
-
]);
|
|
247
|
-
expect(result.ok).toBe(true);
|
|
248
|
-
const parsed = JSON.parse(result.stdout);
|
|
249
|
-
expect(parsed).toMatchObject({
|
|
250
|
-
ok: true,
|
|
251
|
-
provider: "fly",
|
|
252
|
-
action: "roll",
|
|
253
|
-
image: "registry.fly.io/tb-seller:1.0.33",
|
|
254
|
-
dryRun: true,
|
|
255
|
-
completed: true
|
|
256
|
-
});
|
|
257
|
-
expect(Array.isArray(parsed.candidates)).toBe(true);
|
|
258
|
-
expect(parsed.candidates).toContain("tbs-86d81e");
|
|
259
|
-
expect(parsed.candidates).not.toContain("tbs-anpin-ai-0d7517");
|
|
260
|
-
expect(parsed.excluded).toEqual(expect.arrayContaining(["tbs-anpin-ai-0d7517", "tbs-openrouter-ai-06vry"]));
|
|
261
|
-
// attempts 顺序应该跟 candidates 顺序一致
|
|
262
|
-
expect(parsed.attempts.length).toBe(parsed.candidates.length);
|
|
263
|
-
for (let i = 0; i < parsed.attempts.length; i++) {
|
|
264
|
-
expect(parsed.attempts[i].app).toBe(parsed.candidates[i]);
|
|
265
|
-
expect(parsed.attempts[i].ok).toBe(true);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
itRequires("roll rejects empty image and returns 1 (with usage error)", () => {
|
|
270
|
-
const result = runTbAdminReal([
|
|
271
|
-
"seller", "roll", "--dry-run", "--json"
|
|
272
|
-
]);
|
|
273
|
-
// commander requiredOption 失败时, 进程退出非 0
|
|
274
|
-
expect(result.ok).toBe(false);
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
describe("SellerCommandRunner integration with real ConfigManager (no mock)", () => {
|
|
279
|
-
// 验证 Runner 行为通过真 ConfigManager 拉真 config (有 token, 装好 flyctl 的环境)
|
|
280
|
-
function tryRealConfigManager(): ConfigManager | null {
|
|
281
|
-
const home = process.env.HOME || "";
|
|
282
|
-
const adminToml = join(home, ".config", "tokenbuddy", "admin.toml");
|
|
283
|
-
if (!existsSync(adminToml)) {
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
return new ConfigManager(adminToml);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
test("ls(false) returns flyctl stdout string via real FlyProvider (no mock)", () => {
|
|
290
|
-
if (!RUN_LIVE_FLY_TESTS) {
|
|
291
|
-
const runner = new SellerCommandRunner(makeMissingFlyctlConfigManager());
|
|
292
|
-
expect(() => runner.ls(false)).toThrow(/flyctl|not installed|seller_providers/);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
const mgr = tryRealConfigManager();
|
|
296
|
-
if (!mgr) {
|
|
297
|
-
// 环境无 admin profile, 用空 ConfigManager 走 fallback
|
|
298
|
-
const tmp = makeEmptyConfigManager();
|
|
299
|
-
const runner = new SellerCommandRunner(tmp);
|
|
300
|
-
// 没有 provider config -> 走 FlyProvider -> flyctl not installed (test env) -> throw
|
|
301
|
-
try {
|
|
302
|
-
runner.ls(false);
|
|
303
|
-
throw new Error("expected flyctl-not-installed error");
|
|
304
|
-
} catch (err) {
|
|
305
|
-
expect((err as Error).message).toMatch(/flyctl|not installed|seller_providers/);
|
|
306
|
-
}
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
const runner = new SellerCommandRunner(mgr);
|
|
310
|
-
const out = runner.ls(false);
|
|
311
|
-
expect(typeof out).toBe("string");
|
|
312
|
-
expect(out).toMatch(/│/);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
describe("FlyProvider dry-run paths (no mock, real flyctl required)", () => {
|
|
317
|
-
// 1.0.31 行为 1:1 验证: FlyProvider 已有方法的 dry-run 模式不调 flyctl 真子命令,
|
|
318
|
-
// 只输出 plan. 这是 FlyProvider 的核心契约.
|
|
319
|
-
|
|
320
|
-
function makeRealProvider(): FlyProvider | null {
|
|
321
|
-
if (!RUN_LIVE_FLY_TESTS) {
|
|
322
|
-
return new FlyProvider({
|
|
323
|
-
default_region: "sin",
|
|
324
|
-
operator_secret: "test-secret",
|
|
325
|
-
volume_name: "tb_seller_data",
|
|
326
|
-
volume_size_gb: 1
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
const home = process.env.HOME || "";
|
|
330
|
-
const adminToml = join(home, ".config", "tokenbuddy", "admin.toml");
|
|
331
|
-
if (!existsSync(adminToml)) {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
const mgr = new ConfigManager(adminToml);
|
|
335
|
-
const cfg = mgr.getSellerProvider("fly");
|
|
336
|
-
if (!cfg) {
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
|
-
// 1.0.31 FlyProvider 接口 1:1 行为验证, runtime 走真值
|
|
340
|
-
const runtime: Partial<FlyProviderRuntime> = {};
|
|
341
|
-
return new FlyProvider(cfg, runtime);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
test("createSeller dry-run returns plan string without calling flyctl", () => {
|
|
345
|
-
const provider = makeRealProvider();
|
|
346
|
-
if (!provider) {
|
|
347
|
-
// 没 admin profile -> skip
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const plan = provider.createSeller({
|
|
351
|
-
name: "alpha",
|
|
352
|
-
region: "sin",
|
|
353
|
-
image: "registry.fly.io/tb-seller:1.0.31",
|
|
354
|
-
operatorSecret: "secret",
|
|
355
|
-
flyConfig: "deploy/fly.io/fly.tb-seller.toml",
|
|
356
|
-
volumeName: "tb_seller_data",
|
|
357
|
-
volumeSizeGb: 1,
|
|
358
|
-
dryRun: true
|
|
359
|
-
});
|
|
360
|
-
expect(plan).toMatch(/DRY-RUN/);
|
|
361
|
-
expect(plan).toContain("tb-seller-alpha");
|
|
362
|
-
expect(plan).toContain("registry.fly.io/tb-seller:1.0.31");
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
test("deploySeller dry-run returns plan string without calling flyctl", () => {
|
|
366
|
-
const provider = makeRealProvider();
|
|
367
|
-
if (!provider) {
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
const plan = provider.deploySeller({
|
|
371
|
-
app: "tbs-86d81e",
|
|
372
|
-
image: "registry.fly.io/tb-seller:1.0.31",
|
|
373
|
-
dryRun: true
|
|
374
|
-
});
|
|
375
|
-
expect(plan).toMatch(/DRY-RUN/);
|
|
376
|
-
expect(plan).toContain("tbs-86d81e");
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
test("removeSeller dry-run returns plan string without calling flyctl", () => {
|
|
380
|
-
const provider = makeRealProvider();
|
|
381
|
-
if (!provider) {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const plan = provider.removeSeller("tbs-86d81e", true);
|
|
385
|
-
expect(plan).toMatch(/DRY-RUN/);
|
|
386
|
-
expect(plan).toContain("tbs-86d81e");
|
|
387
|
-
});
|
|
388
|
-
});
|