Files
modelstudioai__cli/packages/cli/tests/e2e/helpers.ts
T
若麒 66402d9868 chore(dev): run CLI workspace entries from source
- use tsx for bl/kscli dev and test runners
- point local library exports to src while keeping publishConfig on dist
- switch vendored telemetry modules to .cjs for source-run compatibility
- update stress/e2e helpers and agent docs for the new source execution path
2026-07-09 19:39:13 +08:00

216 lines
7.4 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 { execFile } from "child_process";
import { mkdirSync, readFileSync } from "fs";
import { promisify } from "util";
import { basename, dirname, join } from "path";
import { fileURLToPath } from "url";
import { readConfigFile } from "bailian-cli-core";
const execFileAsync = promisify(execFile);
/**
* Vitest `global-setup.ts` 写入 `test/output/` 下本文件名,供各 worker 进程读取同一会话 id。
* (仅模块内变量无法跨 Vitest 多进程 worker 共享。)
*/
export const E2E_RUN_SESSION_FILENAME = ".e2e-run-session";
/**
* 单次 `vp test` / Vitest 运行共用的 E2E 输出会话目录名(惰性缓存于当前进程)。
*/
let e2eOutputSessionId: string | undefined;
/** `packages/cli` 根目录(含 `src/main.ts` */
export const cliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
const mainTs = join(cliPackageRoot, "src", "main.ts");
/** Monorepo 根(含根 `package.json` */
export function monorepoRoot(): string {
return join(cliPackageRoot, "..", "..");
}
function localBin(name: string): string {
return join(
monorepoRoot(),
"node_modules",
".bin",
process.platform === "win32" ? `${name}.cmd` : name,
);
}
function readE2eRunSessionFromOutputDir(): string | undefined {
try {
const p = join(monorepoRoot(), "test", "output", E2E_RUN_SESSION_FILENAME);
const t = readFileSync(p, "utf8").trim();
return t.length > 0 ? t : undefined;
} catch {
return undefined;
}
}
function getE2eOutputSessionId(): string {
if (!e2eOutputSessionId) {
const fromEnv = process.env.BAILIAN_E2E_RUN_ID?.trim();
if (fromEnv) {
e2eOutputSessionId = fromEnv.replace(/[^a-zA-Z0-9._-]+/g, "-");
} else {
const fromFile = readE2eRunSessionFromOutputDir();
if (fromFile) {
e2eOutputSessionId = fromFile.replace(/[^a-zA-Z0-9._-]+/g, "-");
} else {
e2eOutputSessionId = `e2e-run-${Date.now()}-${process.pid}`;
}
}
}
return e2eOutputSessionId;
}
/**
* 在 `test/output/<会话>/` 下创建用例子目录。
* 会话 id 优先 `BAILIAN_E2E_RUN_ID`,否则读 Vitest globalSetup 写入的 `test/output/.e2e-run-session`
* 再否则回退为单进程 id非 Vitest 直接跑用例时)。
* 若已设 `BAILIAN_E2E_OUT` 则直接使用(不再套会话目录)。
*/
export function makeE2eOutputDir(label: string): string {
const fromEnv = process.env.BAILIAN_E2E_OUT?.trim();
if (fromEnv) {
mkdirSync(fromEnv, { recursive: true });
return fromEnv;
}
const safe = label.replace(/[^a-zA-Z0-9._-]+/g, "-");
const sessionDir = join(monorepoRoot(), "test", "output", getE2eOutputSessionId());
mkdirSync(sessionDir, { recursive: true });
const dir = join(sessionDir, `e2e-vp-${safe}-${Date.now()}`);
mkdirSync(dir, { recursive: true });
return dir;
}
/** 全局 `--timeout` 秒数(视频等长任务) */
export function cliTimeoutSeconds(): string {
return process.env.BAILIAN_E2E_TIMEOUT_SEC?.trim() || "3600";
}
export function cliTimeoutPrefix(): string[] {
return ["--timeout", cliTimeoutSeconds()];
}
/** 显式开启后才跑真实网络 E2E避免默认 `vp test` 依赖密钥或打外网 */
export function isBailianE2EEnabled(): boolean {
return process.env.BAILIAN_E2E === "1";
}
/** 可调 DashScope 的 API Key环境变量优先否则读 ~/.bailian/config.json */
export function isDashScopeE2EReady(): boolean {
if (!isBailianE2EEnabled()) return false;
if (process.env.DASHSCOPE_API_KEY?.trim()) return true;
try {
const f = readConfigFile();
return typeof f.api_key === "string" && f.api_key.length > 0;
} catch {
return false;
}
}
/**
* Console-gateway 命令quota / usage free / usage stats的 E2E 就绪检查:
* 需 `BAILIAN_E2E=1` 且存在 console access_token`~/.bailian/config.json` 的
* `access_token`;凭证解析已集中到 authStage不再读环境变量
*
* 仅检查 token 是否存在——无法本地判断是否过期。token 过期时 gated 用例仍会执行,
* 但用 `isConsoleAuthFailure` 把“session 未登录/已过期”的优雅报错视为通过,保持
* 与 deploy/dataset “无 key / 有效 key / 失效 key 均绿”的一致策略。
*/
export function isConsoleE2EReady(): boolean {
if (!isBailianE2EEnabled()) return false;
try {
const config = readConfigFile();
return typeof config.access_token === "string" && config.access_token.length > 0;
} catch {
return false;
}
}
/** 语音与图像(可设 `BAILIAN_E2E_MEDIA=0` 在仅跑文本/记忆/知识库时跳过) */
export function isBailianE2EMediaEnabled(): boolean {
if (process.env.BAILIAN_E2E_MEDIA === "0") return false;
return isBailianE2EEnabled();
}
/** 文生视频 / 图生视频 / 参考视频 / 视频编辑(耗时长,默认关闭) */
export function isBailianE2EVideoEnabled(): boolean {
return isBailianE2EEnabled() && process.env.BAILIAN_E2E_VIDEO === "1";
}
/** 从 `import.meta.url` 生成 OUT 子目录标签,避免并行用例目录冲突 */
export function e2eLabelFromMetaUrl(metaUrl: string): string {
return basename(fileURLToPath(metaUrl), ".ts").replace(/\.e2e\.test$/, "");
}
/** 知识库用例:须显式索引 ID + API-KEY */
export function isKnowledgeE2EReady(): boolean {
if (!isBailianE2EEnabled()) return false;
if (!process.env.BAILIAN_E2E_INDEX_ID) return false;
return isDashScopeE2EReady();
}
export interface RunCliResult {
stdout: string;
stderr: string;
exitCode: number;
}
/**
* 子进程执行 CLI等价于在 `packages/cli` 下 `tsx src/main.ts ...`)。
* request_id 等诊断信息在 stderr`--output json` 时 JSON 在 stdout。
*/
export async function runCli(
args: string[],
envOverrides: NodeJS.ProcessEnv = {},
): Promise<RunCliResult> {
try {
const { stdout, stderr } = await execFileAsync(localBin("tsx"), [mainTs, ...args], {
cwd: cliPackageRoot,
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
env: {
...process.env,
NODE_NO_WARNINGS: "1",
DO_NOT_TRACK: "1",
...envOverrides,
},
});
return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 };
} catch (err: unknown) {
const e = err as {
stdout?: string;
stderr?: string;
code?: number;
};
return {
stdout: e.stdout ?? "",
stderr: e.stderr ?? "",
exitCode: typeof e.code === "number" ? e.code : 1,
};
}
}
export function parseStdoutJson<T = unknown>(stdout: string): T {
const t = stdout.trim();
// Extract JSON object — stdout may contain [perf] console.time lines before JSON
const jsonMatch = t.match(/\{[\s\S]*\}/);
if (!jsonMatch) throw new Error(`No JSON object found in stdout: ${t.slice(0, 200)}`);
return JSON.parse(jsonMatch[0]) as T;
}
/**
* 判断一次 CLI 运行是否因 console session 未登录/已过期而失败。
*
* Console E2E 用例的 readiness 闸(`isConsoleE2EReady`)只能判断 token 是否存在,
* 无法判断是否过期token 失效时 gated 用例仍会执行并拿到鉴权错误。本函数让用例
* 参考 deploy/dataset 的做法:只要 CLI 把鉴权错误优雅上抛(非零退出 + stderr 说明
* session 失效),即视为通过,而不是强求 exit 0 的成功输出。
*/
export function isConsoleAuthFailure(result: RunCliResult): boolean {
if (result.exitCode === 0) return false;
return /not logged in|has expired|NotLogined|Run `bl auth login/i.test(result.stderr);
}