Skip to content

Commit ad236e9

Browse files
committed
test(runtime): move unit tests from cli to runtime package
1 parent d971a04 commit ad236e9

3 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { expect, test } from "vite-plus/test";
2+
import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core";
3+
import { parseFlags } from "../src/args.ts";
4+
import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts";
5+
6+
const IMAGE_GENERATE_OPTIONS = [
7+
{ flag: "--prompt <text>", description: "Image description", required: true },
8+
{ flag: "--model <model>", description: "Model ID" },
9+
{ flag: "--watermark <bool>", description: BOOL_FLAG_WATERMARK },
10+
{ flag: "--no-wait", description: "Return task ID immediately without waiting" },
11+
];
12+
13+
test("parseFlags rejects unknown long flags", () => {
14+
expect(() =>
15+
parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
16+
).toThrowError(
17+
expect.objectContaining({
18+
name: "BailianError",
19+
exitCode: ExitCode.USAGE,
20+
message: expect.stringContaining('Unknown flag "--xxxx"'),
21+
}),
22+
);
23+
});
24+
25+
test("parseFlags rejects unknown flags with = syntax", () => {
26+
expect(() =>
27+
parseFlags(
28+
["--prompt=cat", "--unknown-flag=yes"],
29+
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
30+
),
31+
).toThrow(/Unknown flag "--unknown-flag"/);
32+
});
33+
34+
test("parseFlags accepts defined command and global flags", () => {
35+
const flags = parseFlags(
36+
["--quiet", "--prompt", "cat", "--watermark", "false"],
37+
[...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS],
38+
);
39+
expect(flags.quiet).toBe(true);
40+
expect(flags.prompt).toBe("cat");
41+
expect(flags.watermark).toBe("false");
42+
});
43+
44+
test("parseFlags rejects value flag when next token is another flag", () => {
45+
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
46+
for (const argv of [
47+
["--watermark", "--prompt", "cat"],
48+
["--watermark", "-h"],
49+
["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"],
50+
]) {
51+
expect(() => parseFlags(argv, opts)).toThrowError(
52+
expect.objectContaining({
53+
name: "BailianError",
54+
exitCode: ExitCode.USAGE,
55+
message: expect.stringContaining("Flag --watermark requires a value"),
56+
}),
57+
);
58+
}
59+
});
60+
61+
test("parseFlags rejects trailing value flag without value", () => {
62+
expect(() =>
63+
parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]),
64+
).toThrowError(
65+
expect.objectContaining({
66+
message: expect.stringContaining("Flag --watermark requires a value"),
67+
}),
68+
);
69+
});
70+
71+
test("parseFlags allows boolean flags without values adjacent to other flags", () => {
72+
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
73+
const flags = parseFlags(
74+
["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"],
75+
opts,
76+
);
77+
expect(flags.quiet).toBe(true);
78+
expect(flags.dryRun).toBe(true);
79+
expect(flags.noWait).toBe(true);
80+
expect(flags.prompt).toBe("cat");
81+
expect(flags.watermark).toBe("false");
82+
});
83+
84+
test("parseFlags does not treat the next flag as a boolean flag value", () => {
85+
const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS];
86+
expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError(
87+
expect.objectContaining({
88+
message: expect.stringContaining("Flag --prompt requires a value"),
89+
}),
90+
);
91+
// --dry-run is boolean: no value check; parsing continues to --prompt.
92+
const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts);
93+
expect(flags.dryRun).toBe(true);
94+
expect(flags.prompt).toBe("cat");
95+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { expect, test } from "vite-plus/test";
2+
import { createStepDispatcher } from "../src/pipeline/dispatcher.ts";
3+
import { executePipeline } from "../src/pipeline/executor.ts";
4+
import { collectPipelineIssues } from "../src/pipeline/validation.ts";
5+
import { getByJsonPointer } from "../src/pipeline/schema.ts";
6+
import { normalizeConcurrency } from "../src/pipeline/scheduler.ts";
7+
import { WORKFLOW_VERSION, type PipelineDefinition } from "../src/pipeline/types.ts";
8+
9+
test("cli package skeleton", () => {
10+
expect(true).toBe(true);
11+
});
12+
13+
test("pipeline execution can use an isolated step dispatcher", async () => {
14+
const dispatcher = createStepDispatcher();
15+
dispatcher.registerStep("test/echo", (input, ctx) => ({
16+
data: { input, hasSignal: !!ctx.signal },
17+
}));
18+
19+
const controller = new AbortController();
20+
const pipeline: PipelineDefinition = {
21+
version: WORKFLOW_VERSION,
22+
steps: [{ id: "echo", type: "test/echo", input: { message: "hello" } }],
23+
};
24+
25+
const report = await executePipeline(
26+
pipeline,
27+
{},
28+
{
29+
stepDispatcher: dispatcher,
30+
signal: controller.signal,
31+
},
32+
);
33+
34+
expect(report.status).toBe("succeeded");
35+
expect(report.steps[0]?.output?.data).toEqual({
36+
input: { message: "hello" },
37+
hasSignal: true,
38+
});
39+
});
40+
41+
test("dry-run never executes $js expressions (preview must not run code)", async () => {
42+
const dispatcher = createStepDispatcher();
43+
dispatcher.registerStep("test/echo", (input) => ({ data: input }));
44+
const flag = "__bailian_dryrun_should_not_run__";
45+
delete (globalThis as Record<string, unknown>)[flag];
46+
47+
const pipeline: PipelineDefinition = {
48+
version: WORKFLOW_VERSION,
49+
steps: [
50+
{
51+
id: "s1",
52+
type: "test/echo",
53+
input: { probe: { $js: `(globalThis[${JSON.stringify(flag)}] = true), 1` } },
54+
},
55+
],
56+
};
57+
58+
const report = await executePipeline(pipeline, {}, { stepDispatcher: dispatcher, dryRun: true });
59+
expect(report.status).toBe("planned");
60+
expect((globalThis as Record<string, unknown>)[flag]).toBeUndefined();
61+
});
62+
63+
test("script/js rejects non-literal code sourced from another step ($from)", () => {
64+
const dispatcher = createStepDispatcher();
65+
dispatcher.registerStep("test/echo", (input) => ({ data: input }));
66+
dispatcher.registerStep("script/js", () => ({ data: {} }));
67+
68+
const pipeline: PipelineDefinition = {
69+
version: WORKFLOW_VERSION,
70+
steps: [
71+
{ id: "gen", type: "test/echo", input: { message: "x" } },
72+
{
73+
id: "run",
74+
type: "script/js",
75+
input: { code: { $from: "gen", path: "/data/message" } as never },
76+
},
77+
],
78+
};
79+
80+
const issues = collectPipelineIssues(pipeline, dispatcher);
81+
expect(issues.some((issue) => issue.includes('literal string "code"'))).toBe(true);
82+
});
83+
84+
test("script/js accepts a literal string code", () => {
85+
const dispatcher = createStepDispatcher();
86+
dispatcher.registerStep("script/js", () => ({ data: {} }));
87+
88+
const pipeline: PipelineDefinition = {
89+
version: WORKFLOW_VERSION,
90+
steps: [{ id: "run", type: "script/js", input: { code: "return 1" } }],
91+
};
92+
93+
expect(collectPipelineIssues(pipeline, dispatcher)).toEqual([]);
94+
});
95+
96+
test("getByJsonPointer refuses prototype keys and inherited properties", () => {
97+
const obj = { a: { b: 1 } };
98+
expect(getByJsonPointer(obj, "/a/b")).toBe(1);
99+
expect(getByJsonPointer(obj, "/__proto__")).toBeUndefined();
100+
expect(getByJsonPointer(obj, "/constructor")).toBeUndefined();
101+
expect(getByJsonPointer(obj, "/a/constructor/constructor")).toBeUndefined();
102+
expect(getByJsonPointer(obj, "/toString")).toBeUndefined();
103+
});
104+
105+
test("normalizeConcurrency clamps to a safe maximum", () => {
106+
expect(normalizeConcurrency(undefined)).toBe(1);
107+
expect(normalizeConcurrency(4)).toBe(4);
108+
expect(normalizeConcurrency(100000)).toBe(64);
109+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { expect, test } from "vite-plus/test";
2+
import { readProxyEnv } from "../src/proxy.ts";
3+
4+
test("readProxyEnv: 未设置任何代理变量时全部为 undefined", () => {
5+
expect(readProxyEnv({})).toEqual({
6+
httpProxy: undefined,
7+
httpsProxy: undefined,
8+
noProxy: undefined,
9+
});
10+
});
11+
12+
test("readProxyEnv: 空白值视为未设置", () => {
13+
expect(readProxyEnv({ HTTPS_PROXY: "", HTTP_PROXY: " ", NO_PROXY: "" })).toEqual({
14+
httpProxy: undefined,
15+
httpsProxy: undefined,
16+
noProxy: undefined,
17+
});
18+
});
19+
20+
test("readProxyEnv: 大小写变量均可识别,小写优先", () => {
21+
expect(readProxyEnv({ HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe("http://upper:1");
22+
expect(readProxyEnv({ https_proxy: "http://lower:1" }).httpsProxy).toBe("http://lower:1");
23+
expect(
24+
readProxyEnv({ https_proxy: "http://lower:1", HTTPS_PROXY: "http://upper:1" }).httpsProxy,
25+
).toBe("http://lower:1");
26+
});
27+
28+
test("readProxyEnv: 空字符串小写变量不屏蔽已设置的大写变量", () => {
29+
expect(readProxyEnv({ https_proxy: "", HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe(
30+
"http://upper:1",
31+
);
32+
expect(readProxyEnv({ http_proxy: "", HTTP_PROXY: "http://upper:2" }).httpProxy).toBe(
33+
"http://upper:2",
34+
);
35+
});
36+
37+
test("readProxyEnv: NO_PROXY 独立读取", () => {
38+
const r = readProxyEnv({ NO_PROXY: "*.aliyuncs.com" });
39+
expect(r.noProxy).toBe("*.aliyuncs.com");
40+
expect(r.httpProxy).toBeUndefined();
41+
expect(r.httpsProxy).toBeUndefined();
42+
});

0 commit comments

Comments
 (0)