mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
test(runtime): move unit tests from cli to runtime package
This commit is contained in:
@@ -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");
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user