feat: 初始化 miot_x TypeScript 工作流构建器项目
This commit is contained in:
27
tests/backup.test.ts
Normal file
27
tests/backup.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { BACKUP_HEADER, backupStats, packBackup, readBackupFile, unpackBackup } from "../src/index.ts";
|
||||
|
||||
const sampleBackupPath = "resources/备份2026_4_26 19_17_42.bak";
|
||||
|
||||
describe("备份文件处理", () => {
|
||||
test("真实备份可以解包并统计", () => {
|
||||
const backup = readBackupFile(sampleBackupPath);
|
||||
expect(backupStats(backup)).toEqual({
|
||||
version: 2,
|
||||
ruleCount: 15,
|
||||
globalVariableCount: 25,
|
||||
nodeCount: 267,
|
||||
});
|
||||
});
|
||||
|
||||
test("打包结果使用固定头并可往返解包", () => {
|
||||
const backup = readBackupFile(sampleBackupPath);
|
||||
const packed = packBackup(backup);
|
||||
expect([...packed.subarray(0, 4)]).toEqual([...BACKUP_HEADER]);
|
||||
expect(unpackBackup(packed)).toEqual(backup);
|
||||
});
|
||||
|
||||
test("非法文件头会报错", () => {
|
||||
expect(() => unpackBackup(new Uint8Array([0, 1, 2, 3, 4]))).toThrow("备份文件头不匹配");
|
||||
});
|
||||
});
|
||||
94
tests/compiler.test.ts
Normal file
94
tests/compiler.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readDeviceInventoryFile, readBackupFile } from "../src/index.ts";
|
||||
import { collectRuleDefinitions, compileRule, compileWorkflow } from "../src/compiler.ts";
|
||||
import examples, { conditionBeforeAction, eventTurnOn, mergedTriggers } from "../examples/rules/index.ts";
|
||||
|
||||
const now = 1_760_000_000_000;
|
||||
const devices = readDeviceInventoryFile("resources/devices.json");
|
||||
|
||||
describe("规则编译", () => {
|
||||
test("单条事件触发开灯规则生成稳定 JSON", () => {
|
||||
const { rule } = compileRule(eventTurnOn, { now });
|
||||
expect(rule).toEqual({
|
||||
id: String(now),
|
||||
cfg: {
|
||||
id: String(now),
|
||||
userData: {
|
||||
name: "示例-事件触发开灯",
|
||||
transform: { x: 0, y: 0, scale: 1, rotate: 0 },
|
||||
lastUpdateTime: now,
|
||||
version: 0,
|
||||
},
|
||||
uiType: "test",
|
||||
enable: true,
|
||||
},
|
||||
nodes: [
|
||||
{
|
||||
id: "DI1",
|
||||
type: "deviceInput",
|
||||
props: {
|
||||
did: "blt.3.1htiptpdgco00",
|
||||
siid: 2,
|
||||
eiid: 1008,
|
||||
},
|
||||
inputs: {},
|
||||
outputs: { output: ["DO1.trigger"] },
|
||||
cfg: {
|
||||
urn: "urn:miot-spec-v2:device:motion-sensor:0000A014:xiaomi-pir1:2",
|
||||
name: "deviceInput",
|
||||
version: 1,
|
||||
pos: { x: 240, y: 180, width: 450, height: 206 },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "DO1",
|
||||
type: "deviceOutput",
|
||||
props: {
|
||||
did: "group.1815373077765824512",
|
||||
siid: 2,
|
||||
piid: 1,
|
||||
value: true,
|
||||
},
|
||||
inputs: { trigger: null },
|
||||
outputs: { output: [] },
|
||||
cfg: {
|
||||
urn: "urn:miot-spec-v2:device:light:0000A001:mijia-group3:3:0000C802",
|
||||
name: "deviceOutput",
|
||||
version: 1,
|
||||
pos: { x: 800, y: 180, width: 528, height: 164 },
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("状态判断后动作会生成 deviceGet 并连接满足路径", () => {
|
||||
const { rule } = compileRule(conditionBeforeAction, { now });
|
||||
expect(rule.nodes.map((node) => node.type)).toEqual(["deviceInput", "deviceGet", "deviceOutput"]);
|
||||
expect(rule.nodes[0]!.outputs.output).toEqual(["DG1.input"]);
|
||||
expect(rule.nodes[1]!.outputs.output).toEqual(["DO1.trigger"]);
|
||||
expect(rule.nodes[1]!.outputs.output2).toEqual([]);
|
||||
});
|
||||
|
||||
test("多个触发器会生成 signalOr 合并节点", () => {
|
||||
const { rule } = compileRule(mergedTriggers, { now });
|
||||
expect(rule.nodes.map((node) => node.type)).toEqual(["deviceInput", "deviceInput", "signalOr", "deviceOutput"]);
|
||||
expect(rule.nodes[0]!.outputs.output).toEqual(["SO1.input0"]);
|
||||
expect(rule.nodes[1]!.outputs.output).toEqual(["SO1.input1"]);
|
||||
expect(rule.nodes[2]!.outputs.output).toEqual(["DO1.trigger"]);
|
||||
});
|
||||
|
||||
test("模块导出收集支持默认数组和命名规则", async () => {
|
||||
const moduleExports = await import("../examples/rules/index.ts");
|
||||
const definitions = collectRuleDefinitions(moduleExports);
|
||||
expect(definitions.map((definition) => definition.name)).toEqual(examples.map((definition) => definition.name));
|
||||
});
|
||||
|
||||
test("编译并追加到真实备份时保留原有规则", () => {
|
||||
const base = readBackupFile("resources/备份2026_4_26 19_17_42.bak");
|
||||
const result = compileWorkflow([eventTurnOn], base, { now, devices });
|
||||
expect(result.diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]);
|
||||
expect(result.backup.rules).toHaveLength(base.rules.length + 1);
|
||||
expect(result.backup.rules.at(-1)?.cfg.userData.name).toBe("示例-事件触发开灯");
|
||||
});
|
||||
});
|
||||
41
tests/validate.test.ts
Normal file
41
tests/validate.test.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { compileRule, readDeviceInventoryFile, validateBackup } from "../src/index.ts";
|
||||
import { eventTurnOn } from "../examples/rules/index.ts";
|
||||
|
||||
const now = 1_760_000_000_000;
|
||||
|
||||
describe("备份校验", () => {
|
||||
test("非法节点 ID 会失败", () => {
|
||||
const backup = backupFromRule();
|
||||
backup.rules[0]!.nodes[0]!.id = "bad-id";
|
||||
const codes = validateBackup(backup).map((diagnostic) => diagnostic.code);
|
||||
expect(codes).toContain("node-id-invalid");
|
||||
});
|
||||
|
||||
test("非法连接目标会失败", () => {
|
||||
const backup = backupFromRule();
|
||||
backup.rules[0]!.nodes[0]!.outputs.output = ["DO1.missing"];
|
||||
const codes = validateBackup(backup).map((diagnostic) => diagnostic.code);
|
||||
expect(codes).toContain("connection-port-missing");
|
||||
});
|
||||
|
||||
test("缺失设备会按严格模式报错", () => {
|
||||
const backup = backupFromRule();
|
||||
const diagnostics = validateBackup(backup, { devices: { devList: {} } });
|
||||
expect(diagnostics.filter((diagnostic) => diagnostic.code === "device-not-found")).toHaveLength(2);
|
||||
});
|
||||
|
||||
test("真实设备清单可通过设备校验", () => {
|
||||
const backup = backupFromRule();
|
||||
const diagnostics = validateBackup(backup, { devices: readDeviceInventoryFile("resources/devices.json") });
|
||||
expect(diagnostics.filter((diagnostic) => diagnostic.severity === "error")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
function backupFromRule() {
|
||||
return {
|
||||
version: 2 as const,
|
||||
rules: [compileRule(eventTurnOn, { now }).rule],
|
||||
variables: { global: {} },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user