test(runtime): move unit tests from cli to runtime package

This commit is contained in:
若麒
2026-06-23 19:32:21 +08:00
parent d971a04fb8
commit ad236e9b11
3 changed files with 246 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
import { expect, test } from "vite-plus/test";
import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core";
import { parseFlags } from "../src/args.ts";
import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts";
const IMAGE_GENERATE_OPTIONS = [
{ flag: "--prompt <text>", description: "Image description", required: true },
{ flag: "--model <model>", description: "Model ID" },
{ flag: "--watermark <bool>", description: BOOL_FLAG_WATERMARK },
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
];
test("parseFlags rejects unknown long flags", () => {
expect(() =>
parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
).toThrowError(
expect.objectContaining({
name: "BailianError",
exitCode: ExitCode.USAGE,
message: expect.stringContaining('Unknown flag "--xxxx"'),
}),
);
});
test("parseFlags rejects unknown flags with = syntax", () => {
expect(() =>
parseFlags(
["--prompt=cat", "--unknown-flag=yes"],
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
),
).toThrow(/Unknown flag "--unknown-flag"/);
});
test("parseFlags accepts defined command and global flags", () => {
const flags = parseFlags(
["--quiet", "--prompt", "cat", "--watermark", "false"],
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
);
expect(flags.quiet).toBe(true);
expect(flags.prompt).toBe("cat");
expect(flags.watermark).toBe("false");
});
test("parseFlags rejects value flag when next token is another flag", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
for (const argv of [
["--watermark", "--prompt", "cat"],
["--watermark", "-h"],
["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"],
]) {
expect(() => parseFlags(argv, opts)).toThrowError(
expect.objectContaining({
name: "BailianError",
exitCode: ExitCode.USAGE,
message: expect.stringContaining("Flag --watermark requires a value"),
}),
);
}
});
test("parseFlags rejects trailing value flag without value", () => {
expect(() =>
parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
).toThrowError(
expect.objectContaining({
message: expect.stringContaining("Flag --watermark requires a value"),
}),
);
});
test("parseFlags allows boolean flags without values adjacent to other flags", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
const flags = parseFlags(
["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"],
opts,
);
expect(flags.quiet).toBe(true);
expect(flags.dryRun).toBe(true);
expect(flags.noWait).toBe(true);
expect(flags.prompt).toBe("cat");
expect(flags.watermark).toBe("false");
});
test("parseFlags does not treat the next flag as a boolean flag value", () => {
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError(
expect.objectContaining({
message: expect.stringContaining("Flag --prompt requires a value"),
}),
);
// --dry-run is boolean: no value check; parsing continues to --prompt.
const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts);
expect(flags.dryRun).toBe(true);
expect(flags.prompt).toBe("cat");
});
+109
View File
@@ -0,0 +1,109 @@
import { expect, test } from "vite-plus/test";
import { createStepDispatcher } from "../src/pipeline/dispatcher.ts";
import { executePipeline } from "../src/pipeline/executor.ts";
import { collectPipelineIssues } from "../src/pipeline/validation.ts";
import { getByJsonPointer } from "../src/pipeline/schema.ts";
import { normalizeConcurrency } from "../src/pipeline/scheduler.ts";
import { WORKFLOW_VERSION, type PipelineDefinition } from "../src/pipeline/types.ts";
test("cli package skeleton", () => {
expect(true).toBe(true);
});
test("pipeline execution can use an isolated step dispatcher", async () => {
const dispatcher = createStepDispatcher();
dispatcher.registerStep("test/echo", (input, ctx) => ({
data: { input, hasSignal: !!ctx.signal },
}));
const controller = new AbortController();
const pipeline: PipelineDefinition = {
version: WORKFLOW_VERSION,
steps: [{ id: "echo", type: "test/echo", input: { message: "hello" } }],
};
const report = await executePipeline(
pipeline,
{},
{
stepDispatcher: dispatcher,
signal: controller.signal,
},
);
expect(report.status).toBe("succeeded");
expect(report.steps[0]?.output?.data).toEqual({
input: { message: "hello" },
hasSignal: true,
});
});
test("dry-run never executes $js expressions (preview must not run code)", async () => {
const dispatcher = createStepDispatcher();
dispatcher.registerStep("test/echo", (input) => ({ data: input }));
const flag = "__bailian_dryrun_should_not_run__";
delete (globalThis as Record<string, unknown>)[flag];
const pipeline: PipelineDefinition = {
version: WORKFLOW_VERSION,
steps: [
{
id: "s1",
type: "test/echo",
input: { probe: { $js: `(globalThis[${JSON.stringify(flag)}] = true), 1` } },
},
],
};
const report = await executePipeline(pipeline, {}, { stepDispatcher: dispatcher, dryRun: true });
expect(report.status).toBe("planned");
expect((globalThis as Record<string, unknown>)[flag]).toBeUndefined();
});
test("script/js rejects non-literal code sourced from another step ($from)", () => {
const dispatcher = createStepDispatcher();
dispatcher.registerStep("test/echo", (input) => ({ data: input }));
dispatcher.registerStep("script/js", () => ({ data: {} }));
const pipeline: PipelineDefinition = {
version: WORKFLOW_VERSION,
steps: [
{ id: "gen", type: "test/echo", input: { message: "x" } },
{
id: "run",
type: "script/js",
input: { code: { $from: "gen", path: "/data/message" } as never },
},
],
};
const issues = collectPipelineIssues(pipeline, dispatcher);
expect(issues.some((issue) => issue.includes('literal string "code"'))).toBe(true);
});
test("script/js accepts a literal string code", () => {
const dispatcher = createStepDispatcher();
dispatcher.registerStep("script/js", () => ({ data: {} }));
const pipeline: PipelineDefinition = {
version: WORKFLOW_VERSION,
steps: [{ id: "run", type: "script/js", input: { code: "return 1" } }],
};
expect(collectPipelineIssues(pipeline, dispatcher)).toEqual([]);
});
test("getByJsonPointer refuses prototype keys and inherited properties", () => {
const obj = { a: { b: 1 } };
expect(getByJsonPointer(obj, "/a/b")).toBe(1);
expect(getByJsonPointer(obj, "/__proto__")).toBeUndefined();
expect(getByJsonPointer(obj, "/constructor")).toBeUndefined();
expect(getByJsonPointer(obj, "/a/constructor/constructor")).toBeUndefined();
expect(getByJsonPointer(obj, "/toString")).toBeUndefined();
});
test("normalizeConcurrency clamps to a safe maximum", () => {
expect(normalizeConcurrency(undefined)).toBe(1);
expect(normalizeConcurrency(4)).toBe(4);
expect(normalizeConcurrency(100000)).toBe(64);
});
+42
View File
@@ -0,0 +1,42 @@
import { expect, test } from "vite-plus/test";
import { readProxyEnv } from "../src/proxy.ts";
test("readProxyEnv: 未设置任何代理变量时全部为 undefined", () => {
expect(readProxyEnv({})).toEqual({
httpProxy: undefined,
httpsProxy: undefined,
noProxy: undefined,
});
});
test("readProxyEnv: 空白值视为未设置", () => {
expect(readProxyEnv({ HTTPS_PROXY: "", HTTP_PROXY: " ", NO_PROXY: "" })).toEqual({
httpProxy: undefined,
httpsProxy: undefined,
noProxy: undefined,
});
});
test("readProxyEnv: 大小写变量均可识别,小写优先", () => {
expect(readProxyEnv({ HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe("http://upper:1");
expect(readProxyEnv({ https_proxy: "http://lower:1" }).httpsProxy).toBe("http://lower:1");
expect(
readProxyEnv({ https_proxy: "http://lower:1", HTTPS_PROXY: "http://upper:1" }).httpsProxy,
).toBe("http://lower:1");
});
test("readProxyEnv: 空字符串小写变量不屏蔽已设置的大写变量", () => {
expect(readProxyEnv({ https_proxy: "", HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe(
"http://upper:1",
);
expect(readProxyEnv({ http_proxy: "", HTTP_PROXY: "http://upper:2" }).httpProxy).toBe(
"http://upper:2",
);
});
test("readProxyEnv: NO_PROXY 独立读取", () => {
const r = readProxyEnv({ NO_PROXY: "*.aliyuncs.com" });
expect(r.noProxy).toBe("*.aliyuncs.com");
expect(r.httpProxy).toBeUndefined();
expect(r.httpsProxy).toBeUndefined();
});