Files
lisheng.lisheng aef6c1accf fix(config agent): align agent configs to Alibaba Cloud Model Studio docs
Adopt the code-review findings and reconcile every writer with the official
Model Studio docs (with cc-switch / each agent's own docs as secondary refs).

- qwen-code: drop deprecated security.auth.apiKey/baseUrl (keep only
  selectedType), restore $version:3, model:{name}; env BAILIAN_API_KEY.
- codex: official env_key="OPENAI_API_KEY" structure (drop requires_openai_auth
  / model_reasoning_effort / disable_response_storage); respect $CODEX_HOME.
- hermes: revert to official flat model.* block; api_mode only for anthropic
  endpoints (omitted for OpenAI-compatible).
- opencode: drop setCacheKey; target existing opencode.jsonc when present.
- openclaw: stop hardcoding 1M contextWindow; add optional --context-window;
  add agents.defaults.models.
- claude-code: respect $CLAUDE_CONFIG_DIR.
- utils: on parse failure, throw and keep the original file (no silent wipe).
- tests: rewrite writer unit tests + add missing-flag e2e for every required
  flag; regenerate skill reference; document `bl config agent` in READMEs.
2026-07-19 17:53:55 +08:00

606 lines
18 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { CONFIG_ROUTES } from "./topic-routes.ts";
/**
* Config 相关 E2E
*/
describe("e2e: config", () => {
test("config show --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(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",
]);
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",
]);
expect(listResult.exitCode, listResult.stderr).toBe(0);
expect(listResult.stderr).toMatch(/list|active|profile/i);
const useResult = await runCommandE2e(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",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/ui|--port|--no-open|web/i);
});
test("config ui --dry-run 打印计划不起服务", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"ui",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ host?: string; routes?: string[] }>(stdout);
expect(data.host).toBe("127.0.0.1");
expect(Array.isArray(data.routes)).toBe(true);
});
test("config show --output json", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"show",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
config_file?: string;
base_url?: string;
timeout?: number;
}>(stdout);
expect(data.config_file).toBeDefined();
expect(data.base_url).toBeDefined();
expect(data.timeout).toBeDefined();
});
test("config show --output text", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"show",
"--output",
"text",
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toMatch(/config_file|timeout|base_url/i);
});
test("config show 脱敏 security_token", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-config-show-secret-"));
try {
const securityToken = "sts-sensitive-token";
writeFileSync(
join(configDir, "config.json"),
JSON.stringify({ security_token: securityToken }, null, 2) + "\n",
);
const { stdout, stderr, exitCode } = await runCommandE2e(
CONFIG_ROUTES,
["config", "show", "--output", "json"],
{ BAILIAN_CONFIG_DIR: configDir },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ security_token?: string }>(stdout);
expect(data.security_token).toBe("sts-...oken");
expect(stdout).not.toContain(securityToken);
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("config set 缺少 --key / --value 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--quiet",
]);
expect(exitCode, stderr).toBe(2);
expect(stderr).toMatch(/--key|--value|Usage:/i);
});
test("config use 缺少 --name 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"use",
"--quiet",
]);
expect(exitCode, stderr).toBe(2);
expect(stderr).toMatch(/--name|Usage:/i);
});
test("config use 持久化激活项,config list 展示激活状态", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-config-use-"));
try {
const configPath = join(configDir, "config.json");
writeFileSync(
configPath,
JSON.stringify({ dev: { output: "json" } }, null, 2) + "\n",
);
const env = { BAILIAN_CONFIG_DIR: configDir };
const useResult = await runCommandE2e(
CONFIG_ROUTES,
["config", "use", "--name", "dev", "--output", "json"],
env,
);
expect(useResult.exitCode, useResult.stderr).toBe(0);
expect(
parseStdoutJson<{ active_config?: string }>(useResult.stdout)
.active_config,
).toBe("dev");
expect(JSON.parse(readFileSync(configPath, "utf8")).active_config).toBe(
"dev",
);
const listResult = await runCommandE2e(
CONFIG_ROUTES,
["config", "list", "--output", "json"],
env,
);
expect(listResult.exitCode, listResult.stderr).toBe(0);
const listData = parseStdoutJson<{
active_config?: string;
profiles?: string[];
}>(listResult.stdout);
expect(listData.active_config).toBe("dev");
expect(listData.profiles).toEqual(["default", "dev"]);
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("config use --dry-run 校验目标但不修改激活项", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-config-use-dry-run-"));
try {
const configPath = join(configDir, "config.json");
writeFileSync(
configPath,
JSON.stringify({ dev: { output: "json" } }, null, 2) + "\n",
);
const result = await runCommandE2e(
CONFIG_ROUTES,
["config", "use", "--name", "dev", "--dry-run", "--output", "json"],
{ BAILIAN_CONFIG_DIR: configDir },
);
expect(result.exitCode, result.stderr).toBe(0);
expect(
parseStdoutJson<{ would_activate?: string }>(result.stdout)
.would_activate,
).toBe("dev");
expect(
JSON.parse(readFileSync(configPath, "utf8")).active_config,
).toBeUndefined();
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("config use 拒绝不存在的 Profile 且不写入状态", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-config-use-missing-"));
try {
const configPath = join(configDir, "config.json");
writeFileSync(
configPath,
JSON.stringify({ output: "text" }, null, 2) + "\n",
);
const result = await runCommandE2e(
CONFIG_ROUTES,
["config", "use", "--name", "missing", "--output", "json"],
{ BAILIAN_CONFIG_DIR: configDir },
);
expect(result.exitCode).toBe(2);
expect(result.stderr).toMatch(/does not exist/);
expect(
JSON.parse(readFileSync(configPath, "utf8")).active_config,
).toBeUndefined();
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("config set 非法 key 时退出为用法错误", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--key",
"not-a-real-key",
"--value",
"x",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid config key|not-a-real-key/i);
});
test("config set 非法 output", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--key",
"output",
"--value",
"yaml",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid output|text, json/i);
});
test("config set 非法 timeout", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--key",
"timeout",
"--value",
"0",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid timeout|positive/i);
});
test("config set 归一化 Base URL 并拒绝非法协议", async () => {
const configDir = mkdtempSync(join(tmpdir(), "bl-config-base-url-"));
try {
const setResult = await runCommandE2e(
CONFIG_ROUTES,
[
"config",
"set",
"--key",
"base_url",
"--value",
"https://proxy.example.com/bailian/compatible-mode/v1/?x=1#fragment",
"--output",
"json",
],
{ BAILIAN_CONFIG_DIR: configDir },
);
expect(setResult.exitCode, setResult.stderr).toBe(0);
expect(
parseStdoutJson<{ base_url?: string }>(setResult.stdout).base_url,
).toBe("https://proxy.example.com/bailian");
expect(
JSON.parse(readFileSync(join(configDir, "config.json"), "utf8"))
.base_url,
).toBe("https://proxy.example.com/bailian");
const invalidResult = await runCommandE2e(
CONFIG_ROUTES,
[
"config",
"set",
"--key",
"base_url",
"--value",
"ftp://example.com/models",
],
{ BAILIAN_CONFIG_DIR: configDir },
);
expect(invalidResult.exitCode).toBe(2);
expect(invalidResult.stderr).toMatch(/Invalid model base URL/);
} finally {
rmSync(configDir, { recursive: true, force: true });
}
});
test("config set --dry-run 不落盘(仅输出 would_set)", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--dry-run",
"--key",
"output",
"--value",
"json",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_set?: { output?: string } }>(stdout);
expect(data.would_set?.output).toBe("json");
});
test("config set --dry-run 支持连字符别名 key", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--dry-run",
"--key",
"default-text-model",
"--value",
"qwen3.7-max",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_set?: { default_text_model?: string };
}>(stdout);
expect(data.would_set?.default_text_model).toBe("qwen3.7-max");
});
test("config set --dry-run 展示归一化后的 Base URL", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--dry-run",
"--key",
"base-url",
"--value",
"https://proxy.example.com/apps/anthropic/?x=1#fragment",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_set?: { base_url?: string } }>(stdout);
expect(data.would_set?.base_url).toBe("https://proxy.example.com");
});
test("config set --dry-run 支持 AccessKey 短字段别名", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--dry-run",
"--key",
"access-key-id",
"--value",
"LTAI-config-placeholder",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_set?: { access_key_id?: string } }>(
stdout,
);
expect(data.would_set?.access_key_id).toBe("LTAI-config-placeholder");
});
test("config set 不接受旧 OpenAPI AccessKey 字段名", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"set",
"--key",
"openapi_access_key_id",
"--value",
"LTAI-config-placeholder",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Invalid config key|openapi_access_key_id/);
});
test("config agent --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"agent",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/agent|--base-url|--model/i);
});
test("config agent 缺少 --api-key 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"agent",
"--agent",
"claude-code",
"--base-url",
"https://dashscope.aliyuncs.com/apps/anthropic",
"--model",
"qwen3-max",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--api-key|Usage:/i);
});
test("config agent 缺少 --base-url 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"agent",
"--agent",
"claude-code",
"--api-key",
"sk-placeholder",
"--model",
"qwen3-max",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--base-url|Usage:/i);
});
test("config agent 缺少 --model 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"agent",
"--agent",
"claude-code",
"--base-url",
"https://dashscope.aliyuncs.com/apps/anthropic",
"--api-key",
"sk-placeholder",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--model|Usage:/i);
});
test("config agent 缺少 --agent 时报用法错误并退出 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"agent",
"--base-url",
"https://dashscope.aliyuncs.com/apps/anthropic",
"--api-key",
"sk-placeholder",
"--model",
"qwen3-max",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--agent|Usage:/i);
});
test("config agent 非法 --agent 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
"config",
"agent",
"--agent",
"not-an-agent",
"--base-url",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"--api-key",
"sk-placeholder",
"--model",
"qwen3-max",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/not-an-agent|claude-code|agent/i);
});
test("config agent --dry-run 输出脱敏信息且不写盘", async () => {
const home = mkdtempSync(join(tmpdir(), "bl-config-agent-dry-"));
try {
const { stdout, stderr, exitCode } = await runCommandE2e(
CONFIG_ROUTES,
[
"config",
"agent",
"--agent",
"claude-code",
"--base-url",
"https://dashscope.aliyuncs.com/apps/anthropic",
"--api-key",
"sk-secret-placeholder",
"--model",
"qwen3-max",
"--dry-run",
"--output",
"json",
],
{ HOME: home },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
agent?: string;
base_url?: string;
model?: string;
api_key?: string;
}>(stdout);
expect(data.agent).toBe("claude-code");
expect(data.base_url).toBe(
"https://dashscope.aliyuncs.com/apps/anthropic",
);
expect(data.model).toBe("qwen3-max");
expect(stdout).not.toContain("sk-secret-placeholder");
expect(existsSync(join(home, ".claude", "settings.json"))).toBe(false);
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test("config agent codex 写入 config.toml 与 auth.json(官方 env_key 结构)", async () => {
const home = mkdtempSync(join(tmpdir(), "bl-config-agent-codex-"));
try {
const { stderr, exitCode } = await runCommandE2e(
CONFIG_ROUTES,
[
"config",
"agent",
"--agent",
"codex",
"--base-url",
"https://dashscope.aliyuncs.com/compatible-mode/v1",
"--api-key",
"sk-codex-placeholder",
"--model",
"qwen3-coder-plus",
],
{ HOME: home },
);
expect(exitCode, stderr).toBe(0);
const toml = readFileSync(join(home, ".codex", "config.toml"), "utf8");
expect(toml).toContain('model_provider = "bailian-cli"');
expect(toml).toContain('env_key = "OPENAI_API_KEY"');
expect(toml).toContain('wire_api = "responses"');
expect(toml).not.toContain("requires_openai_auth");
const auth = JSON.parse(
readFileSync(join(home, ".codex", "auth.json"), "utf8"),
);
expect(auth.OPENAI_API_KEY).toBe("sk-codex-placeholder");
} finally {
rmSync(home, { recursive: true, force: true });
}
});
test("config agent hermes 写入官方扁平 model.* 结构", async () => {
const home = mkdtempSync(join(tmpdir(), "bl-config-agent-hermes-"));
try {
const { stderr, exitCode } = await runCommandE2e(
CONFIG_ROUTES,
[
"config",
"agent",
"--agent",
"hermes",
"--base-url",
"https://dashscope.aliyuncs.com/apps/anthropic",
"--api-key",
"sk-hermes-placeholder",
"--model",
"qwen3-max",
],
{ HOME: home },
);
expect(exitCode, stderr).toBe(0);
const yamlText = readFileSync(
join(home, ".hermes", "config.yaml"),
"utf8",
);
expect(yamlText).toContain("provider: custom");
expect(yamlText).toContain("api_mode: anthropic_messages");
expect(yamlText).not.toContain("custom_providers");
} finally {
rmSync(home, { recursive: true, force: true });
}
});
});