Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions docs/agents/cli-e2e-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

## 架构分层

| 层级 | 路径 | 测什么 |
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-runlive(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 路径除外)。

## 触发条件

Expand All @@ -29,28 +29,34 @@
### 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)

### 产品 smoke

- 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)` 等显式超时

## 双层 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 / 真实集成
Expand All @@ -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 块**末尾**
Expand Down Expand Up @@ -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>` 通过
Expand Down
48 changes: 36 additions & 12 deletions packages/cli/tests/e2e/registry.smoke.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
Expand All @@ -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/);
Expand All @@ -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>/);
Expand All @@ -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]`,
);
});
});
4 changes: 2 additions & 2 deletions packages/commands/tests/e2e/advisor-recommend.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
7 changes: 4 additions & 3 deletions packages/commands/tests/e2e/auth.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
isOpenApiE2EReady,
makeE2eOutputDir,
parseStdoutJson,
runCommandHelp,
runCommandE2e,
} from "./helpers.ts";
import { AUTH_ROUTES } from "./topic-routes.ts";
Expand All @@ -15,7 +16,7 @@ import { AUTH_ROUTES } from "./topic-routes.ts";

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/);
Expand Down Expand Up @@ -108,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);
});
Expand Down
20 changes: 20 additions & 0 deletions packages/commands/tests/e2e/command-help.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
14 changes: 7 additions & 7 deletions packages/commands/tests/e2e/config.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -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);
});
Expand Down Expand Up @@ -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);
});
Expand Down
6 changes: 3 additions & 3 deletions packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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",
Expand All @@ -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",
Expand Down
10 changes: 8 additions & 2 deletions packages/commands/tests/e2e/dataset.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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",
Expand Down
9 changes: 4 additions & 5 deletions packages/commands/tests/e2e/deploy.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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",
Expand Down
Loading