1
0
Files
DiAL/tests/server/checker/runner/cmd/runner.test.ts
lanyuanxiaoyao 7926514986 feat: 配置变量系统与 target id/name 双字段标识
- 新增顶层 variables 段支持 string/number/boolean 字面量
- target 字符串字段支持 、、{...} 转义语法
- 变量解析优先级: variables -> process.env -> 默认值 -> 报错
- 完整引用保留原始类型,部分引用拼接为字符串
- 变量替换在 YAML 解析后、AJV 校验前执行
- 替换仅作用于 targets,跳过 id/type 字段
- target 新增必填 id 字段作为唯一标识,name 改为可选展示名称
- 数据库存储/API/前端全面迁移到 id 标识
- 统一 checker 运行时类型检查为 es-toolkit predicates
- 同步 delta specs 到主 specs,归档 config-variables 变更
2026-05-17 00:37:54 +08:00

173 lines
5.6 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 { describe, expect, test } from "bun:test";
import type { ResolvedCommandTarget } from "../../../../../src/server/checker/runner/cmd/types";
import type { CheckerContext } from "../../../../../src/server/checker/runner/types";
import { CommandChecker } from "../../../../../src/server/checker/runner/cmd/execute";
const checker = new CommandChecker();
const processEnv = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
);
function makeCtx(timeoutMs = 5000): CheckerContext {
const controller = new AbortController();
setTimeout(() => controller.abort(), timeoutMs);
return { signal: controller.signal };
}
function makeTarget(
cmd: Partial<ResolvedCommandTarget["cmd"]>,
overrides?: Partial<ResolvedCommandTarget>,
): ResolvedCommandTarget {
return {
cmd: {
args: ["-e", "console.log('hello')"],
cwd: process.cwd(),
env: processEnv,
exec: "bun",
maxOutputBytes: 1024 * 1024,
...cmd,
},
group: "default",
id: "test-cmd",
intervalMs: 60000,
name: "test-cmd",
timeoutMs: 5000,
type: "cmd",
...overrides,
};
}
describe("CommandChecker", () => {
test("exitCode=0 成功", async () => {
const result = await checker.execute(makeTarget({ args: ["-e", "process.exit(0)"], exec: "bun" }), makeCtx());
expect(result.matched).toBe(true);
expect(result.statusDetail).toBe("exitCode=0");
expect(result.failure).toBeNull();
});
test("exitCode=1 不匹配默认 [0]", async () => {
const result = await checker.execute(makeTarget({ args: ["-e", "process.exit(1)"], exec: "bun" }), makeCtx());
expect(result.matched).toBe(false);
expect(result.statusDetail).toBe("exitCode=1");
expect(result.failure!.phase).toBe("exitCode");
});
test("exitCode=1 匹配自定义 [1]", async () => {
const result = await checker.execute(
makeTarget({ args: ["-e", "process.exit(1)"], exec: "bun" }, { expect: { exitCode: [1] } }),
makeCtx(),
);
expect(result.matched).toBe(true);
expect(result.statusDetail).toBe("exitCode=1");
});
test("命令不存在返回 spawn 错误", async () => {
const result = await checker.execute(makeTarget({ exec: "dial-command-not-found-xyz" }), makeCtx());
expect(result.matched).toBe(false);
expect(result.failure!.phase).toBe("exitCode");
expect(result.failure!.message).toBeTruthy();
});
test("超时返回错误", async () => {
const result = await checker.execute(
makeTarget({ args: ["-e", "await Bun.sleep(10000)"], exec: "bun" }, { timeoutMs: 100 }),
makeCtx(100),
);
expect(result.matched).toBe(false);
expect(result.failure!.message).toContain("超时");
});
test("stdout 输出捕获", async () => {
const result = await checker.execute(
makeTarget({ args: ["-e", "console.log('hello world')"], exec: "bun" }),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("stdout 匹配 expect", async () => {
const result = await checker.execute(
makeTarget(
{ args: ["-e", "console.log('hello')"], exec: "bun" },
{ expect: { stdout: [{ contains: "hello" }] } },
),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("stdout 不匹配 expect", async () => {
const result = await checker.execute(
makeTarget(
{ args: ["-e", "console.log('hello')"], exec: "bun" },
{ expect: { stdout: [{ contains: "nonexistent" }] } },
),
makeCtx(),
);
expect(result.matched).toBe(false);
expect(result.failure!.phase).toBe("stdout");
});
test("stderr 匹配 expect", async () => {
const result = await checker.execute(
makeTarget(
{ args: ["-e", "process.stderr.write('error\\n')"], exec: "bun" },
{ expect: { stderr: [{ contains: "error" }] } },
),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("输出超过 maxOutputBytes", async () => {
const result = await checker.execute(
makeTarget({ args: ["-e", "process.stdout.write('y\\n'.repeat(1000))"], exec: "bun", maxOutputBytes: 10 }),
makeCtx(),
);
expect(result.matched).toBe(false);
expect(result.failure!.message).toContain("超过限制");
});
test("durationMs 非空", async () => {
const result = await checker.execute(makeTarget({ args: ["-e", "process.exit(0)"], exec: "bun" }), makeCtx());
expect(result.durationMs).not.toBeNull();
expect(result.durationMs!).toBeGreaterThanOrEqual(0);
});
test("不使用 shell通配符不被展开", async () => {
const result = await checker.execute(
makeTarget({ args: ["-e", "console.log('*')"], exec: "bun" }, { expect: { stdout: [{ contains: "*" }] } }),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("execute 使用 resolved env", async () => {
const result = await checker.execute(
makeTarget(
{
args: ["-e", "console.log(process.env.DIAL_TEST_ENV ?? '')"],
env: { ...processEnv, DIAL_TEST_ENV: "resolved-env" },
exec: "bun",
},
{ expect: { stdout: [{ contains: "resolved-env" }] } },
),
makeCtx(),
);
expect(result.matched).toBe(true);
});
test("serialize 返回命令摘要和 config JSON", () => {
const target = makeTarget({ args: ["-e", "console.log('hello')"], exec: "bun" });
const s = checker.serialize(target);
expect(s.target).toBe("exec bun -e console.log('hello')");
const config = JSON.parse(s.config) as { args: string[]; exec: string };
expect(config.exec).toBe("bun");
expect(config.args).toEqual(["-e", "console.log('hello')"]);
});
});