mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38926aeb22 | |||
| e3f2996291 | |||
| 59250c4f44 | |||
| 98ebeb398c | |||
| 5c96077d9f | |||
| 8a7674e7c3 | |||
| 4fdd8389f8 | |||
| bd8b08b617 |
@@ -6,6 +6,12 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
||||
|
||||
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed API Key validation from `bl auth login`.
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### Added
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
|
||||
|
||||
## [1.18.1] - 2026-08-28
|
||||
|
||||
### 已移除
|
||||
|
||||
- 移除 `bl auth login` 的 API Key 校验。
|
||||
|
||||
## [1.18.0] - 2026-08-27
|
||||
|
||||
### 新增
|
||||
|
||||
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
|
||||
`~/.bailian/config.json` 可同时保存 `api_key`、`access_token` 与 `access_key_*`。登录任一种方式不得删除另一种:
|
||||
|
||||
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 验证成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
|
||||
- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
|
||||
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
|
||||
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
|
||||
- `bl auth logout --console` 只清 `access_token`
|
||||
@@ -47,9 +47,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
|
||||
- `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
|
||||
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
|
||||
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
|
||||
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录落盘成功后物化写入,升级 preset 需要重新登录
|
||||
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
|
||||
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
|
||||
- 显式 `auth login --config <name>` 在凭证落盘成功后自动激活目标 Profile;未传
|
||||
`--config` 时继续写当前激活项,失败和 dry-run 不切换
|
||||
- `resolveConsole()` — `auth: "console"` 命令;当前 token 来自 config `access_token`,region/site/switchAgent 来自 flag > config > 默认
|
||||
- `resolveOpenApi()` — `auth: "openapi"` 命令;优先级 `--access-key-id/--access-key-secret` > `ALIBABA_CLOUD_ACCESS_KEY_ID/ALIBABA_CLOUD_ACCESS_KEY_SECRET` > config `access_key_*`。兼容读取旧字段 `openapi_access_key_*`,新写入只写短字段
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## 架构分层
|
||||
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
| 层级 | 路径 | 测什么 |
|
||||
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup(`private`,不发布) |
|
||||
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/live(gated);每用例最小路由 |
|
||||
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
|
||||
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help;根 help、鉴权域等代表性子进程冒烟 |
|
||||
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help;`--version`、`search --help` 等代表性子进程冒烟 |
|
||||
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
|
||||
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
|
||||
|
||||
## 触发条件
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
### commands E2E
|
||||
|
||||
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
|
||||
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程)
|
||||
- 子进程:缺参、dry-run、live 使用 `runCommandE2e(routes, args)`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射)
|
||||
- fixtures:`packages/commands/tests/e2e/fixtures/`
|
||||
- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map)
|
||||
|
||||
@@ -37,10 +38,12 @@
|
||||
|
||||
- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts`
|
||||
- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
|
||||
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage;禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
|
||||
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
|
||||
|
||||
### 共享
|
||||
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`
|
||||
- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
|
||||
- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts`
|
||||
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
|
||||
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
|
||||
@@ -48,9 +51,12 @@
|
||||
## 双层 describe(固定结构)
|
||||
|
||||
```ts
|
||||
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
|
||||
describe("e2e: <topic>", () => {
|
||||
test("<subcommand> --help 正常退出", ...);
|
||||
test("<subcommand> --help 正常退出", async () => {
|
||||
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// 2) skipIf:缺参 / dry-run / 真实集成
|
||||
@@ -74,7 +80,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
|
||||
## 用例类型
|
||||
|
||||
1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
|
||||
1. **--help**:`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
|
||||
2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`
|
||||
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
|
||||
4. **真实集成**:放在 skip 块**末尾**
|
||||
@@ -105,7 +111,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
|
||||
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
|
||||
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
|
||||
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
|
||||
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
|
||||
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
|
||||
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
|
||||
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
- 解析阶段用局部变量保留“是否显式传入 `--config`”的信息;完成 Config 选择后不进入 `Settings`。
|
||||
- `--config default` 必须显式选择顶层配置并绕过命名激活项。
|
||||
- 普通命令的显式 `--config` 只覆盖本次选择,不修改持久化激活状态;例外是
|
||||
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
|
||||
`auth login --config ...`,凭证落盘成功后自动激活该 Profile。
|
||||
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
|
||||
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
|
||||
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。
|
||||
- 对命中内置 preset 的 Profile,API Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
|
||||
- 对命中内置 preset 的 Profile,API Key 登录落盘成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
|
||||
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get` → `video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
|
||||
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
|
||||
- Console access token 自动刷新等后台读写必须携带 `settings.configName`,不得直接读写顶层 default。
|
||||
@@ -30,7 +30,7 @@
|
||||
## 3. 保持读写命令交互一致
|
||||
|
||||
- `auth login`、`config set` 等写命令未传 `--config` 时修改当前激活项。
|
||||
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证验证成功并实际落盘时
|
||||
- `auth login --config <name>` 显式指定不存在的 Profile 时,仅在凭证实际落盘时
|
||||
创建和激活;`config set --config <name>` 可创建但不自动激活。
|
||||
- `config show`、`auth status` 和业务消费等读命令不得因为显式指定不存在的名称而创建 Profile。
|
||||
- `auth logout` 默认只清理当前激活项;显式 `--config` 只清理指定项。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli",
|
||||
"version": "1.18.0",
|
||||
"version": "1.18.1",
|
||||
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
|
||||
"keywords": [
|
||||
"agent",
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { mkdtempSync, rmSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { afterAll, describe, expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runCli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "bl");
|
||||
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
|
||||
|
||||
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
|
||||
|
||||
function runCliSmoke(args: string[]) {
|
||||
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
|
||||
}
|
||||
|
||||
describe("e2e: bl registry smoke", () => {
|
||||
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
|
||||
const { stderr, exitCode } = await runCli(["--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/\bbl\b/i);
|
||||
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
|
||||
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
|
||||
});
|
||||
|
||||
test("分组帮助按叶子命令展示不同鉴权域", async () => {
|
||||
const { stderr, exitCode } = await runCli(["app", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
|
||||
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
|
||||
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
|
||||
[["token-plan", "list-seats"], "AK/SK"],
|
||||
[["config", "show"], "No Auth"],
|
||||
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
|
||||
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain(`Authentication: ${authLabel}`);
|
||||
});
|
||||
|
||||
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/Global Flags:/);
|
||||
expect(stderr).toMatch(/--console-region <region>/);
|
||||
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
|
||||
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: bl ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-commands",
|
||||
"version": "1.18.0",
|
||||
"version": "1.18.1",
|
||||
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import {
|
||||
BailianError,
|
||||
ExitCode,
|
||||
chatPath,
|
||||
requestJson,
|
||||
normalizeModelBaseUrl,
|
||||
type AuthPersistPatch,
|
||||
type AuthStore,
|
||||
type Identity,
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
|
||||
|
||||
interface ApiKeyLoginDeps {
|
||||
identity: Identity;
|
||||
settings: Settings;
|
||||
authStore: AuthStore;
|
||||
}
|
||||
|
||||
interface ApiKeyLoginProfile {
|
||||
baseUrl: string;
|
||||
persistBaseUrl?: string;
|
||||
defaultTextModel?: string;
|
||||
defaultVideoModel?: string;
|
||||
@@ -30,65 +17,19 @@ interface ApiKeyLoginProfile {
|
||||
persistPatch?: AuthPersistPatch;
|
||||
}
|
||||
|
||||
const RETRY_DELAY_BASE_MS = 500;
|
||||
|
||||
function canRetry(error: unknown): boolean {
|
||||
if (error instanceof BailianError) {
|
||||
if (error.exitCode === ExitCode.NETWORK || error.exitCode === ExitCode.TIMEOUT) return true;
|
||||
const status = error.api?.httpStatus;
|
||||
return status === 401 || (status !== undefined && status >= 500);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return (
|
||||
error.name === "AbortError" ||
|
||||
error.name === "TimeoutError" ||
|
||||
error.message.includes("timed out") ||
|
||||
error.message === "fetch failed"
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function validateAndPersistApiKey(
|
||||
/**
|
||||
* Persist an API key (and optional profile defaults) without a live model probe.
|
||||
* Login is credential storage; connectivity is verified on the first API command.
|
||||
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
|
||||
*/
|
||||
export async function persistApiKey(
|
||||
deps: ApiKeyLoginDeps,
|
||||
key: string,
|
||||
profile: ApiKeyLoginProfile,
|
||||
): Promise<void> {
|
||||
process.stderr.write("Testing key... ");
|
||||
const httpDeps = { identity: deps.identity, settings: deps.settings };
|
||||
const baseUrl = normalizeModelBaseUrl(profile.baseUrl);
|
||||
const persistBaseUrl = profile.persistBaseUrl
|
||||
? normalizeModelBaseUrl(profile.persistBaseUrl)
|
||||
: undefined;
|
||||
const validationModel = "qwen3.8-max";
|
||||
const requestOpts = {
|
||||
url: baseUrl + chatPath(),
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${key}` },
|
||||
timeout: Math.min(deps.settings.timeout, 30),
|
||||
body: {
|
||||
model: validationModel,
|
||||
messages: [{ role: "user", content: "hi" }],
|
||||
max_tokens: 1,
|
||||
stream: false,
|
||||
},
|
||||
};
|
||||
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
try {
|
||||
await requestJson<unknown>(httpDeps, requestOpts);
|
||||
break;
|
||||
} catch (error) {
|
||||
if (attempt >= 3 || !canRetry(error)) {
|
||||
process.stderr.write("Failed\n");
|
||||
throw error;
|
||||
}
|
||||
const delayMs = RETRY_DELAY_BASE_MS * 2 ** (attempt - 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
process.stderr.write("Valid\n");
|
||||
await deps.authStore.login({
|
||||
...profile.persistPatch,
|
||||
api_key: key,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type Settings,
|
||||
} from "bailian-cli-core";
|
||||
import { listenLocalServer, openInBrowser } from "../shared/local-server.ts";
|
||||
import { validateAndPersistApiKey } from "./login-api-key.ts";
|
||||
import { persistApiKey } from "./login-api-key.ts";
|
||||
|
||||
/** 登录流程的能力面:身份(UA)、有效配置(timeout 等)、auth 域落盘。 */
|
||||
export interface LoginDeps {
|
||||
@@ -413,9 +413,7 @@ export async function runConsoleLogin(
|
||||
workspace_id: workspaceId || undefined,
|
||||
};
|
||||
if (apiKey) {
|
||||
const testBaseUrl = baseUrl || deps.authStore.resolveBaseUrl();
|
||||
await validateAndPersistApiKey(deps, apiKey, {
|
||||
baseUrl: testBaseUrl,
|
||||
await persistApiKey(deps, apiKey, {
|
||||
persistBaseUrl: baseUrl || undefined,
|
||||
persistPatch: callbackPatch,
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
normalizeModelBaseUrl,
|
||||
} from "bailian-cli-core";
|
||||
import { emitBare } from "bailian-cli-runtime";
|
||||
import { validateAndPersistApiKey } from "./login-api-key.ts";
|
||||
import { persistApiKey } from "./login-api-key.ts";
|
||||
import { resolveConsoleOrigin, runConsoleLogin } from "./login-console.ts";
|
||||
|
||||
const LOGIN_MODE_HINT = "Choose exactly one login mode: --api-key, --console, or --open-api";
|
||||
@@ -33,8 +33,8 @@ export default defineCommand({
|
||||
type: "string",
|
||||
valueHint: "<url>",
|
||||
description: {
|
||||
"en-US": "Model API base URL (used with --api-key for validation)",
|
||||
"zh-CN": "模型 API Base URL(用于配合 --api-key 进行验证)",
|
||||
"en-US": "Model API base URL to store with --api-key",
|
||||
"zh-CN": "与 --api-key 一并保存的模型 API Base URL",
|
||||
},
|
||||
},
|
||||
console: {
|
||||
@@ -164,19 +164,17 @@ export default defineCommand({
|
||||
if (!key) return;
|
||||
|
||||
if (settings.dryRun) {
|
||||
emitBare("Would validate and save API key.");
|
||||
emitBare("Would save API key.");
|
||||
return;
|
||||
}
|
||||
const profilePreset = getModelProfilePreset(settings.configName);
|
||||
const stored = store.stored();
|
||||
const storedBaseUrl = stored.baseUrl;
|
||||
const resolvedBaseUrl = baseUrl || store.resolveBaseUrl(profilePreset?.baseUrl);
|
||||
const persistBaseUrl = baseUrl || (!storedBaseUrl ? profilePreset?.baseUrl : undefined);
|
||||
const apiKeyCapabilities = profilePreset
|
||||
? [...new Set([...(stored.apiKeyCapabilities ?? []), ...profilePreset.apiKeyCapabilities])]
|
||||
: stored.apiKeyCapabilities;
|
||||
await validateAndPersistApiKey(deps, key, {
|
||||
baseUrl: resolvedBaseUrl,
|
||||
await persistApiKey(deps, key, {
|
||||
persistBaseUrl,
|
||||
defaultTextModel: profilePreset?.defaultTextModel,
|
||||
defaultVideoModel: profilePreset?.defaultVideoModel,
|
||||
@@ -187,5 +185,6 @@ export default defineCommand({
|
||||
defaultSpeechRecognitionModel: profilePreset?.defaultSpeechRecognitionModel,
|
||||
apiKeyCapabilities,
|
||||
});
|
||||
process.stderr.write(`API key saved to ${store.path}\n`);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
fileId: {
|
||||
@@ -8,14 +8,28 @@ const DELETE_FLAGS = {
|
||||
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--file-id <id>",
|
||||
usageArgs: "--file-id <id> [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--file-id file-id-xxx", "--file-id file-id-xxx --dry-run"],
|
||||
exampleArgs: [
|
||||
"--file-id file-id-xxx",
|
||||
"--file-id file-id-xxx --dry-run",
|
||||
"--file-id file-id-xxx --yes",
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the dataset file is permanently removed.",
|
||||
"zh-CN": "该操作不可撤销——数据集文件将被永久删除。",
|
||||
},
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const fileId = flags.fileId;
|
||||
@@ -25,6 +39,11 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteDataset(ctx.client, fileId);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
ExitCode,
|
||||
type FlagsDef,
|
||||
} from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
deployedModel: {
|
||||
@@ -25,6 +25,10 @@ const DELETE_FLAGS = {
|
||||
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
/**
|
||||
@@ -40,9 +44,19 @@ export default defineCommand({
|
||||
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck]",
|
||||
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--deployed-model dep-...", "--deployed-model dep-... --dry-run"],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the deployment is permanently destroyed.",
|
||||
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
|
||||
},
|
||||
],
|
||||
exampleArgs: [
|
||||
"--deployed-model dep-...",
|
||||
"--deployed-model dep-... --dry-run",
|
||||
"--deployed-model dep-... --yes",
|
||||
],
|
||||
async run(ctx) {
|
||||
const { settings, flags } = ctx;
|
||||
const deployedModel = flags.deployedModel;
|
||||
@@ -73,6 +87,11 @@ export default defineCommand({
|
||||
}
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteDeployment(ctx.client, deployedModel);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
|
||||
import { emitResult, emitBare } from "bailian-cli-runtime";
|
||||
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
|
||||
const DELETE_FLAGS = {
|
||||
jobId: {
|
||||
@@ -8,15 +8,23 @@ const DELETE_FLAGS = {
|
||||
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID(必填)" },
|
||||
required: true,
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
export default defineCommand({
|
||||
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
|
||||
auth: "apiKey",
|
||||
usageArgs: "--job-id <id>",
|
||||
usageArgs: "--job-id <id> [--yes]",
|
||||
flags: DELETE_FLAGS,
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
|
||||
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
|
||||
notes: [
|
||||
{
|
||||
"en-US": "Irreversible — the job record is permanently removed.",
|
||||
"zh-CN": "该操作不可撤销——任务记录将被永久删除。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.",
|
||||
@@ -32,6 +40,11 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmDangerousAction(
|
||||
`Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
|
||||
const response = await deleteFineTune(ctx.client, jobId);
|
||||
|
||||
if (settings.quiet) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
|
||||
import { emitResult } from "bailian-cli-runtime";
|
||||
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";
|
||||
import { formatNumber } from "../shared/format.ts";
|
||||
|
||||
const MINUTE_SECONDS = 60;
|
||||
@@ -10,7 +10,7 @@ export default defineCommand({
|
||||
"zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置",
|
||||
},
|
||||
auth: "apiKey",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete]",
|
||||
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]",
|
||||
flags: {
|
||||
model: {
|
||||
type: "string",
|
||||
@@ -41,11 +41,19 @@ export default defineCommand({
|
||||
"zh-CN": "清除该模型的所有自定义限流配置",
|
||||
},
|
||||
},
|
||||
yes: {
|
||||
type: "switch",
|
||||
description: {
|
||||
"en-US": "Skip the confirmation prompt for --delete",
|
||||
"zh-CN": "使用 --delete 时跳过确认提示",
|
||||
},
|
||||
},
|
||||
},
|
||||
exampleArgs: [
|
||||
"--model qwen-plus --rpm 60 --tpm 100000",
|
||||
"--model qwen3-max --tpm 500000",
|
||||
"--model qwen-plus --delete",
|
||||
"--model qwen-plus --delete --yes",
|
||||
"--model qwen-plus --rpm 60 --output json",
|
||||
],
|
||||
notes: [
|
||||
@@ -55,6 +63,10 @@ export default defineCommand({
|
||||
"zh-CN":
|
||||
"未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。",
|
||||
},
|
||||
{
|
||||
"en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.",
|
||||
"zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。",
|
||||
},
|
||||
{
|
||||
"en-US":
|
||||
"Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.",
|
||||
@@ -98,6 +110,13 @@ export default defineCommand({
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.delete) {
|
||||
await confirmDangerousAction(
|
||||
`Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`,
|
||||
flags.yes ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
const result = await ctx.client.requestJson<{ request_id?: string }>({
|
||||
path: modelsLimitsPath(),
|
||||
method: "POST",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { ADVISOR_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: advisor recommend", () => {
|
||||
test("advisor recommend --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(ADVISOR_ROUTES, [
|
||||
"advisor",
|
||||
"recommend",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import http from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
@@ -9,60 +7,16 @@ import {
|
||||
isOpenApiE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { AUTH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
interface ValidationServer {
|
||||
baseUrl: string;
|
||||
requests: Array<{
|
||||
path: string;
|
||||
body: Record<string, unknown>;
|
||||
authorization?: string;
|
||||
sourceConfig?: string;
|
||||
openApiSource?: string;
|
||||
}>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
async function startValidationServer(statusCode = 200): Promise<ValidationServer> {
|
||||
const requests: ValidationServer["requests"] = [];
|
||||
const server = http.createServer((request, response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk: Buffer) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
const rawBody = Buffer.concat(chunks).toString("utf8");
|
||||
requests.push({
|
||||
path: request.url ?? "",
|
||||
body: rawBody ? (JSON.parse(rawBody) as Record<string, unknown>) : {},
|
||||
authorization: request.headers.authorization,
|
||||
sourceConfig: request.headers["x-dashscope-source-config"] as string | undefined,
|
||||
openApiSource: request.headers["x-dashscope-openapisource"] as string | undefined,
|
||||
});
|
||||
response.writeHead(statusCode, { "Content-Type": "application/json" });
|
||||
if (statusCode >= 400) {
|
||||
response.end(JSON.stringify({ code: "InvalidApiKey", message: "invalid key" }));
|
||||
return;
|
||||
}
|
||||
response.end(
|
||||
JSON.stringify({ choices: [{ message: { role: "assistant", content: "ok" } }] }),
|
||||
);
|
||||
});
|
||||
});
|
||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address() as AddressInfo;
|
||||
return {
|
||||
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||
requests,
|
||||
close: () => new Promise<void>((resolve) => server.close(() => resolve())),
|
||||
};
|
||||
}
|
||||
|
||||
/** Auth E2E:本地参数/持久化契约默认执行;真实鉴权请求按对应 readiness gate 执行。 */
|
||||
|
||||
describe("e2e: auth", () => {
|
||||
test("auth login --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "login", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/login|api-key/i);
|
||||
expect(stderr).toMatch(/--console-site/);
|
||||
@@ -155,13 +109,13 @@ describe("e2e: auth", () => {
|
||||
});
|
||||
|
||||
test("auth logout --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "logout", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/logout|dry-run|yes/i);
|
||||
});
|
||||
|
||||
test("auth status --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(AUTH_ROUTES, ["auth", "status", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/status|output/i);
|
||||
});
|
||||
@@ -172,7 +126,7 @@ describe("e2e: auth", () => {
|
||||
expect(stderr).toMatch(/Choose exactly one login mode/);
|
||||
});
|
||||
|
||||
test("auth login --dry-run --api-key 不发起校验与落盘", async () => {
|
||||
test("auth login --dry-run --api-key 不发起落盘", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [
|
||||
"auth",
|
||||
"login",
|
||||
@@ -181,7 +135,7 @@ describe("e2e: auth", () => {
|
||||
"sk-e2e-dry-run-placeholder",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("Would validate and save API key.");
|
||||
expect(stdout).toContain("Would save API key.");
|
||||
});
|
||||
|
||||
test("auth login --dry-run 仍校验显式 Base URL", async () => {
|
||||
@@ -198,104 +152,83 @@ describe("e2e: auth", () => {
|
||||
expect(stderr).toMatch(/Invalid model base URL/);
|
||||
});
|
||||
|
||||
test("auth login --api-key 验证后原子保存凭证和 Base URL", async () => {
|
||||
const validationServer = await startValidationServer();
|
||||
test("auth login --api-key 原子保存凭证和 Base URL(无联网探测)", async () => {
|
||||
const configDir = makeE2eOutputDir("auth-api-key-login");
|
||||
const sdkBaseUrl = `${validationServer.baseUrl}/compatible-mode/v1/?source=login#fragment`;
|
||||
try {
|
||||
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(validationServer.requests).toHaveLength(1);
|
||||
expect(validationServer.requests[0]).toMatchObject({
|
||||
path: "/compatible-mode/v1/chat/completions",
|
||||
authorization: "Bearer sk-e2e-placeholder",
|
||||
sourceConfig: expect.any(String),
|
||||
openApiSource: "BailianCLI",
|
||||
body: {
|
||||
model: "qwen3.8-max",
|
||||
stream: false,
|
||||
},
|
||||
});
|
||||
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(validationServer.baseUrl);
|
||||
} finally {
|
||||
await validationServer.close();
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
test("auth login --config token-plan 接受 Anthropic SDK Base URL", async () => {
|
||||
const validationServer = await startValidationServer();
|
||||
const configDir = makeE2eOutputDir("auth-token-plan-anthropic-base-url");
|
||||
try {
|
||||
const login = await runCommandE2e(
|
||||
AUTH_ROUTES,
|
||||
[
|
||||
"auth",
|
||||
"login",
|
||||
"--config",
|
||||
"token-plan",
|
||||
"--api-key",
|
||||
"sk-sp-e2e-placeholder",
|
||||
"--base-url",
|
||||
`${validationServer.baseUrl}/apps/anthropic?source=sdk#fragment`,
|
||||
],
|
||||
{
|
||||
BAILIAN_CONFIG_DIR: configDir,
|
||||
DASHSCOPE_API_KEY: "",
|
||||
DASHSCOPE_BASE_URL: "",
|
||||
},
|
||||
);
|
||||
expect(login.exitCode, login.stderr).toBe(0);
|
||||
expect(validationServer.requests).toHaveLength(1);
|
||||
expect(validationServer.requests[0].path).toBe("/compatible-mode/v1/chat/completions");
|
||||
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: validationServer.baseUrl,
|
||||
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",
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
await validationServer.close();
|
||||
}
|
||||
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",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("auth login --config token-plan 追加新版 capability preset 且不删除已有能力", async () => {
|
||||
const validationServer = await startValidationServer();
|
||||
const configDir = makeE2eOutputDir("auth-token-plan-preset-login");
|
||||
writeFileSync(
|
||||
join(configDir, "config.json"),
|
||||
@@ -315,72 +248,56 @@ describe("e2e: auth", () => {
|
||||
) + "\n",
|
||||
);
|
||||
|
||||
try {
|
||||
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: validationServer.baseUrl,
|
||||
},
|
||||
);
|
||||
expect(login.exitCode, login.stderr).toBe(0);
|
||||
expect(validationServer.requests).toHaveLength(1);
|
||||
expect(validationServer.requests[0]).toMatchObject({
|
||||
path: "/compatible-mode/v1/chat/completions",
|
||||
authorization: "Bearer sk-sp-e2e-placeholder",
|
||||
sourceConfig: expect.any(String),
|
||||
openApiSource: "BailianCLI",
|
||||
body: {
|
||||
model: "qwen3.8-max",
|
||||
stream: false,
|
||||
},
|
||||
});
|
||||
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(
|
||||
validationServer.baseUrl,
|
||||
);
|
||||
expect((config["token-plan"] as Record<string, unknown>).api_key).not.toBe(
|
||||
"sk-env-must-not-be-persisted",
|
||||
);
|
||||
} finally {
|
||||
await validationServer.close();
|
||||
}
|
||||
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",
|
||||
);
|
||||
});
|
||||
|
||||
test("auth login --config token-plan 为显式空白名单追加 capability preset", async () => {
|
||||
const validationServer = await startValidationServer();
|
||||
const configDir = makeE2eOutputDir("auth-token-plan-empty-capabilities-login");
|
||||
writeFileSync(
|
||||
join(configDir, "config.json"),
|
||||
@@ -395,51 +312,46 @@ describe("e2e: auth", () => {
|
||||
) + "\n",
|
||||
);
|
||||
|
||||
try {
|
||||
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: validationServer.baseUrl,
|
||||
},
|
||||
);
|
||||
expect(login.exitCode, login.stderr).toBe(0);
|
||||
expect(validationServer.requests).toHaveLength(1);
|
||||
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",
|
||||
],
|
||||
});
|
||||
} finally {
|
||||
await validationServer.close();
|
||||
}
|
||||
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",
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("auth login 未传 --config 时写当前激活 Config", async () => {
|
||||
const validationServer = await startValidationServer();
|
||||
const configDir = makeE2eOutputDir("auth-active-profile-login");
|
||||
const storedBaseUrl = "https://dev-profile.example.test";
|
||||
writeFileSync(
|
||||
join(configDir, "config.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
active_config: "dev",
|
||||
dev: { base_url: validationServer.baseUrl },
|
||||
dev: { base_url: storedBaseUrl },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -451,60 +363,48 @@ describe("e2e: auth", () => {
|
||||
DASHSCOPE_API_KEY: "",
|
||||
DASHSCOPE_BASE_URL: "",
|
||||
};
|
||||
try {
|
||||
const activeLogin = await runCommandE2e(
|
||||
AUTH_ROUTES,
|
||||
["auth", "login", "--api-key", "sk-active-placeholder"],
|
||||
env,
|
||||
);
|
||||
expect(activeLogin.exitCode, activeLogin.stderr).toBe(0);
|
||||
const activeLogin = await runCommandE2e(
|
||||
AUTH_ROUTES,
|
||||
["auth", "login", "--api-key", "sk-active-placeholder"],
|
||||
env,
|
||||
);
|
||||
expect(activeLogin.exitCode, activeLogin.stderr).toBe(0);
|
||||
|
||||
expect(validationServer.requests).toHaveLength(1);
|
||||
|
||||
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: validationServer.baseUrl,
|
||||
});
|
||||
} finally {
|
||||
await validationServer.close();
|
||||
}
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
test("auth login --api-key 验证失败不留下半配置", async () => {
|
||||
const validationServer = await startValidationServer(400);
|
||||
test("auth login --api-key 非法 Base URL 失败时不留下半配置", async () => {
|
||||
const configDir = makeE2eOutputDir("auth-api-key-login-failure");
|
||||
try {
|
||||
const login = await runCommandE2e(
|
||||
AUTH_ROUTES,
|
||||
[
|
||||
"auth",
|
||||
"login",
|
||||
"--config",
|
||||
"failed-profile",
|
||||
"--api-key",
|
||||
"sk-invalid",
|
||||
"--base-url",
|
||||
validationServer.baseUrl,
|
||||
],
|
||||
{
|
||||
BAILIAN_CONFIG_DIR: configDir,
|
||||
DASHSCOPE_API_KEY: "",
|
||||
DASHSCOPE_BASE_URL: "",
|
||||
},
|
||||
);
|
||||
expect(login.exitCode).not.toBe(0);
|
||||
expect(login.stderr).toMatch(/invalid key/);
|
||||
expect(login.stderr).not.toMatch(/API key validation failed|Invalid API key/);
|
||||
expect(existsSync(join(configDir, "config.json"))).toBe(false);
|
||||
} finally {
|
||||
await validationServer.close();
|
||||
}
|
||||
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);
|
||||
});
|
||||
|
||||
test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => {
|
||||
@@ -520,7 +420,7 @@ describe("e2e: auth", () => {
|
||||
"120",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("Would validate and save API key.");
|
||||
expect(stdout).toContain("Would save API key.");
|
||||
});
|
||||
|
||||
test("auth login 缺少密钥且 --output json 时报用法错误并退出 (2)", async () => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { captureCommandHelp, runCommandHelp } from "./helpers.ts";
|
||||
import { QUOTA_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
test("captureCommandHelp 用 topic 最小路由渲染真实命令帮助", async () => {
|
||||
const helpOutput = await captureCommandHelp(QUOTA_ROUTES, ["quota", "list"]);
|
||||
|
||||
expect(helpOutput).toContain("Usage: bl quota list");
|
||||
expect(helpOutput).toContain("--model <model>");
|
||||
expect(helpOutput).toContain("--name <name>");
|
||||
expect(helpOutput).toContain("--output <format>");
|
||||
});
|
||||
|
||||
test("runCommandHelp 保持 commands E2E 的 stderr 与 exitCode 断言接口", async () => {
|
||||
const result = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.stdout).toBe("");
|
||||
expect(result.stderr).toContain("Usage: bl quota list");
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { CONFIG_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -11,29 +11,29 @@ import { CONFIG_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: config", () => {
|
||||
test("config show --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "show", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "show", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/show|config/i);
|
||||
});
|
||||
|
||||
test("config set --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "set", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/set|--key|--value/i);
|
||||
});
|
||||
|
||||
test("config list/use --help 正常退出", async () => {
|
||||
const listResult = await runCommandE2e(CONFIG_ROUTES, ["config", "list", "--help"]);
|
||||
const listResult = await runCommandHelp(CONFIG_ROUTES, ["config", "list", "--help"]);
|
||||
expect(listResult.exitCode, listResult.stderr).toBe(0);
|
||||
expect(listResult.stderr).toMatch(/list|active|profile/i);
|
||||
|
||||
const useResult = await runCommandE2e(CONFIG_ROUTES, ["config", "use", "--help"]);
|
||||
const useResult = await runCommandHelp(CONFIG_ROUTES, ["config", "use", "--help"]);
|
||||
expect(useResult.exitCode, useResult.stderr).toBe(0);
|
||||
expect(useResult.stderr).toMatch(/use|--name|active/i);
|
||||
});
|
||||
|
||||
test("config ui --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "ui", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "ui", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/ui|--port|--no-open|web/i);
|
||||
});
|
||||
@@ -464,7 +464,7 @@ describe("e2e: config", () => {
|
||||
});
|
||||
|
||||
test("config agent --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "agent", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(CONFIG_ROUTES, ["config", "agent", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/agent|--base-url|--model/i);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { CONSOLE_FLAGS_DRY_RUN_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
type ConsoleDryRunMeta = {
|
||||
@@ -10,7 +10,7 @@ type ConsoleDryRunMeta = {
|
||||
|
||||
describe("e2e: console global flags (dry-run)", () => {
|
||||
test("console call --help 不暴露命令级 region/site,示例使用 --console-region", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
"console",
|
||||
"call",
|
||||
"--help",
|
||||
@@ -24,7 +24,7 @@ describe("e2e: console global flags (dry-run)", () => {
|
||||
});
|
||||
|
||||
test("auth login --help:自有 flag 含 --console-site,不含其余 console 域 flag", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(CONSOLE_FLAGS_DRY_RUN_ROUTES, [
|
||||
"auth",
|
||||
"login",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { join } from "path";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
|
||||
import {
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
e2eFixturesDir,
|
||||
} from "./helpers.ts";
|
||||
import { DATASET_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -18,7 +24,7 @@ import { DATASET_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
|
||||
test("dataset upload --help 正常退出并展示 --file", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"upload",
|
||||
"--help",
|
||||
@@ -286,6 +292,42 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => {
|
||||
expect(exitCode, stdout + stderr).not.toBe(0);
|
||||
expect(`${stdout}\n${stderr}`).toMatch(/--schema video is not supported/);
|
||||
});
|
||||
|
||||
test("dataset delete --help 展示 --yes", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test("dataset delete --dry-run 发出结构化动作", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--file-id",
|
||||
"file-id-xxx",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||||
expect(data.action).toBe("dataset.delete");
|
||||
});
|
||||
|
||||
test("dataset delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [
|
||||
"dataset",
|
||||
"delete",
|
||||
"--file-id",
|
||||
"file-id-xxx",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { DEPLOY_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -16,14 +16,13 @@ import { DEPLOY_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
|
||||
test("deploy 列出子命令", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, ["deploy", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/create|list|get|delete|update|scale|models/);
|
||||
expect(stderr).toMatch(/create|list|get|delete|update|scale|models/);
|
||||
});
|
||||
|
||||
test("deploy create --help 正常退出并展示必填项", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, [
|
||||
"deploy",
|
||||
"text",
|
||||
"create",
|
||||
@@ -202,6 +201,25 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
|
||||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||||
expect(data.action).toBe(`deploy.${sub}`);
|
||||
});
|
||||
|
||||
test("deploy delete --help 展示 --yes", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy", "delete", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test("deploy delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
// --skip-precheck 保证确认门在发任何网络请求前触发
|
||||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||||
"deploy",
|
||||
"delete",
|
||||
"--deployed-model",
|
||||
"dep-xxx",
|
||||
"--skip-precheck",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { join } from "path";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
|
||||
import {
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
e2eFixturesDir,
|
||||
} from "./helpers.ts";
|
||||
import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -9,7 +15,7 @@ import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: file upload", () => {
|
||||
test("file upload --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FILE_UPLOAD_ROUTES, [
|
||||
"file",
|
||||
"upload",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { join } from "path";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts";
|
||||
import {
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
e2eFixturesDir,
|
||||
} from "./helpers.ts";
|
||||
import { FINETUNE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -17,14 +23,15 @@ import { FINETUNE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
test("finetune 列出子命令", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, ["finetune"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, ["finetune", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/);
|
||||
expect(stderr).toMatch(
|
||||
/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/,
|
||||
);
|
||||
});
|
||||
|
||||
test("finetune create --help 正常退出并展示必填项", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"text",
|
||||
"create",
|
||||
@@ -287,6 +294,27 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
expect(data.action).toBe(`finetune.${sub}`);
|
||||
});
|
||||
|
||||
test("finetune delete --help 展示 --yes", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"delete",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--yes/i);
|
||||
});
|
||||
|
||||
test("finetune delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"delete",
|
||||
"--job-id",
|
||||
"ft-xxx",
|
||||
]);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
|
||||
test("finetune create --dry-run 解析多 datasets 中的空白", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
@@ -336,7 +364,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
test("finetune audio create --help 不暴露文本超参 flag", async () => {
|
||||
// Audio models don't consume --training-type / --n-epochs / --batch-size /
|
||||
// --learning-rate / --max-length, so those flags are not offered.
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"audio",
|
||||
"create",
|
||||
@@ -369,7 +397,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => {
|
||||
test("finetune video create --help 暴露视频超参且不含文本超参", async () => {
|
||||
// Video exposes --n-epochs / --batch-size / --learning-rate; the text-only
|
||||
// --training-type / --max-length surface is not offered.
|
||||
const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(FINETUNE_ROUTES, [
|
||||
"finetune",
|
||||
"video",
|
||||
"create",
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
} from "e2e/output";
|
||||
import { captureRegistryHelp } from "e2e/registry-smoke";
|
||||
import { runNodeMain, type RunCliResult } from "e2e/runner";
|
||||
import type { AnyCommand } from "bailian-cli-core";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import type { E2eRouteExports } from "./topic-routes.ts";
|
||||
|
||||
export {
|
||||
@@ -42,6 +45,7 @@ export {
|
||||
|
||||
const e2eDir = dirname(fileURLToPath(import.meta.url));
|
||||
const harnessMainTs = join(e2eDir, "harness", "main.ts");
|
||||
const registryCache = new WeakMap<E2eRouteExports, Promise<CommandRegistry>>();
|
||||
|
||||
/** `packages/commands` 根目录 */
|
||||
export const commandsPackageRoot = join(e2eDir, "..", "..");
|
||||
@@ -55,6 +59,51 @@ function serializeRoutes(routes: E2eRouteExports): string {
|
||||
);
|
||||
}
|
||||
|
||||
function getCommandRegistry(routes: E2eRouteExports): Promise<CommandRegistry> {
|
||||
const cachedRegistry = registryCache.get(routes);
|
||||
if (cachedRegistry) return cachedRegistry;
|
||||
|
||||
const registryPromise = import("bailian-cli-commands").then((commandExports) => {
|
||||
const commandLibrary = commandExports as Record<string, unknown>;
|
||||
const commands: Record<string, AnyCommand> = {};
|
||||
for (const [path, exportName] of Object.entries(routes)) {
|
||||
const command = commandLibrary[exportName];
|
||||
if (typeof command !== "object" || command === null || !("run" in command)) {
|
||||
throw new Error(`Unknown bailian-cli-commands export: ${exportName}`);
|
||||
}
|
||||
commands[path] = command as AnyCommand;
|
||||
}
|
||||
return new CommandRegistry(commands, "bl");
|
||||
});
|
||||
registryCache.set(routes, registryPromise);
|
||||
return registryPromise;
|
||||
}
|
||||
|
||||
/** 使用 topic 最小路由在当前 Vitest worker 中渲染命令 help,不启动 CLI 子进程。 */
|
||||
export async function captureCommandHelp(
|
||||
routes: E2eRouteExports,
|
||||
commandPath: string[],
|
||||
): Promise<string> {
|
||||
const registry = await getCommandRegistry(routes);
|
||||
const resolution = resolve([...commandPath, "--help"], registry);
|
||||
if (resolution.kind !== "help" || resolution.path.join(" ") !== commandPath.join(" ")) {
|
||||
throw new Error(`Command help did not resolve: ${commandPath.join(" ")}`);
|
||||
}
|
||||
return captureRegistryHelp(registry, commandPath);
|
||||
}
|
||||
|
||||
/** 与 runCommandE2e 的结果结构兼容,但 help 在当前 Vitest worker 中渲染。 */
|
||||
export async function runCommandHelp(
|
||||
routes: E2eRouteExports,
|
||||
args: string[],
|
||||
): Promise<RunCliResult> {
|
||||
if (args.at(-1) !== "--help") {
|
||||
throw new Error("runCommandHelp requires --help as the final argument");
|
||||
}
|
||||
const stderr = await captureCommandHelp(routes, args.slice(0, -1));
|
||||
return { stdout: "", stderr, exitCode: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 harness 子进程执行命令 E2E。
|
||||
* `routes` 为本用例所需的最小 path → export 映射,不维护全量产品 map。
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
@@ -18,7 +19,7 @@ import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: image edit", () => {
|
||||
test("image edit --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "edit", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(IMAGE_ROUTES, ["image", "edit", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/edit|--image|--prompt|--async|--concurrent/i);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
@@ -19,7 +20,11 @@ import { IMAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: image generate", () => {
|
||||
test("image generate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "generate", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(IMAGE_ROUTES, [
|
||||
"image",
|
||||
"generate",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/generate|--prompt|--model/i);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isChatE2EReady,
|
||||
isMultimodalChatE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import { KNOWLEDGE_CHAT_ROUTES } from "../topic-routes.ts";
|
||||
@@ -31,7 +32,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge chat", () => {
|
||||
test("knowledge chat --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHAT_ROUTES, [
|
||||
"knowledge",
|
||||
"chat",
|
||||
"--help",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isOssImportE2EReady,
|
||||
isTableKbE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import {
|
||||
@@ -47,7 +48,7 @@ const HELP_SMOKE_CASES: Array<[string[], RegExp]> = [
|
||||
describe("e2e: chunk/category/file 全命令 --help 冒烟", () => {
|
||||
test("12 条命令 help 退出 0 且展示代表性 flag(chunk add 单独测)", async () => {
|
||||
for (const [commandPath, flagPattern] of HELP_SMOKE_CASES) {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
...commandPath,
|
||||
"--help",
|
||||
]);
|
||||
@@ -59,7 +60,7 @@ describe("e2e: chunk/category/file 全命令 --help 冒烟", () => {
|
||||
|
||||
describe("e2e: knowledge chunk 组 (静态)", () => {
|
||||
test("chunk add: --help 展示双通道 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_CHUNK_CATEGORY_FILE_ROUTES, [
|
||||
"knowledge",
|
||||
"chunk",
|
||||
"add",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_DELETE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc delete", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_DELETE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"delete",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_LIST_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc list", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_LIST_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_LIST_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"list",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
// Live coverage depends on a real job_id produced by doc upload; it is exercised
|
||||
@@ -10,7 +10,7 @@ import { KNOWLEDGE_DOC_STATUS_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc status", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_STATUS_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_STATUS_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"status",
|
||||
|
||||
@@ -2,12 +2,12 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_TAG_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge doc tag", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_TAG_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"tag",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_DOC_UPLOAD_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
const fixtureDir = mkdtempSync(join(tmpdir(), "doc-upload-e2e-"));
|
||||
@@ -17,7 +17,7 @@ interface DryRunStep {
|
||||
|
||||
describe("e2e: knowledge doc upload", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_DOC_UPLOAD_ROUTES, [
|
||||
"knowledge",
|
||||
"doc",
|
||||
"upload",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_CREATE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
interface DryRunBody {
|
||||
@@ -17,7 +17,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge kb create", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_CREATE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_CREATE_ROUTES, [
|
||||
"knowledge",
|
||||
"create",
|
||||
"--help",
|
||||
|
||||
@@ -4,13 +4,13 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { basename, join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { deleteKbWithRetry } from "./journeys/journey-helpers.ts";
|
||||
import { KNOWLEDGE_KB_DELETE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge kb delete", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_DELETE_ROUTES, [
|
||||
"knowledge",
|
||||
"delete",
|
||||
"--help",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_INFO_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge kb info", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_INFO_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_INFO_ROUTES, [
|
||||
"knowledge",
|
||||
"info",
|
||||
"--help",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_LIST_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
interface DryRunBody {
|
||||
@@ -9,7 +9,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge kb list", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_LIST_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_LIST_ROUTES, [
|
||||
"knowledge",
|
||||
"list",
|
||||
"--help",
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_KB_UPDATE_ROUTES } from "../topic-routes.ts";
|
||||
|
||||
describe("e2e: knowledge kb update", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_KB_UPDATE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_KB_UPDATE_ROUTES, [
|
||||
"knowledge",
|
||||
"update",
|
||||
"--help",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
isSearchE2EReady,
|
||||
isTableSearchE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import { KNOWLEDGE_SEARCH_ROUTES } from "../topic-routes.ts";
|
||||
@@ -20,7 +21,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge search", () => {
|
||||
test("knowledge search --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_SEARCH_ROUTES, [
|
||||
"knowledge",
|
||||
"search",
|
||||
"--help",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { mkdtempSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandE2e } from "../helpers.ts";
|
||||
import { isKbAdminE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "../helpers.ts";
|
||||
import { KNOWLEDGE_SERVICE_ROUTES } from "../topic-routes.ts";
|
||||
import { pickDifferentAgentModel } from "./verified-models.ts";
|
||||
|
||||
@@ -16,7 +16,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge service list", () => {
|
||||
test("--help 展示 flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_SERVICE_ROUTES, [
|
||||
"knowledge",
|
||||
"service",
|
||||
"list",
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
isKbAdminE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "../helpers.ts";
|
||||
import { KNOWLEDGE_ROUTES } from "../topic-routes.ts";
|
||||
@@ -28,7 +29,7 @@ interface DryRunBody {
|
||||
|
||||
describe("e2e: knowledge retrieve", () => {
|
||||
test("knowledge retrieve --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(KNOWLEDGE_ROUTES, [
|
||||
"knowledge",
|
||||
"retrieve",
|
||||
"--help",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { e2eFixturesDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
const AGENTS_DEPLOYMENT_YAML = join(e2eFixturesDir, "managed-agent", "agents-deployment.yaml");
|
||||
@@ -123,7 +123,7 @@ describe("e2e: managed-agent", () => {
|
||||
});
|
||||
|
||||
test("managed-agent apply --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"apply",
|
||||
"--help",
|
||||
@@ -157,7 +157,7 @@ describe("e2e: managed-agent", () => {
|
||||
});
|
||||
|
||||
test("managed-agent skill-list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
|
||||
"managed-agent",
|
||||
"skill-list",
|
||||
"--help",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { MCP_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -18,25 +18,25 @@ import { MCP_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: mcp", () => {
|
||||
test("mcp list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/list|--name|--type|--page/i);
|
||||
});
|
||||
|
||||
test("mcp tools --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "tools", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "tools", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/tools|--server|--url/i);
|
||||
});
|
||||
|
||||
test("mcp call --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "call", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "call", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/call|--target|--arg|--json/i);
|
||||
});
|
||||
|
||||
test("mcp list --help 不暴露 --all 入口(市场全量已下线)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MCP_ROUTES, ["mcp", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).not.toMatch(/--all/);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isBailianE2EEnabled,
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { MEMORY_ROUTES } from "./topic-routes.ts";
|
||||
@@ -38,25 +39,29 @@ function memoryLibraryCliArgs(): string[] {
|
||||
|
||||
describe("e2e: memory", () => {
|
||||
test("memory add --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "add", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "add", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/add|--user-id|--content|messages/i);
|
||||
});
|
||||
|
||||
test("memory list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, ["memory", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/list|--user-id|memory-library/i);
|
||||
});
|
||||
|
||||
test("memory search --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "search", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
|
||||
"memory",
|
||||
"search",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/search|--query|user-id/i);
|
||||
});
|
||||
|
||||
test("memory profile create --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(MEMORY_ROUTES, [
|
||||
"memory",
|
||||
"profile",
|
||||
"create",
|
||||
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { OMNI_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: omni", () => {
|
||||
test("omni --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, ["omni", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(OMNI_ROUTES, ["omni", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/omni|--message|--audio|text-only/i);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { PERMISSION_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: permission", () => {
|
||||
test("permission list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"list",
|
||||
"--help",
|
||||
@@ -17,7 +17,7 @@ describe("e2e: permission", () => {
|
||||
});
|
||||
|
||||
test("permission grant --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"grant",
|
||||
"--help",
|
||||
@@ -29,7 +29,7 @@ describe("e2e: permission", () => {
|
||||
});
|
||||
|
||||
test("permission revoke --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PERMISSION_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PERMISSION_ROUTES, [
|
||||
"permission",
|
||||
"revoke",
|
||||
"--help",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { PIPELINE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: pipeline", () => {
|
||||
@@ -65,7 +65,7 @@ describe("e2e: pipeline", () => {
|
||||
});
|
||||
|
||||
test("pipeline run --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PIPELINE_ROUTES, [
|
||||
"pipeline",
|
||||
"run",
|
||||
"--help",
|
||||
@@ -76,7 +76,7 @@ describe("e2e: pipeline", () => {
|
||||
});
|
||||
|
||||
test("pipeline validate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(PIPELINE_ROUTES, [
|
||||
"pipeline",
|
||||
"validate",
|
||||
"--help",
|
||||
|
||||
@@ -4,20 +4,21 @@ import {
|
||||
isConsoleAuthFailure,
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { QUOTA_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: quota", () => {
|
||||
test("quota list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--name");
|
||||
});
|
||||
|
||||
test("quota list --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl quota list");
|
||||
expect(stderr).toContain("bl quota list --model qwen3-max");
|
||||
@@ -25,30 +26,31 @@ describe("e2e: quota", () => {
|
||||
});
|
||||
|
||||
test("quota update --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--rpm");
|
||||
expect(stderr).toContain("--tpm");
|
||||
expect(stderr).toContain("--delete");
|
||||
expect(stderr).toContain("--yes");
|
||||
});
|
||||
|
||||
test("quota request 作为 quota update 的兼容别名可用", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "request", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "request", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--rpm");
|
||||
expect(stderr).toContain("--delete");
|
||||
});
|
||||
|
||||
test("quota history --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "history", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "history", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--page");
|
||||
expect(stderr).toContain("--model");
|
||||
});
|
||||
|
||||
test("quota check --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "check", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(QUOTA_ROUTES, ["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--period");
|
||||
@@ -91,6 +93,17 @@ describe("e2e: quota", () => {
|
||||
expect(stderr).toContain("cannot be combined");
|
||||
});
|
||||
|
||||
test("quota update --delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||||
// 注入假 key 让 apiKey 鉴权通过;确认门在发任何网络请求前触发
|
||||
const { stderr, exitCode } = await runCommandE2e(
|
||||
QUOTA_ROUTES,
|
||||
["quota", "update", "--model", "qwen-plus", "--delete"],
|
||||
{ DASHSCOPE_API_KEY: "sk-e2e-quota-delete" },
|
||||
);
|
||||
expect(exitCode).toBe(2);
|
||||
expect(stderr).toMatch(/--yes/);
|
||||
});
|
||||
|
||||
test("quota update --rpm 负数报错", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [
|
||||
"quota",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { SEARCH_WEB_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> {
|
||||
@@ -17,7 +17,7 @@ function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?:
|
||||
|
||||
describe("e2e: search web", () => {
|
||||
test("search web --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SEARCH_WEB_ROUTES, [
|
||||
"search",
|
||||
"web",
|
||||
"--help",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { SKILL_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/** Canonical always-published skill; also the backbone of advisor wiki sync */
|
||||
@@ -15,33 +15,33 @@ function makeTempConfigDir(): string {
|
||||
|
||||
describe("e2e: skill", () => {
|
||||
test("skill add --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "add", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "add", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--all/);
|
||||
expect(stderr).toMatch(/--name/);
|
||||
});
|
||||
|
||||
test("skill update --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--all/);
|
||||
expect(stderr).toMatch(/--name/);
|
||||
});
|
||||
|
||||
test("skill remove --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "remove", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "remove", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--name/);
|
||||
});
|
||||
|
||||
test("skill list --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "list", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/list|registry/i);
|
||||
});
|
||||
|
||||
test("skill init --help exits successfully", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SKILL_ROUTES, ["skill", "init", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(SKILL_ROUTES, ["skill", "init", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/bailian/i);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: speech list-voices", () => {
|
||||
test("speech synthesize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"synthesize",
|
||||
"--help",
|
||||
@@ -18,7 +18,7 @@ describe("e2e: speech list-voices", () => {
|
||||
});
|
||||
|
||||
test("speech recognize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--help",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
@@ -50,7 +51,7 @@ describe("e2e: speech recognize", () => {
|
||||
}
|
||||
|
||||
test("speech recognize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"recognize",
|
||||
"--help",
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
@@ -17,7 +18,7 @@ import { SPEECH_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: speech synthesize", () => {
|
||||
test("speech synthesize --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(SPEECH_ROUTES, [
|
||||
"speech",
|
||||
"synthesize",
|
||||
"--help",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { TEXT_CHAT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
/**
|
||||
@@ -8,7 +8,7 @@ import { TEXT_CHAT_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: text chat", () => {
|
||||
test("text chat --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--api\s+<chat\|responses>/i);
|
||||
expect(stderr).toMatch(/chat|--message|model|stream/i);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { makeE2eOutputDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
|
||||
import { makeE2eOutputDir, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { TOKEN_PLAN_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: token-plan", () => {
|
||||
test("token-plan help shows centralized OpenAPI auth flags", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(TOKEN_PLAN_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(TOKEN_PLAN_ROUTES, [
|
||||
"token-plan",
|
||||
"list-seats",
|
||||
"--help",
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { runCommandE2e } from "./helpers.ts";
|
||||
import { runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||||
import { UPDATE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: update", () => {
|
||||
test("update --help 正常退出并展示 --to", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(UPDATE_ROUTES, ["update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--to/);
|
||||
expect(stderr).toMatch(/<version>/);
|
||||
});
|
||||
|
||||
test("update --help 包含 --to 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(UPDATE_ROUTES, ["update", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(UPDATE_ROUTES, ["update", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--to 0.1.14");
|
||||
});
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
isConsoleAuthFailure,
|
||||
isConsoleE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage coding-plan", () => {
|
||||
test("usage coding-plan --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--help",
|
||||
@@ -19,7 +20,7 @@ describe("e2e: usage coding-plan", () => {
|
||||
});
|
||||
|
||||
test("usage coding-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"coding-plan",
|
||||
"--help",
|
||||
|
||||
@@ -3,19 +3,20 @@ import {
|
||||
isConsoleE2EReady,
|
||||
isConsoleAuthFailure,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage free", () => {
|
||||
test("usage free --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--model|quota|free-tier/i);
|
||||
});
|
||||
|
||||
test("usage free --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "free", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage free");
|
||||
expect(stderr).toContain("bl usage free --model qwen3-max");
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isConsoleE2EReady,
|
||||
isConsoleAuthFailure,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
@@ -42,13 +43,13 @@ async function fetchDefaultWorkspaceId(): Promise<string> {
|
||||
|
||||
describe("e2e: usage stats", () => {
|
||||
test("usage stats --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--model|--days|stats/i);
|
||||
});
|
||||
|
||||
test("usage stats --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage stats");
|
||||
expect(stderr).toContain("bl usage stats --model qwen-turbo");
|
||||
@@ -56,7 +57,7 @@ describe("e2e: usage stats", () => {
|
||||
});
|
||||
|
||||
test("usage stats --help 包含 --workspace-id 选项", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, ["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--workspace-id");
|
||||
});
|
||||
|
||||
@@ -3,13 +3,14 @@ import {
|
||||
isConsoleAuthFailure,
|
||||
isConsoleE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { USAGE_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: usage token-plan", () => {
|
||||
test("usage token-plan --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--help",
|
||||
@@ -19,7 +20,7 @@ describe("e2e: usage token-plan", () => {
|
||||
});
|
||||
|
||||
test("usage token-plan --help 包含 --output json 示例", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(USAGE_ROUTES, [
|
||||
"usage",
|
||||
"token-plan",
|
||||
"--help",
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -22,7 +23,11 @@ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001";
|
||||
|
||||
describe("e2e: video download", () => {
|
||||
test("video download --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "download", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"download",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/download|--task-id|--out/i);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -17,7 +18,7 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video edit", () => {
|
||||
test("video edit --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "edit", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, ["video", "edit", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/edit|--video|--prompt|model|--async|--concurrent/i);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -18,7 +19,11 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video generate (i2v)", () => {
|
||||
test("video generate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"generate",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/generate|--prompt|--image|model/i);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -17,7 +18,11 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video generate (t2v)", () => {
|
||||
test("video generate --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"generate",
|
||||
"--help",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/generate|--prompt|--model|download|image/i);
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isDashScopeE2EReady,
|
||||
makeE2eOutputDir,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -18,7 +19,7 @@ import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: video ref (r2v)", () => {
|
||||
test("video ref --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "ref", "--help"]);
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, ["video", "ref", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/ref|--prompt|--image|model|--async|--concurrent/i);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isBailianE2EEnabled,
|
||||
isDashScopeE2EReady,
|
||||
parseStdoutJson,
|
||||
runCommandHelp,
|
||||
runCommandE2e,
|
||||
} from "./helpers.ts";
|
||||
import { VIDEO_ROUTES } from "./topic-routes.ts";
|
||||
@@ -15,7 +16,7 @@ const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim();
|
||||
|
||||
describe("e2e: video task get", () => {
|
||||
test("video task get --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [
|
||||
const { stderr, exitCode } = await runCommandHelp(VIDEO_ROUTES, [
|
||||
"video",
|
||||
"task",
|
||||
"get",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-core",
|
||||
"version": "1.18.0",
|
||||
"version": "1.18.1",
|
||||
"description": "Core SDK for bailian-cli. See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -6,8 +6,8 @@ export function deriveGroupPaths(commandPaths: string[]): string[] {
|
||||
const groups = new Set<string>();
|
||||
for (const path of commandPaths) {
|
||||
const parts = path.split(" ");
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const prefix = parts.slice(0, i).join(" ");
|
||||
for (let partIndex = 1; partIndex < parts.length; partIndex++) {
|
||||
const prefix = parts.slice(0, partIndex).join(" ");
|
||||
const hasChildren = commandPaths.some(
|
||||
(candidate) => candidate.startsWith(`${prefix} `) && candidate !== prefix,
|
||||
);
|
||||
@@ -16,3 +16,21 @@ export function deriveGroupPaths(commandPaths: string[]): string[] {
|
||||
}
|
||||
return [...groups].sort();
|
||||
}
|
||||
|
||||
interface RegistryHelpPrinter {
|
||||
printHelp(commandPath: string[], output: NodeJS.WriteStream): void;
|
||||
}
|
||||
|
||||
export function captureRegistryHelp(printer: RegistryHelpPrinter, commandPath: string[]): string {
|
||||
let helpOutput = "";
|
||||
const output = {
|
||||
isTTY: false,
|
||||
write(chunk: string): boolean {
|
||||
helpOutput += chunk;
|
||||
return true;
|
||||
},
|
||||
} as NodeJS.WriteStream;
|
||||
|
||||
printer.printHelp(commandPath, output);
|
||||
return helpOutput;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { promisify } from "util";
|
||||
import { monorepoRoot } from "./monorepo-root.ts";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const tsxLoader = import.meta.resolve("tsx");
|
||||
|
||||
export interface RunCliResult {
|
||||
stdout: string;
|
||||
@@ -18,24 +19,28 @@ export function resolveTsxBin(): string {
|
||||
return join(root, "node_modules", ".bin", name);
|
||||
}
|
||||
|
||||
/** 子进程通过 tsx 执行指定 main.ts 入口 */
|
||||
/** 子进程通过 Node 的 tsx loader 执行指定 main.ts 入口,不启动 tsx CLI 的 IPC server。 */
|
||||
export async function runNodeMain(
|
||||
mainTs: string,
|
||||
args: string[],
|
||||
options: { cwd: string; env?: NodeJS.ProcessEnv } = { cwd: process.cwd() },
|
||||
): Promise<RunCliResult> {
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(resolveTsxBin(), [mainTs, ...args], {
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_NO_WARNINGS: "1",
|
||||
DO_NOT_TRACK: "1",
|
||||
...options.env,
|
||||
const { stdout, stderr } = await execFileAsync(
|
||||
process.execPath,
|
||||
["--import", tsxLoader, mainTs, ...args],
|
||||
{
|
||||
cwd: options.cwd,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 32 * 1024 * 1024,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_NO_WARNINGS: "1",
|
||||
DO_NOT_TRACK: "1",
|
||||
...options.env,
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 };
|
||||
} catch (err: unknown) {
|
||||
const e = err as {
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
interface FixtureOutput {
|
||||
args: string[];
|
||||
execArgv: string[];
|
||||
marker: string | null;
|
||||
}
|
||||
|
||||
if (process.argv[2] === "--fail") {
|
||||
process.stderr.write("fixture failure\n");
|
||||
process.exitCode = 7;
|
||||
} else {
|
||||
const output: FixtureOutput = {
|
||||
args: process.argv.slice(2),
|
||||
execArgv: process.execArgv,
|
||||
marker: process.env.RUNNER_FIXTURE_MARKER ?? null,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(output)}\n`);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { captureRegistryHelp } from "../src/registry-smoke.ts";
|
||||
|
||||
test("captureRegistryHelp 捕获指定命令路径的非 TTY 输出", () => {
|
||||
const requestedPaths: string[][] = [];
|
||||
const renderer = {
|
||||
printHelp(commandPath: string[], output: NodeJS.WriteStream): void {
|
||||
requestedPaths.push(commandPath);
|
||||
output.write(`Usage: bl ${commandPath.join(" ")}`);
|
||||
},
|
||||
};
|
||||
|
||||
const output = captureRegistryHelp(renderer, ["text", "chat"]);
|
||||
|
||||
expect(output).toBe("Usage: bl text chat");
|
||||
expect(requestedPaths).toEqual([["text", "chat"]]);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { expect, test } from "vite-plus/test";
|
||||
import { runNodeMain } from "../src/run-subprocess.ts";
|
||||
|
||||
const testsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const fixtureMainTs = join(testsDir, "fixtures", "echo-argv.ts");
|
||||
|
||||
test("runNodeMain 通过 Node 的 tsx loader 执行 TypeScript 入口", async () => {
|
||||
const result = await runNodeMain(fixtureMainTs, ["alpha", "beta"], {
|
||||
cwd: testsDir,
|
||||
env: { RUNNER_FIXTURE_MARKER: "forwarded" },
|
||||
});
|
||||
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
const output = JSON.parse(result.stdout) as {
|
||||
args: string[];
|
||||
execArgv: string[];
|
||||
marker: string | null;
|
||||
};
|
||||
expect(output.args).toEqual(["alpha", "beta"]);
|
||||
expect(output.marker).toBe("forwarded");
|
||||
const importFlagIndex = output.execArgv.indexOf("--import");
|
||||
expect(importFlagIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(output.execArgv[importFlagIndex + 1]).toMatch(/tsx/);
|
||||
});
|
||||
|
||||
test("runNodeMain 返回 TypeScript 入口的 stderr 与非零退出码", async () => {
|
||||
const result = await runNodeMain(fixtureMainTs, ["--fail"], { cwd: testsDir });
|
||||
|
||||
expect(result).toEqual({
|
||||
stdout: "",
|
||||
stderr: "fixture failure\n",
|
||||
exitCode: 7,
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "knowledge-studio-cli",
|
||||
"version": "1.18.0",
|
||||
"version": "1.18.1",
|
||||
"description": "Lightweight RAG CLI for Aliyun Model Studio — focused on knowledge-base retrieval.",
|
||||
"keywords": [
|
||||
"alibaba-cloud",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
|
||||
import { CommandRegistry, resolve } from "bailian-cli-runtime";
|
||||
import pkg from "../../package.json" with { type: "json" };
|
||||
import { commands } from "../../src/commands.ts";
|
||||
import { runKscli } from "./helpers.ts";
|
||||
|
||||
const commandPaths = Object.keys(commands).sort();
|
||||
const groupPaths = deriveGroupPaths(commandPaths);
|
||||
const registry = new CommandRegistry(commands, "kscli");
|
||||
|
||||
describe("e2e: kscli registry smoke", () => {
|
||||
test("根帮助展示 kscli 与全局 flag", async () => {
|
||||
@@ -38,13 +40,25 @@ describe("e2e: kscli registry smoke", () => {
|
||||
expect(stderr).toMatch(/--query|Missing required/i);
|
||||
});
|
||||
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: kscli ${path}`);
|
||||
});
|
||||
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
|
||||
const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
|
||||
const commandPath = path.split(" ");
|
||||
|
||||
expect(resolve([...commandPath, "--help"], registry)).toEqual({
|
||||
kind: "help",
|
||||
path: commandPath,
|
||||
});
|
||||
expect(captureRegistryHelp(registry, commandPath)).toContain(
|
||||
`Usage: kscli ${path} <command> [flags]`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bailian-cli-runtime",
|
||||
"version": "1.18.0",
|
||||
"version": "1.18.1",
|
||||
"description": "Runtime framework for bailian-cli (createCli, registry, args, output, pipeline). See https://www.npmjs.com/package/bailian-cli for usage.",
|
||||
"homepage": "https://bailian.console.aliyun.com/cli",
|
||||
"bugs": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-cli
|
||||
metadata:
|
||||
version: "1.18.0"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -53,7 +53,7 @@ bl auth generate-access-token --access-key-id LTAIxxxxx --access-key-secret xxxx
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------------------ | ------ | -------- | ------------------------------------------------------------------------------------- |
|
||||
| `--api-key <key>` | string | no | Model API key to store |
|
||||
| `--base-url <url>` | string | no | Model API base URL (used with --api-key for validation) |
|
||||
| `--base-url <url>` | string | no | Model API base URL to store with --api-key |
|
||||
| `--console` | switch | no | Sign in via browser; use --console-site to choose domestic (default) or international |
|
||||
| `--console-site <site>` | string | no | Console site: domestic, international |
|
||||
| `--open-api` | switch | no | Store Alibaba Cloud OpenAPI AK/SK credentials |
|
||||
|
||||
@@ -149,12 +149,12 @@ bl quota list --output json
|
||||
|
||||
### `bl quota update`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | -------------------------------------------------------------------- |
|
||||
| **Name** | `quota update` |
|
||||
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------------------------- |
|
||||
| **Name** | `quota update` |
|
||||
| **Description** | Update model rate limits (QPM/TPM), or clear them with --delete |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl quota update --model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -164,12 +164,14 @@ bl quota list --output json
|
||||
| `--rpm <n>` | number | no | Max requests per minute (QPM) |
|
||||
| `--tpm <n>` | number | no | Max tokens per minute (TPM) |
|
||||
| `--delete` | switch | no | Clear all custom rate limits for the model |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt for --delete |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Fields you omit keep their current values (server-side OVERLAY merge); --delete clears all custom limits.
|
||||
- --delete requires confirmation; pass --yes to skip the prompt in scripts.
|
||||
- Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.
|
||||
|
||||
#### Examples
|
||||
@@ -186,6 +188,10 @@ bl quota update --model qwen3-max --tpm 500000
|
||||
bl quota update --model qwen-plus --delete
|
||||
```
|
||||
|
||||
```bash
|
||||
bl quota update --model qwen-plus --delete --yes
|
||||
```
|
||||
|
||||
```bash
|
||||
bl quota update --model qwen-plus --rpm 60 --output json
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-finetune
|
||||
metadata:
|
||||
version: "1.18.0"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -19,20 +19,25 @@ Index: [index.md](index.md)
|
||||
|
||||
### `bl dataset delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset delete --file-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------ |
|
||||
| **Name** | `dataset delete` |
|
||||
| **Description** | Delete a dataset file by ID |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl dataset delete --file-id <id> [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | -------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--file-id <id>` | string | yes | Dataset file ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the dataset file is permanently removed.
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -44,6 +49,10 @@ bl dataset delete --file-id file-id-xxx
|
||||
bl dataset delete --file-id file-id-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl dataset delete --file-id file-id-xxx --yes
|
||||
```
|
||||
|
||||
### `bl dataset get`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -73,12 +73,12 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry-
|
||||
|
||||
### `bl deploy delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------------------------------- |
|
||||
| **Name** | `deploy delete` |
|
||||
| **Description** | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck]` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| **Name** | `deploy delete` |
|
||||
| **Description** | Delete a model deployment (must be STOPPED or FAILED) |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl deploy delete --deployed-model <id> [--skip-precheck] [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
@@ -86,9 +86,14 @@ bl deploy audio create --model-name my-cosyvoice-ft --display-name my-tts --dry-
|
||||
| ----------------------- | ------ | -------- | --------------------------------------------- |
|
||||
| `--deployed-model <id>` | string | yes | Deployed model identifier (required) |
|
||||
| `--skip-precheck` | switch | no | Skip the local STOPPED/FAILED status precheck |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the deployment is permanently destroyed.
|
||||
|
||||
#### Examples
|
||||
|
||||
```bash
|
||||
@@ -99,6 +104,10 @@ bl deploy delete --deployed-model dep-...
|
||||
bl deploy delete --deployed-model dep-... --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl deploy delete --deployed-model dep-... --yes
|
||||
```
|
||||
|
||||
### `bl deploy get`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -181,23 +181,25 @@ bl finetune checkpoints --job-id ft-xxx --output json
|
||||
|
||||
### `bl finetune delete`
|
||||
|
||||
| Field | Value |
|
||||
| ------------------ | ---------------------------------- |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune delete --job-id <id>` |
|
||||
| Field | Value |
|
||||
| ------------------ | ------------------------------------------ |
|
||||
| **Name** | `finetune delete` |
|
||||
| **Description** | Delete a fine-tune job record |
|
||||
| **Authentication** | API Key |
|
||||
| **Usage** | `bl finetune delete --job-id <id> [--yes]` |
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | --------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
| Flag | Type | Required | Description |
|
||||
| ------------------ | ------ | -------- | ---------------------------- |
|
||||
| `--job-id <id>` | string | yes | Fine-tune job ID (required) |
|
||||
| `--yes` | switch | no | Skip the confirmation prompt |
|
||||
| `--api-key <key>` | string | no | API key |
|
||||
| `--base-url <url>` | string | no | API base URL |
|
||||
|
||||
#### Notes
|
||||
|
||||
- Irreversible — the job record is permanently removed.
|
||||
- Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.
|
||||
|
||||
#### Examples
|
||||
@@ -210,6 +212,10 @@ bl finetune delete --job-id ft-xxx
|
||||
bl finetune delete --job-id ft-xxx --dry-run
|
||||
```
|
||||
|
||||
```bash
|
||||
bl finetune delete --job-id ft-xxx --yes
|
||||
```
|
||||
|
||||
### `bl finetune export`
|
||||
|
||||
| Field | Value |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-gen
|
||||
metadata:
|
||||
version: "1.18.0"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-managed-agent
|
||||
metadata:
|
||||
version: "1.18.0"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-protocol
|
||||
metadata:
|
||||
version: "1.18.0"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
@@ -68,7 +68,7 @@ After approval, treat Bailian as selected for the current task. Do not ask again
|
||||
Install, API key / console login, endpoint override, and config keys:
|
||||
[`assets/setup.md`](assets/setup.md).
|
||||
|
||||
**Token Plan:** Get the API key from the [subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview), then run `bl auth login --config token-plan --api-key <key>`. The built-in Profile supplies the Base URL and its supported leaf API Key capabilities, and login validates the key before saving it. While that Profile is active, unsupported API Key commands automatically use `default` credentials. A per-command `--api-key` / `--base-url` or `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` bypasses this fallback entirely.
|
||||
**Token Plan:** Get the API key from the [subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview), then run `bl auth login --config token-plan --api-key <key>`. The built-in Profile supplies the Base URL and its supported leaf API Key capabilities, and login saves the key without a live model probe. While that Profile is active, unsupported API Key commands automatically use `default` credentials. A per-command `--api-key` / `--base-url` or `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` bypasses this fallback entirely.
|
||||
|
||||
**Console login:** never run bare `bl auth login --console` — always pass `--console-site domestic` or `--console-site international`. Before login, run `bl config show --output json` and follow the site-selection rules in [`assets/setup.md` → Console site selection](assets/setup.md#console-site-selection).
|
||||
|
||||
|
||||
@@ -82,9 +82,9 @@ bl image generate --prompt "A cat"
|
||||
bl video generate --prompt "A horse running through a field"
|
||||
```
|
||||
|
||||
The built-in Profile supplies the Token Plan Base URL. `auth login` tests the key first, then saves
|
||||
and activates the Profile only when validation succeeds; do not ask the user to configure the Base
|
||||
URL or run a duplicate smoke test.
|
||||
The built-in Profile supplies the Token Plan Base URL. `auth login` saves the key and activates
|
||||
the Profile without a live model probe; do not ask the user to configure the Base URL or run a
|
||||
duplicate smoke test.
|
||||
|
||||
### API Key capability fallback
|
||||
|
||||
@@ -120,7 +120,7 @@ bl config use --name default
|
||||
```
|
||||
|
||||
`auth login --config token-plan` creates or updates that Profile and activates it only after the
|
||||
credential is validated and saved. Failed login and `--dry-run` do not switch Profiles. Use
|
||||
credential is saved. Failed login and `--dry-run` do not switch Profiles. Use
|
||||
`--config default` for a one-command override. Config selection follows explicit `--config` >
|
||||
persisted `active_config` > `default`; credential and endpoint fields inside the selected Profile
|
||||
still follow flag > environment > config.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: bailian-web-search
|
||||
metadata:
|
||||
version: "1.18.0"
|
||||
version: "1.18.1"
|
||||
requires:
|
||||
bins: ["bl"]
|
||||
description: >-
|
||||
|
||||
Reference in New Issue
Block a user