2026-07-16 11:53:09 +08:00
|
|
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
|
|
|
|
import { tmpdir } from "os";
|
2026-07-10 17:46:17 +08:00
|
|
|
|
import { join } from "path";
|
2026-05-28 18:37:07 +08:00
|
|
|
|
import { describe, expect, test } from "vite-plus/test";
|
2026-07-10 17:46:17 +08:00
|
|
|
|
import {
|
|
|
|
|
|
isDashScopeE2EReady,
|
2026-07-16 11:53:09 +08:00
|
|
|
|
isOpenApiE2EReady,
|
2026-07-10 17:46:17 +08:00
|
|
|
|
makeE2eOutputDir,
|
|
|
|
|
|
parseStdoutJson,
|
2026-08-28 19:14:06 +08:00
|
|
|
|
runCommandHelp,
|
2026-07-10 17:46:17 +08:00
|
|
|
|
runCommandE2e,
|
|
|
|
|
|
} from "./helpers.ts";
|
2026-07-10 15:55:53 +08:00
|
|
|
|
import { AUTH_ROUTES } from "./topic-routes.ts";
|
2026-05-28 18:37:07 +08:00
|
|
|
|
|
2026-07-16 11:53:09 +08:00
|
|
|
|
/** Auth E2E:本地参数/持久化契约默认执行;真实鉴权请求按对应 readiness gate 执行。 */
|
2026-05-28 18:37:07 +08:00
|
|
|
|
|
|
|
|
|
|
describe("e2e: auth", () => {
|
|
|
|
|
|
test("auth login --help 正常退出", async () => {
|
2026-08-28 19:14:06 +08:00
|
|
|
|
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "login", "--help"]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stderr).toMatch(/login|api-key/i);
|
2026-06-16 16:51:04 +08:00
|
|
|
|
expect(stderr).toMatch(/--console-site/);
|
2026-07-10 17:46:17 +08:00
|
|
|
|
expect(stderr).toMatch(/--open-api/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("auth login 一次只能选择一种登录模式", async () => {
|
|
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--console",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-e2e-placeholder",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(exitCode).toBe(2);
|
|
|
|
|
|
expect(stderr).toMatch(/Choose exactly one login mode/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("auth login 模式专属参数不能脱离对应模式", async () => {
|
|
|
|
|
|
const openApiFlagOnly = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--access-key-id",
|
|
|
|
|
|
"LTAI-e2e",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(openApiFlagOnly.exitCode).toBe(2);
|
|
|
|
|
|
expect(openApiFlagOnly.stderr).toMatch(/Use --open-api with --access-key-id/);
|
|
|
|
|
|
|
|
|
|
|
|
const baseUrlWithoutApiKey = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--console",
|
|
|
|
|
|
"--base-url",
|
|
|
|
|
|
"https://dashscope.aliyuncs.com",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(baseUrlWithoutApiKey.exitCode).toBe(2);
|
|
|
|
|
|
expect(baseUrlWithoutApiKey.stderr).toMatch(/Use --base-url only with --api-key/);
|
|
|
|
|
|
|
|
|
|
|
|
const consoleSiteWithoutConsole = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-e2e-placeholder",
|
|
|
|
|
|
"--console-site",
|
|
|
|
|
|
"international",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(consoleSiteWithoutConsole.exitCode).toBe(2);
|
|
|
|
|
|
expect(consoleSiteWithoutConsole.stderr).toMatch(/Use --console-site only with --console/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("auth login --open-api 要求 AK/SK 成对输入", async () => {
|
|
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--open-api",
|
|
|
|
|
|
"--access-key-id",
|
|
|
|
|
|
"LTAI-e2e",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(exitCode).toBe(2);
|
|
|
|
|
|
expect(stderr).toMatch(/Provide --access-key-id and --access-key-secret with --open-api/);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-16 11:53:09 +08:00
|
|
|
|
test("auth login --open-api --dry-run 使用 placeholder 时不请求服务端、不写配置", async () => {
|
|
|
|
|
|
const configDir = mkdtempSync(join(tmpdir(), "bl-auth-openapi-dry-run-"));
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
[
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--open-api",
|
|
|
|
|
|
"--access-key-id",
|
|
|
|
|
|
"LTAI-e2e-placeholder",
|
|
|
|
|
|
"--access-key-secret",
|
|
|
|
|
|
"secret-e2e-placeholder",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
],
|
|
|
|
|
|
{
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
|
|
|
|
|
|
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stdout).toContain("Would save OpenAPI AK/SK credentials");
|
|
|
|
|
|
expect(existsSync(join(configDir, "config.json"))).toBe(false);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rmSync(configDir, { recursive: true, force: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-28 18:37:07 +08:00
|
|
|
|
test("auth logout --help 正常退出", async () => {
|
2026-08-28 19:14:06 +08:00
|
|
|
|
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "logout", "--help"]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stderr).toMatch(/logout|dry-run|yes/i);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("auth status --help 正常退出", async () => {
|
2026-08-28 19:14:06 +08:00
|
|
|
|
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "status", "--help"]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stderr).toMatch(/status|output/i);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-29 14:53:24 +08:00
|
|
|
|
test("auth login 缺少 --api-key 时报用法错误并退出 (2)", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--quiet"]);
|
2026-06-29 14:53:24 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(2);
|
2026-07-10 17:46:17 +08:00
|
|
|
|
expect(stderr).toMatch(/Choose exactly one login mode/);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-28 13:10:56 +08:00
|
|
|
|
test("auth login --dry-run --api-key 不发起落盘", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-05-28 18:37:07 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-e2e-dry-run-placeholder",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
2026-08-28 13:10:56 +08:00
|
|
|
|
expect(stdout).toContain("Would save API key.");
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-16 14:43:26 +08:00
|
|
|
|
test("auth login --dry-run 仍校验显式 Base URL", async () => {
|
|
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-e2e-dry-run-placeholder",
|
|
|
|
|
|
"--base-url",
|
|
|
|
|
|
"ftp://example.com/models",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(exitCode).toBe(2);
|
|
|
|
|
|
expect(stderr).toMatch(/Invalid model base URL/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-28 13:10:56 +08:00
|
|
|
|
test("auth login --api-key 原子保存凭证和 Base URL(无联网探测)", async () => {
|
2026-07-15 16:26:43 +08:00
|
|
|
|
const configDir = makeE2eOutputDir("auth-api-key-login");
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const origin = "https://dashscope.example.test";
|
|
|
|
|
|
const sdkBaseUrl = `${origin}/compatible-mode/v1/?source=login#fragment`;
|
|
|
|
|
|
const login = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
["auth", "login", "--api-key", "sk-e2e-placeholder", "--base-url", sdkBaseUrl],
|
|
|
|
|
|
{
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
DASHSCOPE_API_KEY: "",
|
|
|
|
|
|
DASHSCOPE_BASE_URL: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(login.exitCode, login.stderr).toBe(0);
|
|
|
|
|
|
expect(login.stderr).toMatch(/API key saved to/);
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
expect(config.api_key).toBe("sk-e2e-placeholder");
|
|
|
|
|
|
expect(config.base_url).toBe(origin);
|
2026-07-15 16:26:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-16 14:43:26 +08:00
|
|
|
|
test("auth login --config token-plan 接受 Anthropic SDK Base URL", async () => {
|
|
|
|
|
|
const configDir = makeE2eOutputDir("auth-token-plan-anthropic-base-url");
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const origin = "https://token-plan.example.test";
|
|
|
|
|
|
const login = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
[
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--config",
|
|
|
|
|
|
"token-plan",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-sp-e2e-placeholder",
|
|
|
|
|
|
"--base-url",
|
|
|
|
|
|
`${origin}/apps/anthropic?source=sdk#fragment`,
|
|
|
|
|
|
],
|
|
|
|
|
|
{
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
DASHSCOPE_API_KEY: "",
|
|
|
|
|
|
DASHSCOPE_BASE_URL: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(login.exitCode, login.stderr).toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
expect(config["token-plan"]).toMatchObject({
|
|
|
|
|
|
api_key: "sk-sp-e2e-placeholder",
|
|
|
|
|
|
base_url: origin,
|
|
|
|
|
|
default_text_model: "qwen3.8-max",
|
|
|
|
|
|
default_video_model: "happyhorse-1.1-t2v",
|
|
|
|
|
|
default_image_to_video_model: "happyhorse-1.1-i2v",
|
|
|
|
|
|
default_reference_to_video_model: "happyhorse-1.1-r2v",
|
|
|
|
|
|
default_image_model: "wan2.7-image",
|
|
|
|
|
|
default_speech_model: "qwen-audio-3.0-tts-plus",
|
|
|
|
|
|
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
|
|
|
|
|
|
api_key_capabilities: [
|
|
|
|
|
|
"text.chat",
|
|
|
|
|
|
"vision.describe",
|
|
|
|
|
|
"image.generate",
|
|
|
|
|
|
"image.edit",
|
|
|
|
|
|
"speech.recognize",
|
|
|
|
|
|
"speech.synthesize",
|
|
|
|
|
|
"video.generate",
|
|
|
|
|
|
"video.ref",
|
|
|
|
|
|
"video.task.get",
|
|
|
|
|
|
"video.download",
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
2026-07-16 14:43:26 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-26 13:42:27 +08:00
|
|
|
|
test("auth login --config token-plan 追加新版 capability preset 且不删除已有能力", async () => {
|
2026-07-15 16:26:43 +08:00
|
|
|
|
const configDir = makeE2eOutputDir("auth-token-plan-preset-login");
|
|
|
|
|
|
writeFileSync(
|
|
|
|
|
|
join(configDir, "config.json"),
|
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
"token-plan": {
|
|
|
|
|
|
default_text_model: "custom-text-model",
|
2026-07-20 16:44:53 +08:00
|
|
|
|
default_video_model: "custom-video-model",
|
|
|
|
|
|
default_image_to_video_model: "custom-image-to-video-model",
|
|
|
|
|
|
default_reference_to_video_model: "custom-reference-to-video-model",
|
2026-07-15 16:26:43 +08:00
|
|
|
|
default_image_model: "custom-image-model",
|
2026-08-26 13:42:27 +08:00
|
|
|
|
api_key_capabilities: ["text.chat", "custom.command"],
|
2026-07-15 16:26:43 +08:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
) + "\n",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const login = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
|
|
|
|
|
|
{
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
DASHSCOPE_API_KEY: "sk-env-must-not-be-persisted",
|
|
|
|
|
|
DASHSCOPE_BASE_URL: "https://env-must-not-override-preset.example.test",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(login.exitCode, login.stderr).toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
expect(config.api_key).toBeUndefined();
|
|
|
|
|
|
expect(config.active_config).toBe("token-plan");
|
|
|
|
|
|
expect(config["token-plan"]).toMatchObject({
|
|
|
|
|
|
api_key: "sk-sp-e2e-placeholder",
|
|
|
|
|
|
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
|
|
|
|
|
|
default_text_model: "qwen3.8-max",
|
|
|
|
|
|
default_video_model: "happyhorse-1.1-t2v",
|
|
|
|
|
|
default_image_to_video_model: "happyhorse-1.1-i2v",
|
|
|
|
|
|
default_reference_to_video_model: "happyhorse-1.1-r2v",
|
|
|
|
|
|
default_image_model: "wan2.7-image",
|
|
|
|
|
|
default_speech_model: "qwen-audio-3.0-tts-plus",
|
|
|
|
|
|
default_speech_recognition_model: "qwen-audio-3.0-asr-flash",
|
|
|
|
|
|
api_key_capabilities: [
|
|
|
|
|
|
"text.chat",
|
|
|
|
|
|
"custom.command",
|
|
|
|
|
|
"vision.describe",
|
|
|
|
|
|
"image.generate",
|
|
|
|
|
|
"image.edit",
|
|
|
|
|
|
"speech.recognize",
|
|
|
|
|
|
"speech.synthesize",
|
|
|
|
|
|
"video.generate",
|
|
|
|
|
|
"video.ref",
|
|
|
|
|
|
"video.task.get",
|
|
|
|
|
|
"video.download",
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
|
|
|
|
|
expect((config["token-plan"] as Record<string, unknown>).base_url).not.toBe(
|
|
|
|
|
|
"https://env-must-not-override-preset.example.test",
|
|
|
|
|
|
);
|
|
|
|
|
|
expect((config["token-plan"] as Record<string, unknown>).api_key).not.toBe(
|
|
|
|
|
|
"sk-env-must-not-be-persisted",
|
|
|
|
|
|
);
|
2026-07-15 16:26:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-26 13:42:27 +08:00
|
|
|
|
test("auth login --config token-plan 为显式空白名单追加 capability preset", async () => {
|
|
|
|
|
|
const configDir = makeE2eOutputDir("auth-token-plan-empty-capabilities-login");
|
|
|
|
|
|
writeFileSync(
|
|
|
|
|
|
join(configDir, "config.json"),
|
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
"token-plan": {
|
|
|
|
|
|
api_key_capabilities: [],
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
) + "\n",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const login = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
["auth", "login", "--config", "token-plan", "--api-key", "sk-sp-e2e-placeholder"],
|
|
|
|
|
|
{
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
DASHSCOPE_API_KEY: "",
|
|
|
|
|
|
DASHSCOPE_BASE_URL: "https://env-ignored-for-persist.example.test",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(login.exitCode, login.stderr).toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
expect(config["token-plan"]).toMatchObject({
|
|
|
|
|
|
api_key_capabilities: [
|
|
|
|
|
|
"text.chat",
|
|
|
|
|
|
"vision.describe",
|
|
|
|
|
|
"image.generate",
|
|
|
|
|
|
"image.edit",
|
|
|
|
|
|
"speech.recognize",
|
|
|
|
|
|
"speech.synthesize",
|
|
|
|
|
|
"video.generate",
|
|
|
|
|
|
"video.ref",
|
|
|
|
|
|
"video.task.get",
|
|
|
|
|
|
"video.download",
|
|
|
|
|
|
],
|
|
|
|
|
|
});
|
2026-08-26 13:42:27 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-16 11:05:59 +08:00
|
|
|
|
test("auth login 未传 --config 时写当前激活 Config", async () => {
|
|
|
|
|
|
const configDir = makeE2eOutputDir("auth-active-profile-login");
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const storedBaseUrl = "https://dev-profile.example.test";
|
2026-07-16 11:05:59 +08:00
|
|
|
|
writeFileSync(
|
|
|
|
|
|
join(configDir, "config.json"),
|
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
active_config: "dev",
|
2026-08-28 13:10:56 +08:00
|
|
|
|
dev: { base_url: storedBaseUrl },
|
2026-07-16 11:05:59 +08:00
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
) + "\n",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const env = {
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
DASHSCOPE_API_KEY: "",
|
|
|
|
|
|
DASHSCOPE_BASE_URL: "",
|
|
|
|
|
|
};
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const activeLogin = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
["auth", "login", "--api-key", "sk-active-placeholder"],
|
|
|
|
|
|
env,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(activeLogin.exitCode, activeLogin.stderr).toBe(0);
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
expect(config.api_key).toBeUndefined();
|
|
|
|
|
|
expect(config.active_config).toBe("dev");
|
|
|
|
|
|
expect(config.dev).toMatchObject({
|
|
|
|
|
|
api_key: "sk-active-placeholder",
|
|
|
|
|
|
base_url: storedBaseUrl,
|
|
|
|
|
|
});
|
2026-07-16 11:05:59 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-28 13:10:56 +08:00
|
|
|
|
test("auth login --api-key 非法 Base URL 失败时不留下半配置", async () => {
|
2026-07-15 16:26:43 +08:00
|
|
|
|
const configDir = makeE2eOutputDir("auth-api-key-login-failure");
|
2026-08-28 13:10:56 +08:00
|
|
|
|
const login = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
[
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--config",
|
|
|
|
|
|
"failed-profile",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-invalid",
|
|
|
|
|
|
"--base-url",
|
|
|
|
|
|
"ftp://example.com/models",
|
|
|
|
|
|
],
|
|
|
|
|
|
{
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
DASHSCOPE_API_KEY: "",
|
|
|
|
|
|
DASHSCOPE_BASE_URL: "",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(login.exitCode).not.toBe(0);
|
|
|
|
|
|
expect(login.stderr).toMatch(/Invalid model base URL/);
|
|
|
|
|
|
expect(existsSync(join(configDir, "config.json"))).toBe(false);
|
2026-07-15 16:26:43 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-28 18:37:07 +08:00
|
|
|
|
test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-05-28 18:37:07 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
"--api-key",
|
|
|
|
|
|
"sk-e2e-dry-run-placeholder",
|
|
|
|
|
|
"--output",
|
|
|
|
|
|
"json",
|
|
|
|
|
|
"--timeout",
|
|
|
|
|
|
"120",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
2026-08-28 13:10:56 +08:00
|
|
|
|
expect(stdout).toContain("Would save API key.");
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-29 14:53:24 +08:00
|
|
|
|
test("auth login 缺少密钥且 --output json 时报用法错误并退出 (2)", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--output",
|
|
|
|
|
|
"json",
|
|
|
|
|
|
]);
|
2026-06-29 14:53:24 +08:00
|
|
|
|
expect(exitCode).toBe(2);
|
|
|
|
|
|
const err = JSON.parse(stderr.trim()) as { error?: { code?: number; message?: string } };
|
|
|
|
|
|
expect(err.error?.code).toBe(2);
|
2026-07-10 17:46:17 +08:00
|
|
|
|
expect(err.error?.message).toMatch(/Choose exactly one login mode/);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("auth logout --dry-run 不写入配置", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"logout",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stdout).toContain("No changes made.");
|
|
|
|
|
|
expect(stderr).not.toContain("Cleared api_key");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-07 00:07:21 +08:00
|
|
|
|
test("auth logout --dry-run --quiet", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-07-10 11:46:05 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"logout",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
"--quiet",
|
|
|
|
|
|
]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stdout).toContain("No changes made.");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("auth logout --dry-run --output json(不清除密钥)", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-05-28 18:37:07 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"logout",
|
|
|
|
|
|
"--dry-run",
|
|
|
|
|
|
"--output",
|
|
|
|
|
|
"json",
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stdout).toContain("No changes made.");
|
|
|
|
|
|
expect(stderr).not.toContain("Cleared api_key");
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-17 15:57:23 +08:00
|
|
|
|
test("auth logout 清除当前 Config 的全部凭证和 Base URL,保留普通配置", async () => {
|
|
|
|
|
|
const configDir = makeE2eOutputDir("auth-logout-all");
|
|
|
|
|
|
writeFileSync(
|
|
|
|
|
|
join(configDir, "config.json"),
|
|
|
|
|
|
JSON.stringify(
|
|
|
|
|
|
{
|
|
|
|
|
|
api_key: "sk-e2e-placeholder",
|
|
|
|
|
|
base_url: "https://model.example.com",
|
|
|
|
|
|
access_token: "console-token-placeholder",
|
|
|
|
|
|
access_key_id: "LTAI-e2e-placeholder",
|
|
|
|
|
|
access_key_secret: "secret-e2e-placeholder",
|
|
|
|
|
|
security_token: "sts-e2e-placeholder",
|
|
|
|
|
|
output: "json",
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
2,
|
|
|
|
|
|
) + "\n",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "logout"], {
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stderr).toContain("api_key / base_url / access_token");
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
expect(config).toEqual({ output: "json" });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-28 18:37:07 +08:00
|
|
|
|
test.skipIf(!isDashScopeE2EReady())("auth status 文本输出", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-07-10 11:46:05 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"status",
|
|
|
|
|
|
"--output",
|
|
|
|
|
|
"text",
|
|
|
|
|
|
]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
expect(stdout).toMatch(
|
|
|
|
|
|
/Authentication Status|API key:|Console token:|DashScope API:|Console gateway:/,
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test.skipIf(!isDashScopeE2EReady())("auth status --output json", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-07-10 11:46:05 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"status",
|
|
|
|
|
|
"--output",
|
|
|
|
|
|
"json",
|
|
|
|
|
|
]);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
const data = parseStdoutJson<{
|
|
|
|
|
|
authenticated?: boolean;
|
2026-06-28 20:42:33 +08:00
|
|
|
|
api_key?: { source?: string; masked?: string; base_url?: string };
|
2026-05-28 18:37:07 +08:00
|
|
|
|
}>(stdout);
|
|
|
|
|
|
expect(data.authenticated).toBe(true);
|
2026-06-28 20:42:33 +08:00
|
|
|
|
expect(data.api_key?.source).toBeDefined();
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-16 18:16:10 +08:00
|
|
|
|
test.skipIf(!isDashScopeE2EReady())(
|
2026-07-06 17:42:11 +08:00
|
|
|
|
"auth status --output json --quiet(base_url 经 env 指定;凭证域 flag 对 status 不可见)",
|
2026-06-16 18:16:10 +08:00
|
|
|
|
async () => {
|
2026-07-10 11:46:05 +08:00
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(
|
2026-07-10 15:55:53 +08:00
|
|
|
|
AUTH_ROUTES,
|
2026-07-06 20:43:29 +08:00
|
|
|
|
["auth", "status", "--output", "json", "--quiet"],
|
2026-07-06 17:42:11 +08:00
|
|
|
|
{ DASHSCOPE_BASE_URL: "https://dashscope.aliyuncs.com" },
|
|
|
|
|
|
);
|
2026-06-16 18:16:10 +08:00
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
2026-06-28 20:42:33 +08:00
|
|
|
|
const data = parseStdoutJson<{ authenticated?: boolean; api_key?: unknown }>(stdout);
|
2026-06-16 18:16:10 +08:00
|
|
|
|
expect(data.authenticated).toBe(true);
|
2026-06-28 20:42:33 +08:00
|
|
|
|
expect(data.api_key).toBeDefined();
|
2026-06-16 18:16:10 +08:00
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-07-06 17:42:11 +08:00
|
|
|
|
|
|
|
|
|
|
test("auth status 不接受凭证域覆盖 flag(--base-url 报 Unknown flag)", async () => {
|
2026-07-10 15:55:53 +08:00
|
|
|
|
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
2026-07-10 11:46:05 +08:00
|
|
|
|
"auth",
|
|
|
|
|
|
"status",
|
|
|
|
|
|
"--base-url",
|
|
|
|
|
|
"https://x.test",
|
|
|
|
|
|
]);
|
2026-07-06 17:42:11 +08:00
|
|
|
|
expect(exitCode).not.toBe(0);
|
|
|
|
|
|
expect(stderr).toMatch(/Unknown flag.*--base-url/);
|
|
|
|
|
|
});
|
2026-07-10 17:46:17 +08:00
|
|
|
|
|
|
|
|
|
|
test("auth status 展示 env OpenAPI AK/SK 且不接受 OpenAPI flag 覆盖", async () => {
|
|
|
|
|
|
const { stdout, stderr, exitCode } = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
["auth", "status", "--output", "json"],
|
|
|
|
|
|
{
|
|
|
|
|
|
ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-e2e-placeholder",
|
|
|
|
|
|
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "secret-e2e-placeholder",
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(exitCode, stderr).toBe(0);
|
|
|
|
|
|
const data = parseStdoutJson<{
|
|
|
|
|
|
authenticated?: boolean;
|
|
|
|
|
|
openapi?: { source?: string; access_key_id?: string; access_key_secret?: string };
|
|
|
|
|
|
}>(stdout);
|
|
|
|
|
|
expect(data.authenticated).toBe(true);
|
|
|
|
|
|
expect(data.openapi?.source).toBe("env");
|
|
|
|
|
|
expect(data.openapi?.access_key_id).not.toBe("LTAI-e2e-placeholder");
|
|
|
|
|
|
expect(data.openapi?.access_key_secret).not.toBe("secret-e2e-placeholder");
|
|
|
|
|
|
|
|
|
|
|
|
const denied = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--access-key-id", "ak"]);
|
|
|
|
|
|
expect(denied.exitCode).not.toBe(0);
|
|
|
|
|
|
expect(denied.stderr).toMatch(/Unknown flag.*--access-key-id/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-07-16 11:53:09 +08:00
|
|
|
|
test.skipIf(!isOpenApiE2EReady())(
|
|
|
|
|
|
"auth login --open-api 使用环境中的真实 AK/SK,持久化后支持单独 logout",
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID!.trim();
|
|
|
|
|
|
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET!.trim();
|
|
|
|
|
|
const configDir = mkdtempSync(join(tmpdir(), "bl-auth-openapi-login-"));
|
|
|
|
|
|
const env = {
|
|
|
|
|
|
BAILIAN_CONFIG_DIR: configDir,
|
|
|
|
|
|
ALIBABA_CLOUD_ACCESS_KEY_ID: "",
|
|
|
|
|
|
ALIBABA_CLOUD_ACCESS_KEY_SECRET: "",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const login = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
[
|
|
|
|
|
|
"auth",
|
|
|
|
|
|
"login",
|
|
|
|
|
|
"--open-api",
|
|
|
|
|
|
"--access-key-id",
|
|
|
|
|
|
accessKeyId,
|
|
|
|
|
|
"--access-key-secret",
|
|
|
|
|
|
accessKeySecret,
|
|
|
|
|
|
],
|
|
|
|
|
|
env,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(login.exitCode, login.stderr).toBe(0);
|
|
|
|
|
|
expect(login.stderr).toMatch(/OpenAPI credentials saved/);
|
|
|
|
|
|
|
|
|
|
|
|
const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record<
|
|
|
|
|
|
string,
|
|
|
|
|
|
unknown
|
|
|
|
|
|
>;
|
|
|
|
|
|
// 只断言布尔结果,避免失败 diff 把真实凭证打印到测试日志。
|
|
|
|
|
|
expect(config.access_key_id === accessKeyId).toBe(true);
|
|
|
|
|
|
expect(config.access_key_secret === accessKeySecret).toBe(true);
|
|
|
|
|
|
expect(config.openapi_access_key_id).toBeUndefined();
|
|
|
|
|
|
expect(config.openapi_access_key_secret).toBeUndefined();
|
|
|
|
|
|
|
|
|
|
|
|
const status = await runCommandE2e(
|
|
|
|
|
|
AUTH_ROUTES,
|
|
|
|
|
|
["auth", "status", "--output", "json"],
|
|
|
|
|
|
env,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(status.exitCode, status.stderr).toBe(0);
|
|
|
|
|
|
const data = parseStdoutJson<{
|
|
|
|
|
|
authenticated?: boolean;
|
|
|
|
|
|
openapi?: { source?: string; access_key_id?: string; access_key_secret?: string };
|
|
|
|
|
|
}>(status.stdout);
|
|
|
|
|
|
expect(data.authenticated).toBe(true);
|
|
|
|
|
|
expect(data.openapi?.source).toBe("config");
|
|
|
|
|
|
expect(data.openapi?.access_key_id === accessKeyId).toBe(false);
|
|
|
|
|
|
expect(data.openapi?.access_key_secret === accessKeySecret).toBe(false);
|
|
|
|
|
|
|
|
|
|
|
|
const logout = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--open-api"], env);
|
|
|
|
|
|
expect(logout.exitCode, logout.stderr).toBe(0);
|
|
|
|
|
|
expect(logout.stderr).toMatch(/Cleared access_key_id/);
|
|
|
|
|
|
|
|
|
|
|
|
const after = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--output", "json"], env);
|
|
|
|
|
|
expect(after.exitCode, after.stderr).toBe(0);
|
|
|
|
|
|
const afterData = parseStdoutJson<{ authenticated?: boolean; openapi?: unknown }>(
|
|
|
|
|
|
after.stdout,
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(afterData.openapi).toBeUndefined();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
rmSync(configDir, { recursive: true, force: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-05-28 18:37:07 +08:00
|
|
|
|
});
|