Files
chenanran555 a92c58420b fix(managed-agent): use CLI credentials for directory projects
Make project init fully offline, stop generating project-local credential files, bind Publish to the reviewed plan fingerprint, remove the unsafe Apply refresh-only mode, and document Agent-owned File mounts.
2026-09-04 11:22:11 +08:00

244 lines
8.0 KiB
TypeScript

import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, beforeEach, expect, test } from "vite-plus/test";
import {
type ApiKeyCredential,
Client,
ExitCode,
type Identity,
type Settings,
} from "bailian-cli-core";
import {
assertProviderCredentials,
type CredentialHost,
injectProviderCredentials,
prepareProviderEnv,
scrubCredentialEnv,
} from "../src/commands/managed-agent/_engine/credentials.ts";
/**
* 凭证内存注入管道:injectProviderCredentials 把 authStage 解析进 Client 的凭证
* 权威覆写 bailian 配置块(不落 env),scrubCredentialEnv 清空所有凭证 env,
* assertProviderCredentials 对任一已声明 provider 的空 key 给 CLI 权威 AUTH
* 错误(离线命令跳过断言)。用快照隔离凭证 env。
*/
const TRACKED_ENV = [
"DASHSCOPE_API_KEY",
"BAILIAN_API_KEY",
"BAILIAN_BASE_URL",
"BAILIAN_WORKSPACE_ID",
"ANTHROPIC_API_KEY",
"CLAUDE_API_KEY",
"ARK_API_KEY",
"QODER_PAT",
"QODER_API_KEY",
"AGENTS_CONFIG_PATH",
"AGENTS_PROVIDER",
];
let envSnapshot: Record<string, string | undefined> = {};
beforeEach(() => {
envSnapshot = Object.fromEntries(TRACKED_ENV.map((key) => [key, process.env[key]]));
});
afterEach(() => {
for (const key of TRACKED_ENV) {
const value = envSnapshot[key];
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
});
const identity: Identity = {
binName: "bl",
version: "0.0.0-test",
npmPackage: "bailian-cli",
clientName: "bailian-cli",
};
function makeHost(options: { apiCred?: ApiKeyCredential; workspaceId?: string }): CredentialHost {
const settings = { workspaceId: options.workspaceId } as Settings;
return {
settings,
client: new Client({
identity,
settings,
baseUrl: options.apiCred?.baseUrl ?? "https://dashscope.aliyuncs.com",
apiCred: options.apiCred,
}),
};
}
function bailianCred(
token = "sk-auth-chain",
baseUrl = "https://dashscope.aliyuncs.com",
): ApiKeyCredential {
return { token, baseUrl, source: "config" };
}
test("inject:bailian api_key 无条件覆盖(含 yaml 字面量),base_url 空则拼 agentstudio 后缀", () => {
const providers = { bailian: { api_key: "literal-from-yaml", base_url: "" } };
injectProviderCredentials(providers, makeHost({ apiCred: bailianCred() }));
expect(providers.bailian.api_key).toBe("sk-auth-chain");
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
});
test("inject:base_url 已带后缀不重复拼;非空字面量 base_url 保留", () => {
const withSuffix = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(
withSuffix,
makeHost({
apiCred: bailianCred("t", "https://x.maas.aliyuncs.com/api/v1/agentstudio"),
}),
);
expect(withSuffix.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio");
const literal = {
bailian: {
api_key: "",
base_url: "https://custom.example.com/api/v1/agentstudio",
},
};
injectProviderCredentials(literal, makeHost({ apiCred: bailianCred() }));
expect(literal.bailian.base_url).toBe("https://custom.example.com/api/v1/agentstudio");
});
test("inject:目录项目可强制使用 Bailian CLI 解析出的 base_url", () => {
const providers = {
bailian: {
api_key: "from-project-env",
base_url: "https://project-env.example.com/api/v1/agentstudio",
},
};
injectProviderCredentials(providers, makeHost({ apiCred: bailianCred() }), {
overrideBaseUrl: true,
});
expect(providers.bailian.api_key).toBe("sk-auth-chain");
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
});
test("inject:base_url 尾斜杠被规范化,不产生双斜杠", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(
providers,
makeHost({ apiCred: bailianCred("t", "https://dashscope.aliyuncs.com/") }),
);
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
});
test("inject:已带后缀且尾斜杠的 base_url 去斜杠后原样保留", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(
providers,
makeHost({
apiCred: bailianCred("t", "https://x.maas.aliyuncs.com/api/v1/agentstudio/"),
}),
);
expect(providers.bailian.base_url).toBe("https://x.maas.aliyuncs.com/api/v1/agentstudio");
});
test("inject:workspace_id 引用且为空时用 settings 填充;有字面量则保留", () => {
const empty = { bailian: { api_key: "", workspace_id: "" } };
injectProviderCredentials(
empty,
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
);
expect(empty.bailian.workspace_id).toBe("ws-settings");
const literal = { bailian: { api_key: "", workspace_id: "ws-yaml" } };
injectProviderCredentials(
literal,
makeHost({ apiCred: bailianCred(), workspaceId: "ws-settings" }),
);
expect(literal.bailian.workspace_id).toBe("ws-yaml");
});
test("inject:无凭证时 api_key 保持不变,base_url 仍用 client 默认域名补齐(离线/范围外 schema 可用)", () => {
const providers = { bailian: { api_key: "", base_url: "" } };
injectProviderCredentials(providers, makeHost({}));
expect(providers.bailian.api_key).toBe("");
expect(providers.bailian.base_url).toBe("https://dashscope.aliyuncs.com/api/v1/agentstudio");
});
test("inject:非 bailian provider 块不被触碰", () => {
const providers = {
claude: { api_key: "sk-ant" },
ark: { api_key: "ark-key" },
};
injectProviderCredentials(providers, makeHost({ apiCred: bailianCred() }));
expect(providers.claude.api_key).toBe("sk-ant");
expect(providers.ark.api_key).toBe("ark-key");
});
test("assert:所有已声明 provider 的 key 非空时通过", () => {
expect(() =>
assertProviderCredentials({
bailian: { api_key: "x" },
claude: { api_key: "y" },
}),
).not.toThrow();
});
test("assert:定向检查不会被无关 provider 的空 key 阻塞", () => {
expect(() =>
assertProviderCredentials(
{
bailian: { api_key: "x" },
claude: { api_key: "" },
},
["bailian"],
),
).not.toThrow();
});
test("assert:claude key 为空抛 AUTH 且 hint 指向 ANTHROPIC_API_KEY", () => {
let thrown: unknown;
try {
assertProviderCredentials({ claude: { api_key: "" } });
} catch (error) {
thrown = error;
}
const err = thrown as { exitCode?: number; hint?: string; message?: string };
expect(err.exitCode).toBe(ExitCode.AUTH);
expect(err.hint).toContain("ANTHROPIC_API_KEY");
expect(err.message).toContain("claude");
});
test("assert:bailian key 为空(dry-run/未登录)抛 AUTH 且 hint 指向 bl auth login", () => {
let thrown: unknown;
try {
assertProviderCredentials({ bailian: { api_key: "" } });
} catch (error) {
thrown = error;
}
const err = thrown as { exitCode?: number; hint?: string };
expect(err.exitCode).toBe(ExitCode.AUTH);
expect(err.hint).toContain("bl auth login");
});
test("scrub:所有凭证 env 变量被删除", () => {
process.env.DASHSCOPE_API_KEY = "x";
process.env.ANTHROPIC_API_KEY = "y";
process.env.ARK_API_KEY = "z";
process.env.BAILIAN_BASE_URL = "u";
process.env.QODER_PAT = "q";
scrubCredentialEnv();
expect(process.env.DASHSCOPE_API_KEY).toBeUndefined();
expect(process.env.ANTHROPIC_API_KEY).toBeUndefined();
expect(process.env.ARK_API_KEY).toBeUndefined();
expect(process.env.BAILIAN_BASE_URL).toBeUndefined();
expect(process.env.QODER_PAT).toBeUndefined();
});
test("prepare:调用后凭证变量均已定义,避免 yaml 插值抛错", () => {
// 指向不存在的 agents 配置,隔离宿主 ~/.agents/config.json 干扰
process.env.AGENTS_CONFIG_PATH = join(tmpdir(), "no-such-agents-config.json");
delete process.env.ARK_API_KEY;
delete process.env.QODER_API_KEY;
prepareProviderEnv();
// 契约:每个凭证变量都被占位或填充,插值不会因 undefined 抛错
expect(process.env.ARK_API_KEY).toBeDefined();
expect(process.env.QODER_API_KEY).toBeDefined();
});