Files
modelstudioai__cli/packages/commands/tests/e2e/managed-agent.e2e.test.ts
T

1946 lines
66 KiB
TypeScript
Raw 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 { mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import http from "node:http";
import type { AddressInfo } from "node:net";
import { tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
import {
LocalFileStateBackend,
planProjectWithStateBackend,
resolveProjectConfig,
type ResourceAddress,
} from "@openagentpack/sdk";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { parse } from "yaml";
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");
const AGENTS_DEPLOYMENT_INVALID_YAML = join(
e2eFixturesDir,
"managed-agent",
"agents-deployment-invalid.yaml",
);
const AGENTS_YAML = join(e2eFixturesDir, "managed-agent", "agents.yaml");
const LOCAL_SKILL_ZIP_BASE64 =
"UEsDBAoAAAAIAGBoIV3X45MGOwAAAEoAAAAIAAAAU0tJTEwubWTT1dXlykvMTbVSyMlPTszRLUotyC8q4UpJLU4uyiwoyczPs1LwAckoQGQUirMzc3K4dIHalKESQRAtAFBLAwQKAAAAAABgaCFdAAAAAAAAAAAAAAAACAAAAHNjcmlwdHMvUEsDBAoAAAAIAGBoIV3vn4RoIQAAAB8AAAAOAAAAc2NyaXB0cy9ydW4uanNLrSjILypRSM7PKy5RKCrNU7BV0NBUsLVTKCkqTbXmAgBQSwECFAAKAAAACABgaCFd1+OTBjsAAABKAAAACAAAAAAAAAAAAAAAAAAAAAAAU0tJTEwubWRQSwECFAAKAAAAAABgaCFdAAAAAAAAAAAAAAAACAAAAAAAAAAAABAAAABhAAAAc2NyaXB0cy9QSwECFAAKAAAACABgaCFd75+EaCEAAAAfAAAADgAAAAAAAAAAAAAAAACHAAAAc2NyaXB0cy9ydW4uanNQSwUGAAAAAAMAAwCoAAAA1AAAAAAA";
const DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES = [
"bailian.deployment.initial_events.message_required",
"bailian.deployment.file.mount_path.required",
"bailian.deployment.file.mount_path.duplicate",
];
const projectDirectories: string[] = [];
afterEach(async () => {
for (const directory of projectDirectories.splice(0)) {
await rm(directory, { recursive: true, force: true });
}
});
async function writeLocalSkillDirectory(parentDirectory: string): Promise<string> {
const skillDirectory = join(parentDirectory, "local-report");
await mkdir(join(skillDirectory, "scripts"), { recursive: true });
await writeFile(
join(skillDirectory, "SKILL.md"),
`---
name: local-report
description: Local report skill
---
# Local Report
`,
"utf8",
);
await writeFile(join(skillDirectory, "scripts", "run.js"), "export const run = true;\n", "utf8");
return skillDirectory;
}
async function writeLocalSkillZip(parentDirectory: string): Promise<string> {
const skillZipPath = join(parentDirectory, "local-report.zip");
await writeFile(skillZipPath, Buffer.from(LOCAL_SKILL_ZIP_BASE64, "base64"));
return skillZipPath;
}
async function seedTrackedResources(
configPath: string,
addresses: ResourceAddress[],
): Promise<void> {
const loaded = await resolveProjectConfig(configPath);
const stateBackend = new LocalFileStateBackend({ configPath });
const stateScope = { projectId: loaded.projectName ?? basename(dirname(configPath)) };
const backendInput = {
projectName: loaded.projectName,
config: loaded.config,
configPath: loaded.configPath,
providers: loaded.config.providers,
stateBackend,
stateScope,
};
const planned = await planProjectWithStateBackend(backendInput, {
provider: "bailian",
refresh: false,
quiet: true,
});
await stateBackend.write(stateScope, (state) => {
for (const address of addresses) {
const action = planned.plan.actions.find(
(entry) =>
entry.address.provider === address.provider &&
entry.address.type === address.type &&
entry.address.name === address.name,
);
const contentHash = (action?.after as { content_hash?: string } | undefined)?.content_hash;
if (action?.action !== "create" || !contentHash) {
throw new Error(`Cannot seed tracked resource ${address.type}.${address.name}.`);
}
state.setResource({
address,
remote_id: `${address.type}_${address.name}_e2e`,
content_hash: contentHash,
desired_hash: contentHash,
});
}
});
}
/**
* managed-agenthelp / 缺参不依赖密钥;所有 mutation 命令的 --dry-run
* 必须在构建 SDK runtime凭证注入 / 联网 / 写盘)之前短路,因此同样不需要密钥。
* 鉴权分层离线命令init/project init/validate/state list|show|rmauth: "none"
* 联网命令统一 auth: "apiKey" 硬门禁(见 managed-agent-auth-chain e2e
* 真实集成apply/destroy/session 流程)依赖工作区内的 agents.yaml 与远端资源,
* 属批量场景,暂仅覆盖 dry-run 契约。
*/
describe("e2e: managed-agent", () => {
test("validate 接受原生 Bailian Deployment 配置", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"validate",
"--file",
AGENTS_DEPLOYMENT_YAML,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
valid?: boolean;
diagnostics?: Array<{ code?: string; message?: string; severity?: string }>;
}>(stdout);
expect(data.valid).toBe(true);
expect(data.diagnostics).not.toContainEqual(
expect.objectContaining({ code: "bailian.deployment.schedule_unsupported" }),
);
});
test("plan --dry-run 通过 SDK planner 规划原生 Bailian Deployment", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"plan",
"--dry-run",
"--file",
AGENTS_DEPLOYMENT_YAML,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
actions?: Array<{
action?: string;
address?: { provider?: string; type?: string; name?: string };
}>;
diagnostics?: Array<{ code?: string; severity?: string }>;
}>(stdout);
expect(data.actions).toContainEqual(
expect.objectContaining({
action: "create",
address: {
provider: "bailian",
type: "deployment",
name: "daily-report",
},
}),
);
expect(data.diagnostics).not.toContainEqual(
expect.objectContaining({ code: "bailian.deployment.schedule_unsupported" }),
);
});
test("validate 拒绝会产生空消息或无效挂载路径的 Bailian Deployment", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"validate",
"--file",
AGENTS_DEPLOYMENT_INVALID_YAML,
"--output",
"json",
]);
expect(exitCode).toBe(1);
const data = parseStdoutJson<{
valid?: boolean;
diagnostics?: Array<{ code?: string; message?: string; severity?: string }>;
}>(stdout);
expect(data.valid).toBe(false);
const errorDiagnostics =
data.diagnostics?.filter((diagnostic) => diagnostic.severity === "error") ?? [];
const firstError = errorDiagnostics[0];
expect(firstError?.code).toBeTruthy();
expect(firstError?.message).toBeTruthy();
expect(stderr).toContain(`[${firstError?.code}] ${firstError?.message}`);
expect(stderr).toContain(`(+${errorDiagnostics.length - 1} more errors)`);
expect(stderr).not.toMatch(/Validation failed with \d+ error/);
for (const diagnosticCode of DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES) {
expect(data.diagnostics).toContainEqual(
expect.objectContaining({ code: diagnosticCode, severity: "error" }),
);
}
});
test("plan --dry-run 在 Deployment 安全诊断失败时阻断计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"plan",
"--dry-run",
"--file",
AGENTS_DEPLOYMENT_INVALID_YAML,
"--output",
"json",
]);
expect(exitCode).toBe(1);
const data = parseStdoutJson<{
diagnostics?: Array<{ code?: string; message?: string; severity?: string }>;
}>(stdout);
const errorDiagnostics =
data.diagnostics?.filter((diagnostic) => diagnostic.severity === "error") ?? [];
const firstError = errorDiagnostics[0];
expect(firstError?.code).toBeTruthy();
expect(firstError?.message).toBeTruthy();
expect(stderr).toContain(`[${firstError?.code}] ${firstError?.message}`);
expect(stderr).toContain(`(+${errorDiagnostics.length - 1} more errors)`);
expect(stderr).not.toContain("Plan contains errors.");
for (const diagnosticCode of DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES) {
expect(data.diagnostics).toContainEqual(
expect.objectContaining({ code: diagnosticCode, severity: "error" }),
);
}
});
test("managed-agent apply --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
"managed-agent",
"apply",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--file|--yes/i);
expect(stderr).not.toContain("--provider");
expect(stderr).not.toContain("--ci");
expect(stderr).not.toContain("--refresh-only");
});
test("managed-agent apply 不再接受 --refresh-only", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"apply",
"--dry-run",
"--refresh-only",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Unknown flag.*--refresh-only/i);
});
test("managed-agent init 不再暴露 Git 仓库脚手架", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"init",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).not.toContain("--git");
});
test("managed-agent project 暴露目录项目与共享版本管理子命令", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/init|validate|build|publish|workbench|version/i);
});
test("managed-agent project 完成 init、validate、build 与 version status 本地闭环", async () => {
const projectRoot = await mkdtemp(join(tmpdir(), "bailian-managed-agent-project-"));
projectDirectories.push(projectRoot);
const initialized = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"init",
"--project",
projectRoot,
"--output",
"json",
]);
expect(initialized.exitCode, initialized.stderr).toBe(0);
expect(
parseStdoutJson<{ baseline_version?: string }>(initialized.stdout).baseline_version,
).toMatch(/^[a-f0-9]{64}$/);
expect(JSON.parse(await readFile(join(projectRoot, "project.json"), "utf8"))).toEqual({
version: "1",
});
expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull();
expect(await stat(join(projectRoot, ".gitignore")).catch(() => null)).toBeNull();
expect(
JSON.parse(await readFile(join(projectRoot, "agents/assistant/agent.json"), "utf8")),
).toEqual({ name: "Assistant", model: "qwen3.7-max" });
for (const relativePath of [
"skills/_examples/example-skill/skill.json",
"skills/_examples/example-skill/SKILL.md",
"files/_examples/example-file/file.json",
"files/_examples/example-file/example.md",
"vaults/_examples/example-vault/vault.json",
"environments/_examples/example-env/environment.json",
]) {
expect((await stat(join(projectRoot, "agents/assistant", relativePath))).isFile()).toBe(true);
expect(
await readFile(join(projectRoot, "agents/assistant", relativePath, "../README.md"), "utf8"),
).toContain("_examples/");
}
const skillDirectory = join(projectRoot, "agents/assistant/skills/writer");
const filesDirectory = join(projectRoot, "agents/assistant/files");
const nestedFileDirectory = join(filesDirectory, "mount");
await mkdir(skillDirectory, { recursive: true });
await mkdir(nestedFileDirectory, { recursive: true });
await writeFile(join(skillDirectory, "SKILL.md"), "# Writer\n", "utf8");
await writeFile(join(filesDirectory, "brief.txt"), "Build this brief.\n", "utf8");
await writeFile(join(nestedFileDirectory, "mount.md"), "Mount this file.\n", "utf8");
const validated = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"validate",
"--project",
projectRoot,
"--output",
"json",
]);
expect(validated.exitCode, validated.stderr).toBe(0);
expect(
parseStdoutJson<{ diagnostics?: Array<{ severity?: string }> }>(validated.stdout).diagnostics,
).not.toContainEqual(expect.objectContaining({ severity: "error" }));
const vaultDirectory = join(projectRoot, "agents/assistant/vaults/secrets");
await mkdir(vaultDirectory, { recursive: true });
await writeFile(
join(vaultDirectory, "vault.json"),
JSON.stringify({
id: "secrets",
display_name: "Secrets",
credentials: [
{
name: "service",
type: "environment_variable",
secret_name: "SERVICE_TOKEN",
secret_value: "e2e-vault-private-value",
},
],
}),
);
const dryBuild = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"build",
"--project",
projectRoot,
"--dry-run",
"--output",
"json",
]);
expect(dryBuild.exitCode, dryBuild.stderr).toBe(0);
expect(dryBuild.stdout + dryBuild.stderr).not.toContain("e2e-vault-private-value");
expect(dryBuild.stdout).toContain("AGENTS_VAULT_");
expect(await stat(join(projectRoot, ".env")).catch(() => null)).toBeNull();
const built = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"build",
"--project",
projectRoot,
"--output",
"json",
]);
expect(built.exitCode, built.stderr).toBe(0);
const generatedYaml = await readFile(
join(projectRoot, ".openagentpack/build/agents.yaml"),
"utf8",
);
expect(generatedYaml).not.toContain("example-");
expect(generatedYaml).not.toContain("_examples");
expect(generatedYaml).not.toContain("$" + "{SERVICE_TOKEN}");
expect(built.stdout + built.stderr).not.toContain("e2e-vault-private-value");
expect(await readFile(join(projectRoot, ".env"), "utf8")).toContain("e2e-vault-private-value");
expect(await readFile(join(vaultDirectory, "vault.json"), "utf8")).not.toContain(
"e2e-vault-private-value",
);
expect(
await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8"),
).not.toContain("e2e-vault-private-value");
expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain(
"assistant",
);
expect(await readFile(join(projectRoot, ".openagentpack/build/agents.yaml"), "utf8")).toContain(
"provider: bailian",
);
expect(JSON.parse(await readFile(join(skillDirectory, "skill.json"), "utf8"))).toEqual({
id: "writer",
});
expect(JSON.parse(await readFile(join(filesDirectory, "brief/file.json"), "utf8"))).toEqual({
id: "brief",
name: "brief.txt",
source: "./brief.txt",
});
expect(JSON.parse(await readFile(join(nestedFileDirectory, "file.json"), "utf8"))).toEqual({
id: "mount",
name: "mount.md",
source: "./mount.md",
});
const builtAgent = JSON.parse(
await readFile(join(projectRoot, "agents/assistant/agent.json"), "utf8"),
);
expect(builtAgent.skills).toEqual(["writer"]);
expect(builtAgent.files).toEqual(
expect.arrayContaining([
{ file: "brief", mount_path: "/mnt/brief.txt" },
{ file: "mount", mount_path: "/mnt/mount.md" },
]),
);
const status = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"version",
"status",
"--project",
projectRoot,
"--output",
"json",
]);
expect(status.exitCode, status.stderr).toBe(0);
expect(parseStdoutJson<{ enabled?: boolean }>(status.stdout).enabled).toBe(true);
expect(await stat(join(projectRoot, ".openagentpack/state.json")).catch(() => null)).toBeNull();
});
test("managed-agent project version preview 缺少 --version-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"version",
"preview",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--version-id|Missing required/i);
});
test("managed-agent apply 计划失败时在最终错误中保留首条诊断和剩余数量", async () => {
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"apply",
"--file",
AGENTS_DEPLOYMENT_INVALID_YAML,
"--no-refresh",
"--yes",
"--output",
"json",
],
{
DASHSCOPE_API_KEY: "sk-e2e-apply-diagnostics",
BAILIAN_BASE_URL: "http://127.0.0.1:1",
},
);
expect(exitCode).toBe(1);
for (const diagnosticCode of DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES) {
expect(stderr).toContain(`[error] ${diagnosticCode}:`);
}
expect(stderr).toContain(`[${DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES[0]}]`);
expect(stderr).toContain(`(+${DEPLOYMENT_SAFETY_DIAGNOSTIC_CODES.length - 1} more errors)`);
expect(stderr).not.toContain("Cannot apply: resolve the errors above first.");
});
test("managed-agent apply 失败时在 text 和 JSON 最终错误中保留服务端原因", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-apply-error-e2e-"));
const configPath = join(directory, "agents.yaml");
const server = http.createServer((_request, response) => {
response.writeHead(500, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
code: "AGENT_E2E_FAILURE",
message: "intentional apply failure",
request_id: "req_apply_error_e2e",
}),
);
});
await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address() as AddressInfo;
const configSource = `version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: bailian
agents:
apply-error:
name: Apply Error
model: qwen3.8-max
instructions: Exercise the apply error path.
`;
await writeFile(configPath, configSource, "utf8");
const baseArgs = ["managed-agent", "apply", "--file", configPath, "--yes"];
const env = {
DASHSCOPE_API_KEY: "sk-e2e-apply-error",
BAILIAN_BASE_URL: `http://127.0.0.1:${address.port}`,
};
try {
const textResult = await runCommandE2e(MANAGED_AGENT_ROUTES, baseArgs, env);
expect(textResult.exitCode).toBe(1);
expect(textResult.stderr).toMatch(/Error:\s+Bailian API 500:.*intentional apply failure/);
expect(textResult.stderr).not.toMatch(/Error:\s+Apply failed\./);
const jsonResult = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[...baseArgs, "--output", "json"],
env,
);
expect(jsonResult.exitCode).toBe(1);
expect(jsonResult.stderr).toMatch(
/"message":\s*"Bailian API 500:.*intentional apply failure/,
);
expect(jsonResult.stderr).not.toMatch(/"message":\s*"Apply failed\."/);
} finally {
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
await rm(directory, { recursive: true, force: true });
}
});
test("managed-agent agent create --help 展示声明和确认参数", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--name|--model|--instructions|--skill|--skill-dir|--type|--yes/i);
expect(stderr).toMatch(/remote Skill ID/i);
expect(stderr).toMatch(/local Skill directory/i);
expect(stderr).not.toMatch(/--key|--environment|--vault/i);
});
test("managed-agent agent create 缺少 --name 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--model",
"qwen3.8-max",
"--instructions",
"help",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--name|Missing required/i);
});
test.each([
["--environment", "dev"],
["--vault", "secrets"],
])("managed-agent agent create 不接受运行时参数 %s", async (flag, value) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--name",
"Runtime Binding Agent",
"--model",
"qwen3.8-max",
"--instructions",
"help",
flag,
value,
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Unknown option|unknown flag|unexpected/i);
});
test("managed-agent agent create 将 official Skill ID 写为外部引用", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-agent-official-skill-e2e-"));
const configPath = join(directory, "agents.yaml");
await writeFile(
configPath,
`version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: bailian
`,
"utf8",
);
try {
const { exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"agent",
"create",
"--name",
"Report Agent",
"--model",
"qwen3.8-max",
"--instructions",
"Create a report.",
"--skill",
"skill_pptx",
"--type",
"official",
"--file",
configPath,
"--yes",
],
{
DASHSCOPE_API_KEY: "sk-e2e-agent-official-skill",
BAILIAN_BASE_URL: "http://127.0.0.1:1/api/v1/agentstudio",
},
);
expect(exitCode).toBe(1);
const config = parse(await readFile(configPath, "utf8")) as {
agents?: Record<string, { skills?: Array<{ type?: string; skill_id?: string }> }>;
};
expect(config.agents?.["report-agent"]?.skills).toEqual([
{ type: "official", skill_id: "skill_pptx" },
]);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("managed-agent agent create 的 Skill type 默认是 custom", async () => {
const sourceBefore = await readFile(AGENTS_YAML, "utf8");
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--dry-run",
"--name",
"Custom Skill Agent",
"--model",
"qwen3.8-max",
"--instructions",
"Use the custom Skill.",
"--skill",
"skill_custom_abc",
"--file",
AGENTS_YAML,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
agent?: { skills?: Array<{ type?: string; skill_id?: string }> };
yaml_written?: boolean;
}>(stdout);
expect(data.agent?.skills).toEqual([{ type: "custom", skill_id: "skill_custom_abc" }]);
expect(data.yaml_written).toBe(false);
expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore);
});
test("managed-agent agent create --skill-dir 离线规划 Skill 与 Agent 且不写 YAML", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-agent-skill-dir-preview-e2e-"));
const configPath = join(directory, "agents.yaml");
const skillDirectory = await writeLocalSkillDirectory(directory);
const configSource = `version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: bailian
`;
await writeFile(configPath, configSource, "utf8");
try {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--dry-run",
"--name",
"ZIP Agent",
"--model",
"qwen3.8-max",
"--instructions",
"Use the local Skill.",
"--skill-dir",
skillDirectory,
"--file",
configPath,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
agent?: { key?: string; skills?: Array<string | Record<string, unknown>> };
local_skills?: Array<{ key?: string; name?: string; source?: string }>;
yaml_written?: boolean;
actions?: Array<{
action?: string;
address?: { type?: string; name?: string; provider?: string };
}>;
}>(stdout);
expect(data.agent).toEqual(
expect.objectContaining({ key: "zip-agent", skills: ["local-report"] }),
);
expect(data.local_skills).toEqual([
expect.objectContaining({
key: "local-report",
name: "local-report",
source: "local-report",
}),
]);
expect(data.yaml_written).toBe(false);
expect(data.actions).toEqual(
expect.arrayContaining([
expect.objectContaining({
action: "create",
address: expect.objectContaining({ type: "skill", name: "local-report" }),
}),
expect.objectContaining({
action: "create",
address: expect.objectContaining({ type: "agent", name: "zip-agent" }),
}),
]),
);
expect(data.actions).toHaveLength(2);
expect(await readFile(configPath, "utf8")).toBe(configSource);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("managed-agent agent create --skill-dir 兼容本地 ZIP", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-agent-skill-dir-zip-e2e-"));
const configPath = join(directory, "agents.yaml");
const skillZipPath = await writeLocalSkillZip(directory);
const configSource = `version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: bailian
`;
await writeFile(configPath, configSource, "utf8");
try {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--dry-run",
"--name",
"ZIP Agent",
"--model",
"qwen3.8-max",
"--instructions",
"Use the ZIP Skill.",
"--skill-dir",
skillZipPath,
"--file",
configPath,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
agent?: { skills?: Array<string | Record<string, unknown>> };
local_skills?: Array<{ key?: string; source?: string }>;
yaml_written?: boolean;
}>(stdout);
expect(data.agent?.skills).toEqual(["local-report"]);
expect(data.local_skills).toEqual([
expect.objectContaining({ key: "local-report", source: "local-report.zip" }),
]);
expect(data.yaml_written).toBe(false);
expect(await readFile(configPath, "utf8")).toBe(configSource);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("managed-agent agent create --skill-dir 写入顶层 Skill 并在 Agent 中引用 key", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-agent-skill-dir-write-e2e-"));
const configPath = join(directory, "agents.yaml");
const skillDirectory = await writeLocalSkillDirectory(directory);
await writeFile(
configPath,
`version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: bailian
`,
"utf8",
);
const server = http.createServer((_request, response) => {
response.writeHead(500, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "intentional Skill directory upload failure" }));
});
await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address() as AddressInfo;
const args = [
"managed-agent",
"agent",
"create",
"--name",
"ZIP Agent",
"--model",
"qwen3.8-max",
"--instructions",
"Use the local Skill.",
"--skill-dir",
skillDirectory,
"--file",
configPath,
"--yes",
"--output",
"json",
];
const env = {
DASHSCOPE_API_KEY: "sk-e2e-agent-skill-zip",
BAILIAN_BASE_URL: `http://127.0.0.1:${address.port}/api/v1/agentstudio`,
};
try {
for (let attempt = 0; attempt < 2; attempt += 1) {
const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.stderr).toContain("intentional Skill directory upload failure");
}
const config = parse(await readFile(configPath, "utf8")) as {
skills?: Record<string, Record<string, unknown>>;
agents?: Record<string, { skills?: Array<string | Record<string, unknown>> }>;
};
expect(config.skills?.["local-report"]).toEqual({
name: "local-report",
source: "local-report",
origin: "custom",
provider: "bailian",
});
expect(config.agents?.["zip-agent"]?.skills).toEqual(["local-report"]);
expect(config.skills?.["local-report-2"]).toBeUndefined();
expect(config.agents?.["zip-agent-2"]).toBeUndefined();
} finally {
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
await rm(directory, { recursive: true, force: true });
}
});
test.each([
["environment create", ["environment", "create"], /--name|--pip|--yes/i],
["skill create", ["skill", "create"], /--source|--yes/i],
["vault create", ["vault", "create"], /--name|--yes/i],
[
"vault credential create",
["vault", "credential", "create"],
/--vault|--secret-name|--secret-env|--yes/i,
],
["deployment create", ["deployment", "create"], /--name|--agent|--message|--yes/i],
])("managed-agent %s --help 展示资源参数", async (_label, commandArgs, expected) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
...commandArgs,
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(expected);
expect(stderr).not.toMatch(/--key/i);
});
test.each([
[["environment", "create"], /--name|Missing required/i],
[["skill", "create"], /--source|Missing required/i],
[["vault", "create"], /--name|Missing required/i],
[["vault", "credential", "create"], /--vault|Missing required/i],
[["deployment", "create"], /--name|Missing required/i],
])("managed-agent %s 缺少必填参数时退出为用法错误 (2)", async (commandArgs, expected) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
...commandArgs,
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(expected);
});
test("managed-agent agent create 默认只预览,不写 YAML", async () => {
const sourceBefore = await readFile(AGENTS_YAML, "utf8");
const { stdout, stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"agent",
"create",
"--name",
"Create Confirm",
"--model",
"qwen3.8-max",
"--instructions",
"Preview before create",
"--file",
AGENTS_YAML,
"--output",
"json",
],
{ DASHSCOPE_API_KEY: "sk-e2e-agent-create" },
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
agent?: { key?: string; name?: string };
yaml_written?: boolean;
requires_confirmation?: boolean;
ready_to_create?: boolean;
}>(stdout);
expect(data.agent).toEqual(
expect.objectContaining({ key: "create-confirm", name: "Create Confirm" }),
);
expect(data.yaml_written).toBe(false);
expect(data.requires_confirmation).toBe(true);
expect(data.ready_to_create).toBe(true);
expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore);
});
test("managed-agent agent create 远端失败后保留 YAML重试复用 key", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-agent-create-e2e-"));
const configPath = join(directory, "agents.yaml");
await writeFile(configPath, await readFile(AGENTS_YAML, "utf8"), "utf8");
const requestBodies: Array<Record<string, unknown>> = [];
const server = http.createServer((request, response) => {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
const body = Buffer.concat(chunks).toString("utf8");
if (body) requestBodies.push(JSON.parse(body) as Record<string, unknown>);
response.writeHead(500, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "intentional create failure" }));
});
});
await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address() as AddressInfo;
const args = [
"managed-agent",
"agent",
"create",
"--name",
"Retry Agent",
"--model",
"qwen3.8-max",
"--instructions",
"Retry safely",
"--file",
configPath,
"--yes",
"--output",
"json",
];
const env = {
DASHSCOPE_API_KEY: "sk-e2e-agent-create",
BAILIAN_BASE_URL: `http://127.0.0.1:${address.port}/api/v1/agentstudio`,
};
try {
for (let attempt = 0; attempt < 2; attempt += 1) {
const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.stderr).toContain("intentional create failure");
}
const config = parse(await readFile(configPath, "utf8")) as {
agents: Record<string, { name?: string }>;
};
expect(config.agents["retry-agent"]?.name).toBe("Retry Agent");
expect(config.agents["retry-agent-2"]).toBeUndefined();
expect(requestBodies).toHaveLength(2);
expect(requestBodies.every((body) => body.name === "Retry Agent")).toBe(true);
} finally {
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
await rm(directory, { recursive: true, force: true });
}
});
test("managed-agent environment create 远端失败后保留 YAML重试复用 key", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-environment-create-e2e-"));
const configPath = join(directory, "agents.yaml");
await writeFile(configPath, await readFile(AGENTS_YAML, "utf8"), "utf8");
const requestBodies: Array<Record<string, unknown>> = [];
const server = http.createServer((request, response) => {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
const body = Buffer.concat(chunks).toString("utf8");
if (body) requestBodies.push(JSON.parse(body) as Record<string, unknown>);
response.writeHead(500, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "intentional environment create failure" }));
});
});
await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address() as AddressInfo;
const args = [
"managed-agent",
"environment",
"create",
"--name",
"Retry Environment",
"--pip",
"pandas",
"--file",
configPath,
"--yes",
"--output",
"json",
];
const env = {
DASHSCOPE_API_KEY: "sk-e2e-environment-create",
BAILIAN_BASE_URL: `http://127.0.0.1:${address.port}/api/v1/agentstudio`,
};
try {
for (let attempt = 0; attempt < 2; attempt += 1) {
const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env);
expect(result.exitCode, result.stderr).toBe(1);
expect(result.stderr).toContain("intentional environment create failure");
}
const config = parse(await readFile(configPath, "utf8")) as {
environments: Record<string, { name?: string }>;
};
expect(config.environments["retry-environment"]?.name).toBe("Retry Environment");
expect(config.environments["retry-environment-2"]).toBeUndefined();
expect(requestBodies).toHaveLength(2);
expect(requestBodies.every((body) => body.name === "Retry Environment")).toBe(true);
} finally {
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
await rm(directory, { recursive: true, force: true });
}
});
test("vault credential create --yes 缺少 Secret env 时不写 YAML", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-vault-credential-secret-e2e-"));
const configPath = join(directory, "agents.yaml");
const configSource = `version: "1"
providers:
bailian:
api_key: \${DASHSCOPE_API_KEY}
base_url: \${BAILIAN_BASE_URL}
defaults:
provider: bailian
vaults:
production:
display_name: Production
credentials: []
`;
await writeFile(configPath, configSource, "utf8");
try {
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"vault",
"credential",
"create",
"--vault",
"production",
"--name",
"api-token",
"--secret-name",
"API_TOKEN",
"--secret-env",
"E2E_SECRET_THAT_MUST_NOT_EXIST",
"--file",
configPath,
"--yes",
],
{ DASHSCOPE_API_KEY: "sk-e2e-vault-credential" },
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/E2E_SECRET_THAT_MUST_NOT_EXIST.*not set or is empty/i);
expect(await readFile(configPath, "utf8")).toBe(configSource);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("vault credential create 远端失败后复用 YAML 声明且不泄露 Secret", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-vault-credential-retry-e2e-"));
const configPath = join(directory, "agents.yaml");
const requestBodies: Array<Record<string, unknown>> = [];
const server = http.createServer((request, response) => {
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
if (request.method === "GET" && requestUrl.pathname === "/vaults/vault_production_e2e") {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(
JSON.stringify({ id: "vault_production_e2e", type: "vault", display_name: "Production" }),
);
return;
}
if (
request.method === "GET" &&
requestUrl.pathname === "/vaults/vault_production_e2e/credentials"
) {
response.writeHead(200, { "Content-Type": "application/json" });
response.end(JSON.stringify({ data: [], next_page: null }));
return;
}
if (
request.method === "POST" &&
requestUrl.pathname === "/vaults/vault_production_e2e/credentials"
) {
const chunks: Buffer[] = [];
request.on("data", (chunk: Buffer) => chunks.push(chunk));
request.on("end", () => {
requestBodies.push(JSON.parse(Buffer.concat(chunks).toString("utf8")));
response.writeHead(500, { "Content-Type": "application/json" });
response.end(
JSON.stringify({
code: "CREDENTIAL_CREATE_FAILURE",
message: "intentional credential create failure",
request_id: "req_credential_create_e2e",
}),
);
});
return;
}
response.writeHead(404, { "Content-Type": "application/json" });
response.end(JSON.stringify({ message: "unexpected mock route" }));
});
await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address() as AddressInfo;
const configSource = `version: "1"
providers:
bailian:
api_key: test
base_url: http://127.0.0.1:${address.port}
defaults:
provider: bailian
vaults:
production:
display_name: Production
credentials: []
`;
await writeFile(configPath, configSource, "utf8");
const args = [
"managed-agent",
"vault",
"credential",
"create",
"--vault",
"production",
"--name",
"api-token",
"--secret-name",
"API_TOKEN",
"--secret-env",
"ANTHROPIC_API_KEY",
"--file",
configPath,
"--yes",
"--output",
"json",
];
const env = {
DASHSCOPE_API_KEY: "sk-e2e-vault-credential",
ANTHROPIC_API_KEY: "credential-secret-must-not-leak",
};
try {
await seedTrackedResources(configPath, [
{ type: "vault", name: "production", provider: "bailian" },
]);
for (let attempt = 0; attempt < 2; attempt += 1) {
const result = await runCommandE2e(MANAGED_AGENT_ROUTES, args, env);
expect(result.exitCode).toBe(1);
expect(result.stderr).toMatch(/"message":\s*"intentional credential create failure"/);
expect(result.stderr).toMatch(/"http_status":\s*500/);
expect(result.stderr).toMatch(/"api_code":\s*"CREDENTIAL_CREATE_FAILURE"/);
expect(result.stderr).toMatch(/"request_id":\s*"req_credential_create_e2e"/);
expect(`${result.stdout}\n${result.stderr}`).not.toContain(
"credential-secret-must-not-leak",
);
}
const config = parse(await readFile(configPath, "utf8")) as {
vaults: { production: { credentials: Array<{ name?: string; secret_value?: string }> } };
};
expect(config.vaults.production.credentials).toEqual([
expect.objectContaining({
name: "api-token",
secret_value: "${ANTHROPIC_API_KEY}",
}),
]);
expect(requestBodies).toHaveLength(2);
} finally {
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
await rm(directory, { recursive: true, force: true });
}
});
test("deployment create 拒绝非法 Event 与不完整 Schedule", async () => {
const mixedEventInputs = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"deployment",
"create",
"--dry-run",
"--name",
"Mixed Events",
"--agent",
"assistant",
"--message",
"Run",
"--event",
'{"type":"system.message","content":"Be concise"}',
]);
expect(mixedEventInputs.exitCode).toBe(2);
expect(mixedEventInputs.stderr).toMatch(/--message and --event cannot be used together/i);
const invalidEvent = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"deployment",
"create",
"--dry-run",
"--name",
"Invalid Event",
"--agent",
"assistant",
"--event",
'{"type":"tool.call","content":"x"}',
"--file",
AGENTS_YAML,
]);
expect(invalidEvent.exitCode).toBe(2);
expect(invalidEvent.stderr).toMatch(/user\.message or system\.message/i);
const incompleteSchedule = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"deployment",
"create",
"--name",
"Invalid Schedule",
"--agent",
"assistant",
"--message",
"Run",
"--schedule",
"0 9 * * *",
"--quiet",
]);
expect(incompleteSchedule.exitCode).toBe(2);
expect(incompleteSchedule.stderr).toMatch(/--schedule and --timezone/i);
});
test.each(["apply", "destroy"])("managed-agent %s 无 --yes 返回确认请求 (7)", async (command) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
command,
"--file",
"agents.e2e-missing.yaml",
"--api-key",
"e2e-dummy-key",
"--output",
"json",
]);
expect(exitCode).toBe(7);
expect(JSON.parse(stderr)).toMatchObject({
error: { code: 7, type: "requires_confirmation" },
});
});
test.each([
["state rm", ["state", "rm"]],
["session delete", ["session", "delete"]],
["session archive", ["session", "archive"]],
["file delete", ["file", "delete"]],
["deployment run", ["deployment", "run"]],
["deployment pause", ["deployment", "pause"]],
["deployment unpause", ["deployment", "unpause"]],
])("managed-agent %s --help 展示 runtime 注入的 --yes", async (_commandName, commandPath) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
...commandPath,
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--yes/i);
});
test.each([
["state rm", ["state", "rm", "--address", "bailian.agent.assistant"]],
[
"session delete",
["session", "delete", "--session-id", "sess_e2e", "--api-key", "e2e-dummy-key"],
],
["session archive", ["session", "archive", "--session-id", "sess_e2e"]],
["file delete", ["file", "delete", "--file-id", "file_e2e"]],
["deployment run", ["deployment", "run", "--deployment-id", "dep_e2e"]],
["deployment pause", ["deployment", "pause", "--deployment-id", "dep_e2e"]],
["deployment unpause", ["deployment", "unpause", "--deployment-id", "dep_e2e"]],
])("managed-agent %s 无 --yes 返回确认请求 (7)", async (_commandName, commandArgs) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
...commandArgs,
"--output",
"json",
]);
expect(exitCode).toBe(7);
expect(JSON.parse(stderr)).toMatchObject({
error: { code: 7, type: "requires_confirmation" },
});
});
test("managed-agent session delete 缺少 --session-id 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"delete",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--session-id|Missing required/i);
});
test("managed-agent session send 缺少 --message 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"send",
"--session-id",
"sess_e2e",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--message|Missing required/i);
});
test("managed-agent skill-list --help 正常退出", async () => {
const { stderr, exitCode } = await runCommandHelp(MANAGED_AGENT_ROUTES, [
"managed-agent",
"skill-list",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--source|--file/i);
expect(stderr).not.toContain("--provider");
});
test("managed-agent skill-list 非法 --source 时退出为用法错误 (2)", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"skill-list",
"--source",
"builtin",
"--quiet",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/--source must be one of: custom, official/i);
});
test("managed-agent skill-list --source all 通过参数校验(缺配置文件时才失败)", async () => {
// auth: "apiKey" 的凭证解析先于 run() 执行;注入假 key 让用例不依赖环境凭证,
// 命令仍会在配置加载阶段因文件缺失短路,不产生任何网络请求。
const { stderr, exitCode } = await runCommandE2e(
MANAGED_AGENT_ROUTES,
[
"managed-agent",
"skill-list",
"--source",
"all",
"--file",
"agents.e2e-missing.yaml",
"--quiet",
],
{ DASHSCOPE_API_KEY: "sk-e2e-skill-list" },
);
// all 是合法值:不应报 --source 用法错误,而是走到配置加载后因文件缺失退出
expect(exitCode).toBe(2);
expect(stderr).not.toMatch(/--source must be one of/i);
expect(stderr).toMatch(/File not found.*agents\.e2e-missing\.yaml/i);
});
test("managed-agent skill versions --help 展示 cursor 分页选项", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"skill",
"versions",
"--help",
]);
expect(exitCode, stderr).toBe(0);
expect(stderr).toMatch(/--skill-id|--limit|--page|--all/i);
});
test("session event send 拒绝字符串 content", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"event",
"send",
"--dry-run",
"--session-id",
"sess_e2e",
"--event",
'{"type":"message","role":"user","content":"hello"}',
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/invalid event.*content.*expected array/i);
});
test("file upload 不再暴露多 Provider 专属选项", async () => {
const providerResult = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"file",
"upload",
"--dry-run",
"--path",
"missing.txt",
"--provider",
"bailian",
]);
expect(providerResult.exitCode).toBe(2);
expect(providerResult.stderr).toMatch(/Unknown flag.*--provider/i);
const purposeResult = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"file",
"upload",
"--dry-run",
"--path",
"missing.txt",
"--purpose",
"agent",
]);
expect(purposeResult.exitCode).toBe(2);
expect(purposeResult.stderr).toMatch(/Unknown flag.*--purpose/i);
});
});
describe("e2e: managed-agent--dry-run 短路,不联网不写盘)", () => {
test.each([
["session archive", ["session", "archive", "--session-id", "sess_e2e"]],
["session update", ["session", "update", "--session-id", "sess_e2e", "--title", "new"]],
[
"session event send",
[
"session",
"event",
"send",
"--session-id",
"sess_e2e",
"--event",
'{"type":"message","role":"user","content":[{"type":"text","text":"hello"}]}',
],
],
[
"session export",
["session", "export", "--session-id", "sess_e2e", "--output-file", "debug.zip"],
],
["file upload", ["file", "upload", "--path", "missing.txt"]],
[
"file download",
["file", "download", "--file-id", "file_e2e", "--output-file", "artifact.bin"],
],
["file delete", ["file", "delete", "--file-id", "file_e2e"]],
[
"skill download",
[
"skill",
"download",
"--skill-id",
"skill_e2e",
"--skill-version",
"1",
"--output-file",
"skill.zip",
],
],
["deployment run", ["deployment", "run", "--deployment-id", "dep_e2e"]],
["deployment pause", ["deployment", "pause", "--deployment-id", "dep_e2e"]],
["deployment unpause", ["deployment", "unpause", "--deployment-id", "dep_e2e"]],
])("%s --dry-run 在构建 SDK runtime 前短路", async (_label, commandArgs) => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
...commandArgs,
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
});
test("init --dry-run 仅输出计划,不创建文件", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"init",
"--dry-run",
"--file",
"agents.e2e-dry-run.yaml",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_create?: string; provider?: string }>(stdout);
expect(data.would_create).toBe("agents.e2e-dry-run.yaml");
expect(data.provider).toBe("bailian");
});
test("project workbench --dry-run 仅输出目录项目启动计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"workbench",
"--dry-run",
"--project",
"./agent-project",
"--no-open",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_launch?: string;
project_root?: string;
port?: number;
}>(stdout);
expect(data.would_launch).toBe("workbench");
expect(data.project_root).toBe("./agent-project");
expect(data.port).toBe(4848);
});
test("init 不再接受 --provider", async () => {
const { stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"init",
"--dry-run",
"--provider",
"claude",
]);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/Unknown flag.*--provider/i);
});
test("project init --dry-run 不需要密钥且不创建目录", async () => {
const parent = await mkdtemp(join(tmpdir(), "bailian-managed-agent-project-dry-run-"));
projectDirectories.push(parent);
const projectRoot = join(parent, "project");
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"project",
"init",
"--dry-run",
"--project",
projectRoot,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
expect(
parseStdoutJson<{ would_initialize_project?: string }>(stdout).would_initialize_project,
).toBe(projectRoot);
expect(await stat(projectRoot).catch(() => null)).toBeNull();
});
test("apply --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"apply",
"--dry-run",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_apply?: { provider?: string } }>(stdout);
expect(data.would_apply?.provider).toBe("bailian");
});
test("agent create --dry-run 自动生成 key 且不改 YAML", async () => {
const sourceBefore = await readFile(AGENTS_YAML, "utf8");
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"agent",
"create",
"--dry-run",
"--name",
"Create Preview",
"--model",
"qwen3.8-max",
"--instructions",
"Preview only",
"--file",
AGENTS_YAML,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
agent?: { key?: string; name?: string };
yaml_written?: boolean;
actions?: Array<{ action?: string; address?: { name?: string } }>;
}>(stdout);
expect(data.agent).toEqual(
expect.objectContaining({ key: "create-preview", name: "Create Preview" }),
);
expect(data.yaml_written).toBe(false);
expect(data.actions).toContainEqual(
expect.objectContaining({
action: "create",
address: expect.objectContaining({ name: "create-preview" }),
}),
);
expect(data.actions?.every((action) => action.address?.name === "create-preview")).toBe(true);
expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore);
});
test.each([
[
"environment",
["environment", "create", "--name", "Create Environment", "--pip", "pandas"],
"environment",
"create-environment",
],
["vault", ["vault", "create", "--name", "Create Vault"], "vault", "create-vault"],
])("%s create --dry-run 只规划目标资源且不改 YAML", async (_label, commandArgs, type, key) => {
const sourceBefore = await readFile(AGENTS_YAML, "utf8");
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
...commandArgs,
"--dry-run",
"--file",
AGENTS_YAML,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
resource?: { type?: string; key?: string };
yaml_written?: boolean;
actions?: Array<{ action?: string; address?: { type?: string; name?: string } }>;
}>(stdout);
expect(data.resource).toEqual(expect.objectContaining({ type, key }));
expect(data.yaml_written).toBe(false);
expect(data.actions).toContainEqual(
expect.objectContaining({
action: "create",
address: expect.objectContaining({ type, name: key }),
}),
);
expect(
data.actions?.every(
(action) => action.address?.type === type && action.address?.name === key,
),
).toBe(true);
expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore);
});
test("skill create --dry-run 从 SKILL.md 取 name 且不改 YAML", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-skill-create-e2e-"));
const skillPath = join(directory, "SKILL.md");
const sourceBefore = await readFile(AGENTS_YAML, "utf8");
await writeFile(
skillPath,
"---\nname: create-skill\ndescription: E2E Skill\n---\n\n# Create Skill\n",
"utf8",
);
try {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"skill",
"create",
"--dry-run",
"--source",
skillPath,
"--file",
AGENTS_YAML,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
resource?: { type?: string; key?: string; name?: string };
yaml_written?: boolean;
actions?: Array<{ action?: string; address?: { type?: string; name?: string } }>;
}>(stdout);
expect(data.resource).toEqual(
expect.objectContaining({ type: "skill", key: "create-skill", name: "create-skill" }),
);
expect(data.yaml_written).toBe(false);
expect(data.actions).toContainEqual(
expect.objectContaining({
action: "create",
address: expect.objectContaining({ type: "skill", name: "create-skill" }),
}),
);
expect(await readFile(AGENTS_YAML, "utf8")).toBe(sourceBefore);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("deployment create --dry-run 只要求依赖 no-op不检查无关资源", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-deployment-create-e2e-"));
const configPath = join(directory, "agents.yaml");
const configSource = `version: "1"
providers:
bailian:
api_key: test
workspace_id: ws_e2e
defaults:
provider: bailian
agents:
assistant:
name: Assistant
model: qwen3.8-max
instructions: Help the user.
provider: bailian
`;
await writeFile(configPath, configSource, "utf8");
try {
await seedTrackedResources(configPath, [
{ type: "agent", name: "assistant", provider: "bailian" },
]);
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"deployment",
"create",
"--dry-run",
"--name",
"Daily Report",
"--agent",
"assistant",
"--message",
"Generate the report",
"--file",
configPath,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
resource?: { type?: string; key?: string };
yaml_written?: boolean;
ready_to_create?: boolean;
actions?: Array<{ action?: string; address?: { type?: string; name?: string } }>;
}>(stdout);
expect(data.resource).toEqual(
expect.objectContaining({ type: "deployment", key: "daily-report" }),
);
expect(data.yaml_written).toBe(false);
expect(data.ready_to_create).toBe(true);
expect(data.actions?.filter((action) => action.action !== "no-op")).toEqual([
expect.objectContaining({
action: "create",
address: expect.objectContaining({ type: "deployment", name: "daily-report" }),
}),
]);
expect(await readFile(configPath, "utf8")).toBe(configSource);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("vault credential create --dry-run 不读取远端、不要求 Secret、不写 YAML", async () => {
const directory = await mkdtemp(join(tmpdir(), "bl-vault-credential-create-e2e-"));
const configPath = join(directory, "agents.yaml");
const configSource = `version: "1"
providers:
bailian:
api_key: test
workspace_id: ws_e2e
defaults:
provider: bailian
vaults:
production:
display_name: Production
credentials: []
`;
await writeFile(configPath, configSource, "utf8");
try {
await seedTrackedResources(configPath, [
{ type: "vault", name: "production", provider: "bailian" },
]);
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"vault",
"credential",
"create",
"--dry-run",
"--vault",
"production",
"--name",
"api-token",
"--secret-name",
"API_TOKEN",
"--secret-env",
"E2E_SECRET_THAT_IS_NOT_SET",
"--file",
configPath,
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
credential?: { name?: string; secret_name?: string; secret_env?: string };
yaml_written?: boolean;
remote_checked?: boolean;
reuse_remote?: boolean;
}>(stdout);
expect(data.credential).toEqual({
name: "api-token",
secret_name: "API_TOKEN",
secret_env: "E2E_SECRET_THAT_IS_NOT_SET",
});
expect(data.yaml_written).toBe(false);
expect(data.remote_checked).toBe(false);
expect(data.reuse_remote).toBe(false);
expect(stdout).not.toContain("__dry_run_secret__");
expect(await readFile(configPath, "utf8")).toBe(configSource);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
test("destroy --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"destroy",
"--dry-run",
"--cascade",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_destroy?: { cascade?: boolean } }>(stdout);
expect(data.would_destroy?.cascade).toBe(true);
});
test("session create --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"create",
"--dry-run",
"--agent",
"assistant",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_create_session?: { agent?: string } }>(stdout);
expect(data.would_create_session?.agent).toBe("assistant");
});
test("session delete --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"delete",
"--dry-run",
"--session-id",
"sess_e2e",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_delete_session?: string }>(stdout);
expect(data.would_delete_session).toBe("sess_e2e");
});
test("session send --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"send",
"--dry-run",
"--session-id",
"sess_e2e",
"--message",
"干跑",
"--no-stream",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_send?: { session_id?: string; message?: string; mode?: string };
}>(stdout);
expect(data.would_send?.session_id).toBe("sess_e2e");
expect(data.would_send?.message).toBe("干跑");
expect(data.would_send?.mode).toBe("polling");
});
test("session run --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"session",
"run",
"--dry-run",
"--prompt",
"干跑",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{
would_run?: { prompt?: string; mode?: string };
}>(stdout);
expect(data.would_run?.prompt).toBe("干跑");
expect(data.would_run?.mode).toBe("streaming");
});
test("state rm --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"state",
"rm",
"--dry-run",
"--address",
"bailian.agent.assistant",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_remove?: string }>(stdout);
expect(data.would_remove).toBe("bailian.agent.assistant");
});
test("state import --dry-run 仅输出计划", async () => {
const { stdout, stderr, exitCode } = await runCommandE2e(MANAGED_AGENT_ROUTES, [
"managed-agent",
"state",
"import",
"--dry-run",
"--address",
"bailian.agent.assistant",
"--remote-id",
"agent-e2e",
"--output",
"json",
]);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ would_import?: string; remote_id?: string }>(stdout);
expect(data.would_import).toBe("bailian.agent.assistant");
expect(data.remote_id).toBe("agent-e2e");
});
});