Files
modelstudioai__cli/packages/commands/tests/e2e/managed-agent-auth-chain.e2e.test.ts
T
2026-07-27 21:33:36 +08:00

299 lines
12 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 { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vite-plus/test";
import { e2eFixturesDir, parseStdoutJson, runCommandE2e } from "./helpers.ts";
import { MANAGED_AGENT_ROUTES } from "./topic-routes.ts";
/**
* managed-agent 凭证链 e2e验证 bl 自有配置体系config 写入 / 命名 Profile /
* logout与错误映射如何流入 SDK 引擎。全部离线:凭证门禁用 `managed-agent plan`
* 验证(空 state 不发网络请求,但 auth: "apiKey" 硬门禁 + 引擎全量 provider key
* 断言照常生效);`validate` / `state list` 属离线命令,无凭证也必须可用。
* 配置一律通过 BAILIAN_CONFIG_DIR 指向临时目录,绝不触碰真实用户配置。
*/
const ROUTES = {
...MANAGED_AGENT_ROUTES,
"auth logout": "authLogout",
};
const AGENTS_YAML = join(e2eFixturesDir, "managed-agent", "agents.yaml");
const AGENTS_YAML_INVALID = join(e2eFixturesDir, "managed-agent", "agents-invalid.yaml");
const AGENTS_YAML_MULTI = join(e2eFixturesDir, "managed-agent", "agents-multi.yaml");
const tempDirs: string[] = [];
afterEach(() => {
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
});
/** 新建隔离配置目录并写入 config.json返回子进程 env 覆盖(清空外部凭证 env。 */
function makeConfigEnv(config: Record<string, unknown>): NodeJS.ProcessEnv {
const configDir = mkdtempSync(join(tmpdir(), "bl-managed-agent-auth-"));
tempDirs.push(configDir);
writeFileSync(join(configDir, "config.json"), `${JSON.stringify(config, null, 2)}\n`);
return {
BAILIAN_CONFIG_DIR: configDir,
DASHSCOPE_API_KEY: "",
DASHSCOPE_BASE_URL: "",
BAILIAN_BASE_URL: "",
BAILIAN_WORKSPACE_ID: "",
};
}
function validateArgs(file: string): string[] {
return ["managed-agent", "validate", "--file", file, "--quiet"];
}
/** plan 是凭证门禁命令:空 state 下不发网络,但 authStage + 引擎断言照常生效。 */
function planArgs(file: string): string[] {
return ["managed-agent", "plan", "--file", file, "--quiet"];
}
/** 隔离宿主机的 ~/.agents/config.json避免它强制覆盖 provider 凭证 env。 */
function isolatedAgentsConfigEnv(): NodeJS.ProcessEnv {
return { AGENTS_CONFIG_PATH: join(tmpdir(), "bl-e2e-no-agents-config.json") };
}
/**
* 在临时目录里搭一套非空 state 的项目agents.yaml 复用单 provider fixture
* agents.state.json 预置一条已追踪资源 —— 非空 state 是触发 plan 默认 refresh
* 路径的前提,用于验证 --dry-run 强制离线。目录纳入 tempDirs 自动清理。
*/
function makeStatefulProject(): { configPath: string; statePath: string } {
const dir = mkdtempSync(join(tmpdir(), "bl-managed-agent-dry-run-"));
tempDirs.push(dir);
const configPath = join(dir, "agents.yaml");
const statePath = join(dir, "agents.state.json");
writeFileSync(configPath, readFileSync(AGENTS_YAML, "utf8"));
writeFileSync(
statePath,
`${JSON.stringify(
{
resources: [
{
address: { provider: "bailian", type: "agent", name: "assistant" },
remote_id: "agent-e2e-dry-run",
},
],
},
null,
2,
)}\n`,
);
return { configPath, statePath };
}
/** 分配一个刚释放的本地端口,连接必然 ECONNREFUSED用于网络错误场景。 */
async function closedPort(): Promise<number> {
const server = createServer();
try {
await new Promise<void>((resolveListen) => server.listen(0, "127.0.0.1", resolveListen));
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("failed to allocate a closed port");
}
return address.port;
} finally {
await new Promise<void>((resolveClose) => server.close(() => resolveClose()));
}
}
describe("e2e: managed-agent 凭证链config 写入 / Profile / logout / 错误映射)", () => {
test("config.json 写入的 api_key 流入引擎plan 离线通过", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-config-write" });
const { stderr, exitCode } = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(exitCode, stderr).toBe(0);
});
test("active_config 指向的命名 Profile 提供凭证时通过", async () => {
const env = makeConfigEnv({
work: { api_key: "sk-e2e-profile-work" },
active_config: "work",
});
const { stderr, exitCode } = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(exitCode, stderr).toBe(0);
});
test("active_config 切到无凭证 Profile 时报统一 AUTH 错误 (3)", async () => {
const env = makeConfigEnv({
work: { api_key: "sk-e2e-profile-work" },
empty: {},
active_config: "empty",
});
const { stderr, exitCode } = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/auth login|API key/i);
});
test("auth logout 清除凭证后 plan 报 AUTH而非用残留凭证", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-before-logout" });
const before = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(before.exitCode, before.stderr).toBe(0);
const logout = await runCommandE2e(ROUTES, ["auth", "logout"], env);
expect(logout.exitCode, logout.stderr).toBe(0);
const after = await runCommandE2e(ROUTES, planArgs(AGENTS_YAML), env);
expect(after.exitCode).toBe(3);
expect(after.stderr).toMatch(/auth login|API key/i);
});
test("agents.yaml schema 错误映射为 USAGE (2),不透传原始 zod dump", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
validateArgs(AGENTS_YAML_INVALID),
env,
);
expect(exitCode).toBe(2);
expect(stderr).toMatch(/agents/i);
expect(stderr).not.toMatch(/"code":\s*"invalid_type"/);
});
test("validate --output json 成功路径 stdout 为单个合法 JSON", async () => {
const env = makeConfigEnv({ api_key: "sk-e2e-config-write" });
const { stdout, stderr, exitCode } = await runCommandE2e(
ROUTES,
["managed-agent", "validate", "--file", AGENTS_YAML, "--output", "json"],
env,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ valid?: boolean; diagnostics?: unknown[] }>(stdout);
expect(data.valid).toBe(true);
expect(Array.isArray(data.diagnostics)).toBe(true);
});
test("SDK fetch 连不上时映射为 NETWORK (6) + errno hint不降级成 GENERAL", async () => {
const port = await closedPort();
const env = makeConfigEnv({
api_key: "sk-e2e-network",
base_url: `http://127.0.0.1:${port}`,
});
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
["managed-agent", "session", "get", "--session-id", "sess_net", "--file", AGENTS_YAML],
env,
);
expect(exitCode).toBe(6);
expect(stderr).toMatch(/Network request failed/i);
expect(stderr).toMatch(/ECONNREFUSED|refused/i);
});
});
describe("e2e: managed-agent 鉴权分层(离线命令免登录 / 联网命令统一 apiKey 门禁)", () => {
test("validate 无任何凭证也离线通过 (0)", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(ROUTES, validateArgs(AGENTS_YAML), env);
expect(exitCode, stderr).toBe(0);
});
test("state list 无任何凭证也离线通过 (0)stdout 为合法 JSON", async () => {
const env = makeConfigEnv({});
const { stdout, stderr, exitCode } = await runCommandE2e(
ROUTES,
["managed-agent", "state", "list", "--file", AGENTS_YAML, "--output", "json"],
env,
);
expect(exitCode, stderr).toBe(0);
const data = parseStdoutJson<{ resources?: unknown[] }>(stdout);
expect(Array.isArray(data.resources)).toBe(true);
});
test("plan --no-refresh 无登录时仍被 apiKey 硬门禁拦住 (3)", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML), "--no-refresh"],
env,
);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/auth login|API key/i);
});
test("已登录 bailian 时,多 provider 配置下 plan --no-refresh 离线通过,不查其他 provider key (0)", async () => {
const env = {
...makeConfigEnv({ api_key: "sk-e2e-no-refresh" }),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--no-refresh"],
env,
);
expect(exitCode, stderr).toBe(0);
});
test("统一登录门禁:只配 claude key 未登录 bailian 时plan --provider claude 仍报 AUTH (3)", async () => {
const env = {
...makeConfigEnv({}),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "sk-ant-e2e-scope",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--provider", "claude"],
env,
);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/auth login|API key/i);
});
test("已登录但缺 claude key 时,全量断言拦住并给 ANTHROPIC_API_KEY hint (3)", async () => {
const env = {
...makeConfigEnv({ api_key: "sk-e2e-bailian-present" }),
...isolatedAgentsConfigEnv(),
ANTHROPIC_API_KEY: "",
CLAUDE_API_KEY: "",
};
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML_MULTI), "--provider", "claude"],
env,
);
expect(exitCode).toBe(3);
expect(stderr).toMatch(/ANTHROPIC_API_KEY/);
});
});
describe("e2e: plan --dry-run 离线契约(不联网 / 不写 state / 免凭证)", () => {
test("无任何凭证时 plan --dry-run 不报 AUTH离线出 plan (0)", async () => {
const env = makeConfigEnv({});
const { stderr, exitCode } = await runCommandE2e(
ROUTES,
[...planArgs(AGENTS_YAML), "--dry-run"],
env,
);
expect(exitCode, stderr).toBe(0);
});
test("有凭证且 state 非空时,--dry-run 跳过 refresh不发请求、state 文件不变;同环境不加 --dry-run 则证明会联网", async () => {
const { configPath, statePath } = makeStatefulProject();
// base_url 指向必然 ECONNREFUSED 的本地端口:一旦 refresh 真发请求必现形。
// refresh 对 API 错误优雅降级(不影响退出码),因此用 stderr 的
// "Failed to refresh" 告警作为「发过请求」的观测信号。
const port = await closedPort();
const env = makeConfigEnv({
api_key: "sk-e2e-dry-run",
base_url: `http://127.0.0.1:${port}`,
});
const stateBefore = readFileSync(statePath, "utf8");
// 对照组:不加 --dry-run默认 refresh 路径真实访问远端 → 出现 refresh 失败告警。
const withoutDryRun = await runCommandE2e(ROUTES, planArgs(configPath), env);
expect(withoutDryRun.stderr).toMatch(/Failed to refresh/i);
// --dry-run同环境必须完全离线成功无任何 refresh 痕迹,且不回写 state 文件。
const withDryRun = await runCommandE2e(ROUTES, [...planArgs(configPath), "--dry-run"], env);
expect(withDryRun.exitCode, withDryRun.stderr).toBe(0);
expect(withDryRun.stderr).not.toMatch(/Failed to refresh/i);
expect(readFileSync(statePath, "utf8")).toBe(stateBefore);
});
});