1
0
Files
DiAL/tests/server/checker/runner/cmd/runner.test.ts
lanyuanxiaoyao cf847ccd7a feat: 重构配置生命周期为 Authoring/Normalized/Resolved 三层
将变量替换和 expect 简写展开统一放入 Normalized 阶段,
运行时 AJV 使用 Normalized schema,导出 schema 面向 Authoring Config。

主要变更:
- 新增 normalizer.ts 实现 normalizeAuthoringConfig()
- 拆分 Authoring/Normalized 双 schema,checker 接口支持 authoring/normalized 片段
- config-loader 流程:normalize → Normalized AJV → semantic → resolve
- validator 兼容层自动分派 raw/normalized expect 形态
- 删除 rawExpect,store.expect 列写入 null
- Authoring schema 对 integer/boolean/enum 字段接受变量引用
- 修复 DB/HTTP validate 入口守卫和 LLM options integer 变量引用
- 优化 compact() 避免 undefined 覆盖隐患
- 移除 content.ts 恒为 true 的前置条件
- 同步 5 个主规范并归档 change
2026-05-22 14:00:47 +08:00

190 lines
6.5 KiB
TypeScript
Raw Permalink 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, ResolveContext } 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 makeResolveContext(): ResolveContext {
return { configDir: process.cwd(), defaultIntervalMs: 30000, defaultTimeoutMs: 10000 };
}
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,
},
description: null,
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.observation).toMatchObject({ 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.observation).toMatchObject({ 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.observation).toMatchObject({ 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("超时");
expect(result.observation?.["error"]).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: { exitCode: [0], stdout: [{ kind: "value", matcher: { 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: { exitCode: [0], stdout: [{ kind: "value", matcher: { 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: { exitCode: [0], stderr: [{ kind: "value", matcher: { 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("超过限制");
expect(result.observation?.["error"]).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: { exitCode: [0], stdout: [{ kind: "value", matcher: { 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: { exitCode: [0], stdout: [{ kind: "value", matcher: { 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')"]);
});
test("resolve 未配置 expect 时物化默认 exitCode", () => {
const result = checker.resolve({ cmd: { exec: "true" }, id: "test", type: "cmd" }, makeResolveContext());
expect("rawExpect" in result).toBe(false);
expect(result.expect).toEqual({ exitCode: [0] });
});
});