@manifesto-ai/core 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/index.d.ts +1750 -19
- package/dist/index.js +18480 -36
- package/dist/index.js.map +1 -1
- package/package.json +5 -3
- package/dist/__tests__/apply.test.d.ts +0 -2
- package/dist/__tests__/apply.test.d.ts.map +0 -1
- package/dist/__tests__/apply.test.js +0 -320
- package/dist/__tests__/apply.test.js.map +0 -1
- package/dist/__tests__/defaults.test.d.ts +0 -2
- package/dist/__tests__/defaults.test.d.ts.map +0 -1
- package/dist/__tests__/defaults.test.js +0 -74
- package/dist/__tests__/defaults.test.js.map +0 -1
- package/dist/__tests__/jcs.test.d.ts +0 -2
- package/dist/__tests__/jcs.test.d.ts.map +0 -1
- package/dist/__tests__/jcs.test.js +0 -45
- package/dist/__tests__/jcs.test.js.map +0 -1
- package/dist/core/apply.d.ts +0 -17
- package/dist/core/apply.d.ts.map +0 -1
- package/dist/core/apply.js +0 -198
- package/dist/core/apply.js.map +0 -1
- package/dist/core/compute.d.ts +0 -17
- package/dist/core/compute.d.ts.map +0 -1
- package/dist/core/compute.js +0 -305
- package/dist/core/compute.js.map +0 -1
- package/dist/core/compute.test.d.ts +0 -2
- package/dist/core/compute.test.d.ts.map +0 -1
- package/dist/core/compute.test.js +0 -950
- package/dist/core/compute.test.js.map +0 -1
- package/dist/core/explain.d.ts +0 -14
- package/dist/core/explain.d.ts.map +0 -1
- package/dist/core/explain.js +0 -78
- package/dist/core/explain.js.map +0 -1
- package/dist/core/index.d.ts +0 -5
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -5
- package/dist/core/index.js.map +0 -1
- package/dist/core/validate.d.ts +0 -16
- package/dist/core/validate.d.ts.map +0 -1
- package/dist/core/validate.js +0 -361
- package/dist/core/validate.js.map +0 -1
- package/dist/core/validate.test.d.ts +0 -2
- package/dist/core/validate.test.d.ts.map +0 -1
- package/dist/core/validate.test.js +0 -719
- package/dist/core/validate.test.js.map +0 -1
- package/dist/core/validation-utils.d.ts +0 -20
- package/dist/core/validation-utils.d.ts.map +0 -1
- package/dist/core/validation-utils.js +0 -351
- package/dist/core/validation-utils.js.map +0 -1
- package/dist/errors.d.ts +0 -30
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -51
- package/dist/errors.js.map +0 -1
- package/dist/evaluator/computed.d.ts +0 -14
- package/dist/evaluator/computed.d.ts.map +0 -1
- package/dist/evaluator/computed.js +0 -60
- package/dist/evaluator/computed.js.map +0 -1
- package/dist/evaluator/context.d.ts +0 -62
- package/dist/evaluator/context.d.ts.map +0 -1
- package/dist/evaluator/context.js +0 -44
- package/dist/evaluator/context.js.map +0 -1
- package/dist/evaluator/dag.d.ts +0 -30
- package/dist/evaluator/dag.d.ts.map +0 -1
- package/dist/evaluator/dag.js +0 -121
- package/dist/evaluator/dag.js.map +0 -1
- package/dist/evaluator/expr.d.ts +0 -11
- package/dist/evaluator/expr.d.ts.map +0 -1
- package/dist/evaluator/expr.js +0 -1030
- package/dist/evaluator/expr.js.map +0 -1
- package/dist/evaluator/expr.test.d.ts +0 -2
- package/dist/evaluator/expr.test.d.ts.map +0 -1
- package/dist/evaluator/expr.test.js +0 -1235
- package/dist/evaluator/expr.test.js.map +0 -1
- package/dist/evaluator/flow.d.ts +0 -36
- package/dist/evaluator/flow.d.ts.map +0 -1
- package/dist/evaluator/flow.js +0 -390
- package/dist/evaluator/flow.js.map +0 -1
- package/dist/evaluator/flow.test.d.ts +0 -2
- package/dist/evaluator/flow.test.d.ts.map +0 -1
- package/dist/evaluator/flow.test.js +0 -499
- package/dist/evaluator/flow.test.js.map +0 -1
- package/dist/evaluator/index.d.ts +0 -6
- package/dist/evaluator/index.d.ts.map +0 -1
- package/dist/evaluator/index.js +0 -6
- package/dist/evaluator/index.js.map +0 -1
- package/dist/factories.d.ts +0 -22
- package/dist/factories.d.ts.map +0 -1
- package/dist/factories.js +0 -44
- package/dist/factories.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.test.d.ts +0 -2
- package/dist/index.test.d.ts.map +0 -1
- package/dist/index.test.js +0 -14
- package/dist/index.test.js.map +0 -1
- package/dist/schema/action.d.ts +0 -14
- package/dist/schema/action.d.ts.map +0 -1
- package/dist/schema/action.js +0 -30
- package/dist/schema/action.js.map +0 -1
- package/dist/schema/common.d.ts +0 -37
- package/dist/schema/common.d.ts.map +0 -1
- package/dist/schema/common.js +0 -20
- package/dist/schema/common.js.map +0 -1
- package/dist/schema/computed.d.ts +0 -23
- package/dist/schema/computed.d.ts.map +0 -1
- package/dist/schema/computed.js +0 -34
- package/dist/schema/computed.js.map +0 -1
- package/dist/schema/defaults.d.ts +0 -12
- package/dist/schema/defaults.d.ts.map +0 -1
- package/dist/schema/defaults.js +0 -19
- package/dist/schema/defaults.js.map +0 -1
- package/dist/schema/domain.d.ts +0 -50
- package/dist/schema/domain.d.ts.map +0 -1
- package/dist/schema/domain.js +0 -60
- package/dist/schema/domain.js.map +0 -1
- package/dist/schema/expr.d.ts +0 -483
- package/dist/schema/expr.d.ts.map +0 -1
- package/dist/schema/expr.js +0 -445
- package/dist/schema/expr.js.map +0 -1
- package/dist/schema/field.d.ts +0 -48
- package/dist/schema/field.d.ts.map +0 -1
- package/dist/schema/field.js +0 -31
- package/dist/schema/field.js.map +0 -1
- package/dist/schema/flow.d.ts +0 -103
- package/dist/schema/flow.d.ts.map +0 -1
- package/dist/schema/flow.js +0 -82
- package/dist/schema/flow.js.map +0 -1
- package/dist/schema/host-context.d.ts +0 -12
- package/dist/schema/host-context.d.ts.map +0 -1
- package/dist/schema/host-context.js +0 -23
- package/dist/schema/host-context.js.map +0 -1
- package/dist/schema/index.d.ts +0 -15
- package/dist/schema/index.d.ts.map +0 -1
- package/dist/schema/index.js +0 -28
- package/dist/schema/index.js.map +0 -1
- package/dist/schema/patch.d.ts +0 -59
- package/dist/schema/patch.d.ts.map +0 -1
- package/dist/schema/patch.js +0 -60
- package/dist/schema/patch.js.map +0 -1
- package/dist/schema/result.d.ts +0 -142
- package/dist/schema/result.d.ts.map +0 -1
- package/dist/schema/result.js +0 -94
- package/dist/schema/result.js.map +0 -1
- package/dist/schema/snapshot.d.ts +0 -153
- package/dist/schema/snapshot.d.ts.map +0 -1
- package/dist/schema/snapshot.js +0 -160
- package/dist/schema/snapshot.js.map +0 -1
- package/dist/schema/trace.d.ts +0 -98
- package/dist/schema/trace.d.ts.map +0 -1
- package/dist/schema/trace.js +0 -90
- package/dist/schema/trace.js.map +0 -1
- package/dist/schema/type-spec.d.ts +0 -34
- package/dist/schema/type-spec.d.ts.map +0 -1
- package/dist/schema/type-spec.js +0 -40
- package/dist/schema/type-spec.js.map +0 -1
- package/dist/utils/canonical.d.ts +0 -37
- package/dist/utils/canonical.d.ts.map +0 -1
- package/dist/utils/canonical.js +0 -122
- package/dist/utils/canonical.js.map +0 -1
- package/dist/utils/canonical.test.d.ts +0 -2
- package/dist/utils/canonical.test.d.ts.map +0 -1
- package/dist/utils/canonical.test.js +0 -183
- package/dist/utils/canonical.test.js.map +0 -1
- package/dist/utils/hash.d.ts +0 -55
- package/dist/utils/hash.d.ts.map +0 -1
- package/dist/utils/hash.js +0 -183
- package/dist/utils/hash.js.map +0 -1
- package/dist/utils/hash.test.d.ts +0 -2
- package/dist/utils/hash.test.d.ts.map +0 -1
- package/dist/utils/hash.test.js +0 -253
- package/dist/utils/hash.test.js.map +0 -1
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -4
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/path.d.ts +0 -42
- package/dist/utils/path.d.ts.map +0 -1
- package/dist/utils/path.js +0 -170
- package/dist/utils/path.js.map +0 -1
- package/dist/utils/path.test.d.ts +0 -2
- package/dist/utils/path.test.d.ts.map +0 -1
- package/dist/utils/path.test.js +0 -248
- package/dist/utils/path.test.js.map +0 -1
|
@@ -1,1235 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { evaluateExpr } from "./expr.js";
|
|
3
|
-
import { createContext } from "./context.js";
|
|
4
|
-
import { isOk } from "../schema/common.js";
|
|
5
|
-
// Helper to create a minimal test context
|
|
6
|
-
function createTestContext(data = {}, input, meta) {
|
|
7
|
-
const snapshot = {
|
|
8
|
-
data,
|
|
9
|
-
computed: {},
|
|
10
|
-
system: {
|
|
11
|
-
status: "idle",
|
|
12
|
-
lastError: null,
|
|
13
|
-
errors: [],
|
|
14
|
-
pendingRequirements: [],
|
|
15
|
-
currentAction: null,
|
|
16
|
-
},
|
|
17
|
-
input,
|
|
18
|
-
meta: {
|
|
19
|
-
version: 0,
|
|
20
|
-
timestamp: meta?.timestamp ?? 0,
|
|
21
|
-
randomSeed: "seed",
|
|
22
|
-
schemaHash: "test-hash",
|
|
23
|
-
},
|
|
24
|
-
};
|
|
25
|
-
const schema = {
|
|
26
|
-
id: "manifesto:test",
|
|
27
|
-
version: "1.0.0",
|
|
28
|
-
hash: "test-hash",
|
|
29
|
-
types: {},
|
|
30
|
-
state: { fields: {} },
|
|
31
|
-
computed: { fields: {} },
|
|
32
|
-
actions: {},
|
|
33
|
-
};
|
|
34
|
-
return createContext(snapshot, schema, meta?.actionName ?? null, "test", meta?.intentId, meta?.timestamp ?? 0);
|
|
35
|
-
}
|
|
36
|
-
// Helper to evaluate and unwrap result
|
|
37
|
-
function evaluate(expr, ctx = createTestContext()) {
|
|
38
|
-
const result = evaluateExpr(expr, ctx);
|
|
39
|
-
if (!isOk(result)) {
|
|
40
|
-
throw new Error(`Evaluation failed: ${result.error.message}`);
|
|
41
|
-
}
|
|
42
|
-
return result.value;
|
|
43
|
-
}
|
|
44
|
-
describe("Expression Evaluator", () => {
|
|
45
|
-
describe("Literals", () => {
|
|
46
|
-
it("lit - should return literal values", () => {
|
|
47
|
-
expect(evaluate({ kind: "lit", value: 42 })).toBe(42);
|
|
48
|
-
expect(evaluate({ kind: "lit", value: "hello" })).toBe("hello");
|
|
49
|
-
expect(evaluate({ kind: "lit", value: true })).toBe(true);
|
|
50
|
-
expect(evaluate({ kind: "lit", value: null })).toBe(null);
|
|
51
|
-
expect(evaluate({ kind: "lit", value: [1, 2, 3] })).toEqual([1, 2, 3]);
|
|
52
|
-
expect(evaluate({ kind: "lit", value: { a: 1 } })).toEqual({ a: 1 });
|
|
53
|
-
});
|
|
54
|
-
it("get - should get values from data", () => {
|
|
55
|
-
const ctx = createTestContext({ count: 10, user: { name: "Alice" } });
|
|
56
|
-
expect(evaluate({ kind: "get", path: "count" }, ctx)).toBe(10);
|
|
57
|
-
expect(evaluate({ kind: "get", path: "user.name" }, ctx)).toBe("Alice");
|
|
58
|
-
expect(evaluate({ kind: "get", path: "nonexistent" }, ctx)).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
it("get - should get values from input", () => {
|
|
61
|
-
const ctx = createTestContext({}, { amount: 100 });
|
|
62
|
-
expect(evaluate({ kind: "get", path: "input.amount" }, ctx)).toBe(100);
|
|
63
|
-
expect(evaluate({ kind: "get", path: "input" }, ctx)).toEqual({ amount: 100 });
|
|
64
|
-
});
|
|
65
|
-
it("get - should get values from system", () => {
|
|
66
|
-
const ctx = createTestContext();
|
|
67
|
-
expect(evaluate({ kind: "get", path: "system.status" }, ctx)).toBe("idle");
|
|
68
|
-
});
|
|
69
|
-
it("get - should get values from meta", () => {
|
|
70
|
-
const ctx = createTestContext({}, undefined, {
|
|
71
|
-
intentId: "intent-123",
|
|
72
|
-
actionName: "testAction",
|
|
73
|
-
timestamp: 1234,
|
|
74
|
-
});
|
|
75
|
-
expect(evaluate({ kind: "get", path: "meta.intentId" }, ctx)).toBe("intent-123");
|
|
76
|
-
expect(evaluate({ kind: "get", path: "meta.actionName" }, ctx)).toBe("testAction");
|
|
77
|
-
expect(evaluate({ kind: "get", path: "meta.timestamp" }, ctx)).toBe(1234);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
describe("Comparison", () => {
|
|
81
|
-
it("eq - should compare equality", () => {
|
|
82
|
-
expect(evaluate({ kind: "eq", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 5 } })).toBe(true);
|
|
83
|
-
expect(evaluate({ kind: "eq", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(false);
|
|
84
|
-
expect(evaluate({ kind: "eq", left: { kind: "lit", value: "a" }, right: { kind: "lit", value: "a" } })).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
it("neq - should compare inequality", () => {
|
|
87
|
-
expect(evaluate({ kind: "neq", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(true);
|
|
88
|
-
expect(evaluate({ kind: "neq", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 5 } })).toBe(false);
|
|
89
|
-
});
|
|
90
|
-
it("gt - should compare greater than", () => {
|
|
91
|
-
expect(evaluate({ kind: "gt", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(true);
|
|
92
|
-
expect(evaluate({ kind: "gt", left: { kind: "lit", value: 3 }, right: { kind: "lit", value: 5 } })).toBe(false);
|
|
93
|
-
expect(evaluate({ kind: "gt", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 5 } })).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
it("gte - should compare greater than or equal", () => {
|
|
96
|
-
expect(evaluate({ kind: "gte", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(true);
|
|
97
|
-
expect(evaluate({ kind: "gte", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 5 } })).toBe(true);
|
|
98
|
-
expect(evaluate({ kind: "gte", left: { kind: "lit", value: 3 }, right: { kind: "lit", value: 5 } })).toBe(false);
|
|
99
|
-
});
|
|
100
|
-
it("lt - should compare less than", () => {
|
|
101
|
-
expect(evaluate({ kind: "lt", left: { kind: "lit", value: 3 }, right: { kind: "lit", value: 5 } })).toBe(true);
|
|
102
|
-
expect(evaluate({ kind: "lt", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
it("lte - should compare less than or equal", () => {
|
|
105
|
-
expect(evaluate({ kind: "lte", left: { kind: "lit", value: 3 }, right: { kind: "lit", value: 5 } })).toBe(true);
|
|
106
|
-
expect(evaluate({ kind: "lte", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 5 } })).toBe(true);
|
|
107
|
-
expect(evaluate({ kind: "lte", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(false);
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe("Logical", () => {
|
|
111
|
-
it("and - should perform logical AND", () => {
|
|
112
|
-
expect(evaluate({ kind: "and", args: [{ kind: "lit", value: true }, { kind: "lit", value: true }] })).toBe(true);
|
|
113
|
-
expect(evaluate({ kind: "and", args: [{ kind: "lit", value: true }, { kind: "lit", value: false }] })).toBe(false);
|
|
114
|
-
expect(evaluate({ kind: "and", args: [{ kind: "lit", value: false }, { kind: "lit", value: true }] })).toBe(false);
|
|
115
|
-
expect(evaluate({ kind: "and", args: [] })).toBe(true); // Empty AND is true
|
|
116
|
-
});
|
|
117
|
-
it("or - should perform logical OR", () => {
|
|
118
|
-
expect(evaluate({ kind: "or", args: [{ kind: "lit", value: true }, { kind: "lit", value: false }] })).toBe(true);
|
|
119
|
-
expect(evaluate({ kind: "or", args: [{ kind: "lit", value: false }, { kind: "lit", value: true }] })).toBe(true);
|
|
120
|
-
expect(evaluate({ kind: "or", args: [{ kind: "lit", value: false }, { kind: "lit", value: false }] })).toBe(false);
|
|
121
|
-
expect(evaluate({ kind: "or", args: [] })).toBe(false); // Empty OR is false
|
|
122
|
-
});
|
|
123
|
-
it("not - should perform logical NOT", () => {
|
|
124
|
-
expect(evaluate({ kind: "not", arg: { kind: "lit", value: true } })).toBe(false);
|
|
125
|
-
expect(evaluate({ kind: "not", arg: { kind: "lit", value: false } })).toBe(true);
|
|
126
|
-
expect(evaluate({ kind: "not", arg: { kind: "lit", value: 0 } })).toBe(true);
|
|
127
|
-
expect(evaluate({ kind: "not", arg: { kind: "lit", value: 1 } })).toBe(false);
|
|
128
|
-
});
|
|
129
|
-
it("and/or - should short-circuit", () => {
|
|
130
|
-
// AND short-circuits on first false
|
|
131
|
-
expect(evaluate({
|
|
132
|
-
kind: "and",
|
|
133
|
-
args: [{ kind: "lit", value: false }, { kind: "lit", value: true }]
|
|
134
|
-
})).toBe(false);
|
|
135
|
-
// OR short-circuits on first true
|
|
136
|
-
expect(evaluate({
|
|
137
|
-
kind: "or",
|
|
138
|
-
args: [{ kind: "lit", value: true }, { kind: "lit", value: false }]
|
|
139
|
-
})).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
describe("Conditional", () => {
|
|
143
|
-
it("if - should evaluate then branch when condition is true", () => {
|
|
144
|
-
expect(evaluate({
|
|
145
|
-
kind: "if",
|
|
146
|
-
cond: { kind: "lit", value: true },
|
|
147
|
-
then: { kind: "lit", value: "yes" },
|
|
148
|
-
else: { kind: "lit", value: "no" },
|
|
149
|
-
})).toBe("yes");
|
|
150
|
-
});
|
|
151
|
-
it("if - should evaluate else branch when condition is false", () => {
|
|
152
|
-
expect(evaluate({
|
|
153
|
-
kind: "if",
|
|
154
|
-
cond: { kind: "lit", value: false },
|
|
155
|
-
then: { kind: "lit", value: "yes" },
|
|
156
|
-
else: { kind: "lit", value: "no" },
|
|
157
|
-
})).toBe("no");
|
|
158
|
-
});
|
|
159
|
-
it("if - should handle truthy/falsy values", () => {
|
|
160
|
-
expect(evaluate({
|
|
161
|
-
kind: "if",
|
|
162
|
-
cond: { kind: "lit", value: 1 },
|
|
163
|
-
then: { kind: "lit", value: "truthy" },
|
|
164
|
-
else: { kind: "lit", value: "falsy" },
|
|
165
|
-
})).toBe("truthy");
|
|
166
|
-
expect(evaluate({
|
|
167
|
-
kind: "if",
|
|
168
|
-
cond: { kind: "lit", value: 0 },
|
|
169
|
-
then: { kind: "lit", value: "truthy" },
|
|
170
|
-
else: { kind: "lit", value: "falsy" },
|
|
171
|
-
})).toBe("falsy");
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
describe("Arithmetic", () => {
|
|
175
|
-
it("add - should add numbers", () => {
|
|
176
|
-
expect(evaluate({ kind: "add", left: { kind: "lit", value: 2 }, right: { kind: "lit", value: 3 } })).toBe(5);
|
|
177
|
-
expect(evaluate({ kind: "add", left: { kind: "lit", value: -1 }, right: { kind: "lit", value: 1 } })).toBe(0);
|
|
178
|
-
});
|
|
179
|
-
it("sub - should subtract numbers", () => {
|
|
180
|
-
expect(evaluate({ kind: "sub", left: { kind: "lit", value: 5 }, right: { kind: "lit", value: 3 } })).toBe(2);
|
|
181
|
-
expect(evaluate({ kind: "sub", left: { kind: "lit", value: 3 }, right: { kind: "lit", value: 5 } })).toBe(-2);
|
|
182
|
-
});
|
|
183
|
-
it("mul - should multiply numbers", () => {
|
|
184
|
-
expect(evaluate({ kind: "mul", left: { kind: "lit", value: 4 }, right: { kind: "lit", value: 3 } })).toBe(12);
|
|
185
|
-
expect(evaluate({ kind: "mul", left: { kind: "lit", value: -2 }, right: { kind: "lit", value: 3 } })).toBe(-6);
|
|
186
|
-
});
|
|
187
|
-
it("div - should divide numbers", () => {
|
|
188
|
-
expect(evaluate({ kind: "div", left: { kind: "lit", value: 10 }, right: { kind: "lit", value: 2 } })).toBe(5);
|
|
189
|
-
expect(evaluate({ kind: "div", left: { kind: "lit", value: 7 }, right: { kind: "lit", value: 2 } })).toBe(3.5);
|
|
190
|
-
});
|
|
191
|
-
it("div - should return null for division by zero", () => {
|
|
192
|
-
expect(evaluate({ kind: "div", left: { kind: "lit", value: 10 }, right: { kind: "lit", value: 0 } })).toBe(null);
|
|
193
|
-
});
|
|
194
|
-
it("mod - should compute modulo", () => {
|
|
195
|
-
expect(evaluate({ kind: "mod", left: { kind: "lit", value: 10 }, right: { kind: "lit", value: 3 } })).toBe(1);
|
|
196
|
-
expect(evaluate({ kind: "mod", left: { kind: "lit", value: 9 }, right: { kind: "lit", value: 3 } })).toBe(0);
|
|
197
|
-
});
|
|
198
|
-
it("mod - should return null for modulo by zero", () => {
|
|
199
|
-
expect(evaluate({ kind: "mod", left: { kind: "lit", value: 10 }, right: { kind: "lit", value: 0 } })).toBe(null);
|
|
200
|
-
});
|
|
201
|
-
it("arithmetic - should coerce types", () => {
|
|
202
|
-
expect(evaluate({ kind: "add", left: { kind: "lit", value: "5" }, right: { kind: "lit", value: 3 } })).toBe(8);
|
|
203
|
-
expect(evaluate({ kind: "add", left: { kind: "lit", value: true }, right: { kind: "lit", value: 1 } })).toBe(2);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
describe("String", () => {
|
|
207
|
-
it("concat - should concatenate strings", () => {
|
|
208
|
-
expect(evaluate({
|
|
209
|
-
kind: "concat",
|
|
210
|
-
args: [{ kind: "lit", value: "hello" }, { kind: "lit", value: " " }, { kind: "lit", value: "world" }]
|
|
211
|
-
})).toBe("hello world");
|
|
212
|
-
});
|
|
213
|
-
it("concat - should coerce non-strings", () => {
|
|
214
|
-
expect(evaluate({
|
|
215
|
-
kind: "concat",
|
|
216
|
-
args: [{ kind: "lit", value: "value: " }, { kind: "lit", value: 42 }]
|
|
217
|
-
})).toBe("value: 42");
|
|
218
|
-
});
|
|
219
|
-
it("substring - should extract substring", () => {
|
|
220
|
-
expect(evaluate({
|
|
221
|
-
kind: "substring",
|
|
222
|
-
str: { kind: "lit", value: "hello world" },
|
|
223
|
-
start: { kind: "lit", value: 0 },
|
|
224
|
-
end: { kind: "lit", value: 5 },
|
|
225
|
-
})).toBe("hello");
|
|
226
|
-
});
|
|
227
|
-
it("substring - should handle no end parameter", () => {
|
|
228
|
-
expect(evaluate({
|
|
229
|
-
kind: "substring",
|
|
230
|
-
str: { kind: "lit", value: "hello world" },
|
|
231
|
-
start: { kind: "lit", value: 6 },
|
|
232
|
-
})).toBe("world");
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
describe("Collection", () => {
|
|
236
|
-
it("len - should return array length", () => {
|
|
237
|
-
expect(evaluate({ kind: "len", arg: { kind: "lit", value: [1, 2, 3] } })).toBe(3);
|
|
238
|
-
expect(evaluate({ kind: "len", arg: { kind: "lit", value: [] } })).toBe(0);
|
|
239
|
-
});
|
|
240
|
-
it("len - should return string length", () => {
|
|
241
|
-
expect(evaluate({ kind: "len", arg: { kind: "lit", value: "hello" } })).toBe(5);
|
|
242
|
-
});
|
|
243
|
-
it("len - should return object key count", () => {
|
|
244
|
-
expect(evaluate({ kind: "len", arg: { kind: "lit", value: { a: 1, b: 2 } } })).toBe(2);
|
|
245
|
-
});
|
|
246
|
-
it("at - should get array element by index", () => {
|
|
247
|
-
expect(evaluate({
|
|
248
|
-
kind: "at",
|
|
249
|
-
array: { kind: "lit", value: [10, 20, 30] },
|
|
250
|
-
index: { kind: "lit", value: 1 },
|
|
251
|
-
})).toBe(20);
|
|
252
|
-
});
|
|
253
|
-
it("at - should return null for out of bounds", () => {
|
|
254
|
-
expect(evaluate({
|
|
255
|
-
kind: "at",
|
|
256
|
-
array: { kind: "lit", value: [10, 20, 30] },
|
|
257
|
-
index: { kind: "lit", value: 10 },
|
|
258
|
-
})).toBe(null);
|
|
259
|
-
});
|
|
260
|
-
it("at - should lookup record by string key", () => {
|
|
261
|
-
const record = { "item-1": { status: "open" }, "item-2": { status: "closed" } };
|
|
262
|
-
expect(evaluate({
|
|
263
|
-
kind: "at",
|
|
264
|
-
array: { kind: "lit", value: record },
|
|
265
|
-
index: { kind: "lit", value: "item-1" },
|
|
266
|
-
})).toEqual({ status: "open" });
|
|
267
|
-
});
|
|
268
|
-
it("at - should return null for missing record key", () => {
|
|
269
|
-
expect(evaluate({
|
|
270
|
-
kind: "at",
|
|
271
|
-
array: { kind: "lit", value: { a: 1 } },
|
|
272
|
-
index: { kind: "lit", value: "missing" },
|
|
273
|
-
})).toBe(null);
|
|
274
|
-
});
|
|
275
|
-
it("first - should get first element", () => {
|
|
276
|
-
expect(evaluate({ kind: "first", array: { kind: "lit", value: [1, 2, 3] } })).toBe(1);
|
|
277
|
-
expect(evaluate({ kind: "first", array: { kind: "lit", value: [] } })).toBe(null);
|
|
278
|
-
});
|
|
279
|
-
it("last - should get last element", () => {
|
|
280
|
-
expect(evaluate({ kind: "last", array: { kind: "lit", value: [1, 2, 3] } })).toBe(3);
|
|
281
|
-
expect(evaluate({ kind: "last", array: { kind: "lit", value: [] } })).toBe(null);
|
|
282
|
-
});
|
|
283
|
-
it("slice - should slice array", () => {
|
|
284
|
-
expect(evaluate({
|
|
285
|
-
kind: "slice",
|
|
286
|
-
array: { kind: "lit", value: [1, 2, 3, 4, 5] },
|
|
287
|
-
start: { kind: "lit", value: 1 },
|
|
288
|
-
end: { kind: "lit", value: 4 },
|
|
289
|
-
})).toEqual([2, 3, 4]);
|
|
290
|
-
});
|
|
291
|
-
it("slice - should handle no end parameter", () => {
|
|
292
|
-
expect(evaluate({
|
|
293
|
-
kind: "slice",
|
|
294
|
-
array: { kind: "lit", value: [1, 2, 3, 4, 5] },
|
|
295
|
-
start: { kind: "lit", value: 2 },
|
|
296
|
-
})).toEqual([3, 4, 5]);
|
|
297
|
-
});
|
|
298
|
-
it("includes - should check if array includes item", () => {
|
|
299
|
-
expect(evaluate({
|
|
300
|
-
kind: "includes",
|
|
301
|
-
array: { kind: "lit", value: [1, 2, 3] },
|
|
302
|
-
item: { kind: "lit", value: 2 },
|
|
303
|
-
})).toBe(true);
|
|
304
|
-
expect(evaluate({
|
|
305
|
-
kind: "includes",
|
|
306
|
-
array: { kind: "lit", value: [1, 2, 3] },
|
|
307
|
-
item: { kind: "lit", value: 5 },
|
|
308
|
-
})).toBe(false);
|
|
309
|
-
});
|
|
310
|
-
it("filter - should filter array with predicate", () => {
|
|
311
|
-
expect(evaluate({
|
|
312
|
-
kind: "filter",
|
|
313
|
-
array: { kind: "lit", value: [1, 2, 3, 4, 5] },
|
|
314
|
-
predicate: { kind: "gt", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 2 } },
|
|
315
|
-
})).toEqual([3, 4, 5]);
|
|
316
|
-
});
|
|
317
|
-
it("map - should map array with mapper", () => {
|
|
318
|
-
expect(evaluate({
|
|
319
|
-
kind: "map",
|
|
320
|
-
array: { kind: "lit", value: [1, 2, 3] },
|
|
321
|
-
mapper: { kind: "mul", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 2 } },
|
|
322
|
-
})).toEqual([2, 4, 6]);
|
|
323
|
-
});
|
|
324
|
-
it("find - should find first matching element", () => {
|
|
325
|
-
expect(evaluate({
|
|
326
|
-
kind: "find",
|
|
327
|
-
array: { kind: "lit", value: [1, 2, 3, 4, 5] },
|
|
328
|
-
predicate: { kind: "gt", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 3 } },
|
|
329
|
-
})).toBe(4);
|
|
330
|
-
});
|
|
331
|
-
it("find - should return null if not found", () => {
|
|
332
|
-
expect(evaluate({
|
|
333
|
-
kind: "find",
|
|
334
|
-
array: { kind: "lit", value: [1, 2, 3] },
|
|
335
|
-
predicate: { kind: "gt", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 10 } },
|
|
336
|
-
})).toBe(null);
|
|
337
|
-
});
|
|
338
|
-
it("every - should check if all elements match", () => {
|
|
339
|
-
expect(evaluate({
|
|
340
|
-
kind: "every",
|
|
341
|
-
array: { kind: "lit", value: [2, 4, 6] },
|
|
342
|
-
predicate: { kind: "eq", left: { kind: "mod", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 2 } }, right: { kind: "lit", value: 0 } },
|
|
343
|
-
})).toBe(true);
|
|
344
|
-
expect(evaluate({
|
|
345
|
-
kind: "every",
|
|
346
|
-
array: { kind: "lit", value: [2, 3, 6] },
|
|
347
|
-
predicate: { kind: "eq", left: { kind: "mod", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 2 } }, right: { kind: "lit", value: 0 } },
|
|
348
|
-
})).toBe(false);
|
|
349
|
-
});
|
|
350
|
-
it("some - should check if any element matches", () => {
|
|
351
|
-
expect(evaluate({
|
|
352
|
-
kind: "some",
|
|
353
|
-
array: { kind: "lit", value: [1, 2, 3] },
|
|
354
|
-
predicate: { kind: "eq", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 2 } },
|
|
355
|
-
})).toBe(true);
|
|
356
|
-
expect(evaluate({
|
|
357
|
-
kind: "some",
|
|
358
|
-
array: { kind: "lit", value: [1, 2, 3] },
|
|
359
|
-
predicate: { kind: "eq", left: { kind: "get", path: "$item" }, right: { kind: "lit", value: 5 } },
|
|
360
|
-
})).toBe(false);
|
|
361
|
-
});
|
|
362
|
-
it("collection ops - should provide $index", () => {
|
|
363
|
-
expect(evaluate({
|
|
364
|
-
kind: "map",
|
|
365
|
-
array: { kind: "lit", value: ["a", "b", "c"] },
|
|
366
|
-
mapper: { kind: "get", path: "$index" },
|
|
367
|
-
})).toEqual([0, 1, 2]);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
describe("Object", () => {
|
|
371
|
-
it("keys - should return object keys", () => {
|
|
372
|
-
expect(evaluate({ kind: "keys", obj: { kind: "lit", value: { a: 1, b: 2 } } })).toEqual(["a", "b"]);
|
|
373
|
-
});
|
|
374
|
-
it("values - should return object values", () => {
|
|
375
|
-
expect(evaluate({ kind: "values", obj: { kind: "lit", value: { a: 1, b: 2 } } })).toEqual([1, 2]);
|
|
376
|
-
});
|
|
377
|
-
it("entries - should return object entries", () => {
|
|
378
|
-
expect(evaluate({ kind: "entries", obj: { kind: "lit", value: { a: 1, b: 2 } } })).toEqual([["a", 1], ["b", 2]]);
|
|
379
|
-
});
|
|
380
|
-
it("merge - should merge objects", () => {
|
|
381
|
-
expect(evaluate({
|
|
382
|
-
kind: "merge",
|
|
383
|
-
objects: [
|
|
384
|
-
{ kind: "lit", value: { a: 1, b: 2 } },
|
|
385
|
-
{ kind: "lit", value: { b: 3, c: 4 } },
|
|
386
|
-
],
|
|
387
|
-
})).toEqual({ a: 1, b: 3, c: 4 });
|
|
388
|
-
});
|
|
389
|
-
it("object ops - should handle non-objects", () => {
|
|
390
|
-
expect(evaluate({ kind: "keys", obj: { kind: "lit", value: null } })).toEqual([]);
|
|
391
|
-
expect(evaluate({ kind: "values", obj: { kind: "lit", value: 42 } })).toEqual([]);
|
|
392
|
-
});
|
|
393
|
-
it("field - should access object property by static key", () => {
|
|
394
|
-
expect(evaluate({
|
|
395
|
-
kind: "field",
|
|
396
|
-
object: { kind: "lit", value: { status: "open", priority: 1 } },
|
|
397
|
-
property: "status",
|
|
398
|
-
})).toBe("open");
|
|
399
|
-
});
|
|
400
|
-
it("field - should return null for missing property", () => {
|
|
401
|
-
expect(evaluate({
|
|
402
|
-
kind: "field",
|
|
403
|
-
object: { kind: "lit", value: { status: "open" } },
|
|
404
|
-
property: "missing",
|
|
405
|
-
})).toBe(null);
|
|
406
|
-
});
|
|
407
|
-
it("field - should return null for non-object base", () => {
|
|
408
|
-
expect(evaluate({
|
|
409
|
-
kind: "field",
|
|
410
|
-
object: { kind: "lit", value: null },
|
|
411
|
-
property: "status",
|
|
412
|
-
})).toBe(null);
|
|
413
|
-
expect(evaluate({
|
|
414
|
-
kind: "field",
|
|
415
|
-
object: { kind: "lit", value: [1, 2, 3] },
|
|
416
|
-
property: "status",
|
|
417
|
-
})).toBe(null);
|
|
418
|
-
expect(evaluate({
|
|
419
|
-
kind: "field",
|
|
420
|
-
object: { kind: "lit", value: 42 },
|
|
421
|
-
property: "status",
|
|
422
|
-
})).toBe(null);
|
|
423
|
-
});
|
|
424
|
-
it("field - should work on result of at() (Issue #135)", () => {
|
|
425
|
-
// Simulates at(items, id).status where items is a record
|
|
426
|
-
const items = { "item-1": { status: "open" }, "item-2": { status: "closed" } };
|
|
427
|
-
expect(evaluate({
|
|
428
|
-
kind: "field",
|
|
429
|
-
object: {
|
|
430
|
-
kind: "at",
|
|
431
|
-
array: { kind: "lit", value: items },
|
|
432
|
-
index: { kind: "lit", value: "item-1" },
|
|
433
|
-
},
|
|
434
|
-
property: "status",
|
|
435
|
-
})).toBe("open");
|
|
436
|
-
});
|
|
437
|
-
});
|
|
438
|
-
describe("Type", () => {
|
|
439
|
-
it("typeof - should return type name", () => {
|
|
440
|
-
expect(evaluate({ kind: "typeof", arg: { kind: "lit", value: 42 } })).toBe("number");
|
|
441
|
-
expect(evaluate({ kind: "typeof", arg: { kind: "lit", value: "hello" } })).toBe("string");
|
|
442
|
-
expect(evaluate({ kind: "typeof", arg: { kind: "lit", value: true } })).toBe("boolean");
|
|
443
|
-
expect(evaluate({ kind: "typeof", arg: { kind: "lit", value: null } })).toBe("null");
|
|
444
|
-
expect(evaluate({ kind: "typeof", arg: { kind: "lit", value: [1, 2] } })).toBe("array");
|
|
445
|
-
expect(evaluate({ kind: "typeof", arg: { kind: "lit", value: { a: 1 } } })).toBe("object");
|
|
446
|
-
});
|
|
447
|
-
it("isNull - should check for null/undefined", () => {
|
|
448
|
-
expect(evaluate({ kind: "isNull", arg: { kind: "lit", value: null } })).toBe(true);
|
|
449
|
-
expect(evaluate({ kind: "isNull", arg: { kind: "lit", value: undefined } })).toBe(true);
|
|
450
|
-
expect(evaluate({ kind: "isNull", arg: { kind: "lit", value: 0 } })).toBe(false);
|
|
451
|
-
expect(evaluate({ kind: "isNull", arg: { kind: "lit", value: "" } })).toBe(false);
|
|
452
|
-
});
|
|
453
|
-
it("coalesce - should return first non-null value", () => {
|
|
454
|
-
expect(evaluate({
|
|
455
|
-
kind: "coalesce",
|
|
456
|
-
args: [{ kind: "lit", value: null }, { kind: "lit", value: undefined }, { kind: "lit", value: 42 }],
|
|
457
|
-
})).toBe(42);
|
|
458
|
-
expect(evaluate({
|
|
459
|
-
kind: "coalesce",
|
|
460
|
-
args: [{ kind: "lit", value: "first" }, { kind: "lit", value: "second" }],
|
|
461
|
-
})).toBe("first");
|
|
462
|
-
expect(evaluate({
|
|
463
|
-
kind: "coalesce",
|
|
464
|
-
args: [{ kind: "lit", value: null }, { kind: "lit", value: null }],
|
|
465
|
-
})).toBe(null);
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
describe("String Extended", () => {
|
|
469
|
-
it("trim - should remove whitespace", () => {
|
|
470
|
-
expect(evaluate({ kind: "trim", str: { kind: "lit", value: " hello " } })).toBe("hello");
|
|
471
|
-
expect(evaluate({ kind: "trim", str: { kind: "lit", value: "\t\nhello\t\n" } })).toBe("hello");
|
|
472
|
-
expect(evaluate({ kind: "trim", str: { kind: "lit", value: "hello" } })).toBe("hello");
|
|
473
|
-
});
|
|
474
|
-
it("trim - should coerce non-strings", () => {
|
|
475
|
-
expect(evaluate({ kind: "trim", str: { kind: "lit", value: 42 } })).toBe("42");
|
|
476
|
-
expect(evaluate({ kind: "trim", str: { kind: "lit", value: null } })).toBe("");
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
describe("Nested Expressions", () => {
|
|
480
|
-
it("should evaluate complex nested expressions", () => {
|
|
481
|
-
// (2 + 3) * 4 = 20
|
|
482
|
-
expect(evaluate({
|
|
483
|
-
kind: "mul",
|
|
484
|
-
left: {
|
|
485
|
-
kind: "add",
|
|
486
|
-
left: { kind: "lit", value: 2 },
|
|
487
|
-
right: { kind: "lit", value: 3 },
|
|
488
|
-
},
|
|
489
|
-
right: { kind: "lit", value: 4 },
|
|
490
|
-
})).toBe(20);
|
|
491
|
-
});
|
|
492
|
-
it("should evaluate complex conditional", () => {
|
|
493
|
-
const ctx = createTestContext({ items: [1, 2, 3, 4, 5] });
|
|
494
|
-
// if len(items) > 3 then first(items) else last(items)
|
|
495
|
-
expect(evaluate({
|
|
496
|
-
kind: "if",
|
|
497
|
-
cond: {
|
|
498
|
-
kind: "gt",
|
|
499
|
-
left: { kind: "len", arg: { kind: "get", path: "items" } },
|
|
500
|
-
right: { kind: "lit", value: 3 },
|
|
501
|
-
},
|
|
502
|
-
then: { kind: "first", array: { kind: "get", path: "items" } },
|
|
503
|
-
else: { kind: "last", array: { kind: "get", path: "items" } },
|
|
504
|
-
}, ctx)).toBe(1);
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
// ============================================================
|
|
508
|
-
// SPEC v2.0.0 §7.2 — Missing Arithmetic Primitives
|
|
509
|
-
// ============================================================
|
|
510
|
-
describe("Arithmetic Extended (SPEC v2.0.0)", () => {
|
|
511
|
-
// --- floor ---
|
|
512
|
-
it("floor - should round down to nearest integer", () => {
|
|
513
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: 4.7 } })).toBe(4);
|
|
514
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: 4.2 } })).toBe(4);
|
|
515
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: 4.0 } })).toBe(4);
|
|
516
|
-
});
|
|
517
|
-
it("floor - should handle negative numbers", () => {
|
|
518
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: -2.3 } })).toBe(-3);
|
|
519
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: -2.0 } })).toBe(-2);
|
|
520
|
-
});
|
|
521
|
-
it("floor - should coerce non-number input", () => {
|
|
522
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: "3.7" } })).toBe(3);
|
|
523
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: true } })).toBe(1);
|
|
524
|
-
expect(evaluate({ kind: "floor", arg: { kind: "lit", value: null } })).toBe(0);
|
|
525
|
-
});
|
|
526
|
-
// --- ceil ---
|
|
527
|
-
it("ceil - should round up to nearest integer", () => {
|
|
528
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: 4.2 } })).toBe(5);
|
|
529
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: 4.7 } })).toBe(5);
|
|
530
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: 4.0 } })).toBe(4);
|
|
531
|
-
});
|
|
532
|
-
it("ceil - should handle negative numbers", () => {
|
|
533
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: -2.3 } })).toBe(-2);
|
|
534
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: -2.0 } })).toBe(-2);
|
|
535
|
-
});
|
|
536
|
-
it("ceil - should coerce non-number input", () => {
|
|
537
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: "3.2" } })).toBe(4);
|
|
538
|
-
expect(evaluate({ kind: "ceil", arg: { kind: "lit", value: null } })).toBe(0);
|
|
539
|
-
});
|
|
540
|
-
// --- round ---
|
|
541
|
-
it("round - should round to nearest integer", () => {
|
|
542
|
-
expect(evaluate({ kind: "round", arg: { kind: "lit", value: 4.5 } })).toBe(5);
|
|
543
|
-
expect(evaluate({ kind: "round", arg: { kind: "lit", value: 4.4 } })).toBe(4);
|
|
544
|
-
expect(evaluate({ kind: "round", arg: { kind: "lit", value: 4.6 } })).toBe(5);
|
|
545
|
-
expect(evaluate({ kind: "round", arg: { kind: "lit", value: -2.5 } })).toBe(-2); // Math.round(-2.5) = -2
|
|
546
|
-
});
|
|
547
|
-
it("round - should coerce non-number input", () => {
|
|
548
|
-
expect(evaluate({ kind: "round", arg: { kind: "lit", value: "3.6" } })).toBe(4);
|
|
549
|
-
expect(evaluate({ kind: "round", arg: { kind: "lit", value: null } })).toBe(0);
|
|
550
|
-
});
|
|
551
|
-
// --- sqrt ---
|
|
552
|
-
it("sqrt - should compute square root", () => {
|
|
553
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: 9 } })).toBe(3);
|
|
554
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: 0 } })).toBe(0);
|
|
555
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: 2 } })).toBeCloseTo(1.4142135);
|
|
556
|
-
});
|
|
557
|
-
it("sqrt - should return null for negative numbers (totality: no NaN)", () => {
|
|
558
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: -1 } })).toBe(null);
|
|
559
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: -100 } })).toBe(null);
|
|
560
|
-
});
|
|
561
|
-
it("sqrt - should coerce non-number input", () => {
|
|
562
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: "16" } })).toBe(4);
|
|
563
|
-
expect(evaluate({ kind: "sqrt", arg: { kind: "lit", value: null } })).toBe(0); // sqrt(0) = 0
|
|
564
|
-
});
|
|
565
|
-
// --- pow ---
|
|
566
|
-
it("pow - should compute exponentiation", () => {
|
|
567
|
-
expect(evaluate({
|
|
568
|
-
kind: "pow",
|
|
569
|
-
base: { kind: "lit", value: 2 },
|
|
570
|
-
exponent: { kind: "lit", value: 10 },
|
|
571
|
-
})).toBe(1024);
|
|
572
|
-
expect(evaluate({
|
|
573
|
-
kind: "pow",
|
|
574
|
-
base: { kind: "lit", value: 3 },
|
|
575
|
-
exponent: { kind: "lit", value: 0 },
|
|
576
|
-
})).toBe(1);
|
|
577
|
-
expect(evaluate({
|
|
578
|
-
kind: "pow",
|
|
579
|
-
base: { kind: "lit", value: 5 },
|
|
580
|
-
exponent: { kind: "lit", value: 2 },
|
|
581
|
-
})).toBe(25);
|
|
582
|
-
});
|
|
583
|
-
it("pow - should handle negative exponents", () => {
|
|
584
|
-
expect(evaluate({
|
|
585
|
-
kind: "pow",
|
|
586
|
-
base: { kind: "lit", value: 2 },
|
|
587
|
-
exponent: { kind: "lit", value: -1 },
|
|
588
|
-
})).toBe(0.5);
|
|
589
|
-
});
|
|
590
|
-
it("pow - should return null for non-finite results (totality)", () => {
|
|
591
|
-
// 0^(-1) = Infinity -> null
|
|
592
|
-
expect(evaluate({
|
|
593
|
-
kind: "pow",
|
|
594
|
-
base: { kind: "lit", value: 0 },
|
|
595
|
-
exponent: { kind: "lit", value: -1 },
|
|
596
|
-
})).toBe(null);
|
|
597
|
-
});
|
|
598
|
-
it("pow - should coerce non-number inputs", () => {
|
|
599
|
-
expect(evaluate({
|
|
600
|
-
kind: "pow",
|
|
601
|
-
base: { kind: "lit", value: "2" },
|
|
602
|
-
exponent: { kind: "lit", value: "3" },
|
|
603
|
-
})).toBe(8);
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
// ============================================================
|
|
607
|
-
// SPEC v2.0.0 §7.2 — Array Aggregation Primitives
|
|
608
|
-
// ============================================================
|
|
609
|
-
describe("Array Aggregation (SPEC v2.0.0)", () => {
|
|
610
|
-
// --- sumArray ---
|
|
611
|
-
it("sumArray - should sum numeric array", () => {
|
|
612
|
-
expect(evaluate({ kind: "sumArray", array: { kind: "lit", value: [1, 2, 3, 4, 5] } })).toBe(15);
|
|
613
|
-
expect(evaluate({ kind: "sumArray", array: { kind: "lit", value: [10, -5, 3] } })).toBe(8);
|
|
614
|
-
});
|
|
615
|
-
it("sumArray - should return 0 for empty array", () => {
|
|
616
|
-
expect(evaluate({ kind: "sumArray", array: { kind: "lit", value: [] } })).toBe(0);
|
|
617
|
-
});
|
|
618
|
-
it("sumArray - should coerce non-numeric elements", () => {
|
|
619
|
-
expect(evaluate({ kind: "sumArray", array: { kind: "lit", value: [1, "2", true] } })).toBe(4); // 1 + 2 + 1
|
|
620
|
-
});
|
|
621
|
-
it("sumArray - should return 0 for non-array input", () => {
|
|
622
|
-
expect(evaluate({ kind: "sumArray", array: { kind: "lit", value: "not array" } })).toBe(0);
|
|
623
|
-
expect(evaluate({ kind: "sumArray", array: { kind: "lit", value: null } })).toBe(0);
|
|
624
|
-
});
|
|
625
|
-
// --- minArray ---
|
|
626
|
-
it("minArray - should find minimum in numeric array", () => {
|
|
627
|
-
expect(evaluate({ kind: "minArray", array: { kind: "lit", value: [5, 1, 3, 2, 4] } })).toBe(1);
|
|
628
|
-
expect(evaluate({ kind: "minArray", array: { kind: "lit", value: [-10, 0, 10] } })).toBe(-10);
|
|
629
|
-
});
|
|
630
|
-
it("minArray - should return null for empty array", () => {
|
|
631
|
-
expect(evaluate({ kind: "minArray", array: { kind: "lit", value: [] } })).toBe(null);
|
|
632
|
-
});
|
|
633
|
-
it("minArray - should return null for non-array input", () => {
|
|
634
|
-
expect(evaluate({ kind: "minArray", array: { kind: "lit", value: null } })).toBe(null);
|
|
635
|
-
});
|
|
636
|
-
// --- maxArray ---
|
|
637
|
-
it("maxArray - should find maximum in numeric array", () => {
|
|
638
|
-
expect(evaluate({ kind: "maxArray", array: { kind: "lit", value: [5, 1, 3, 2, 4] } })).toBe(5);
|
|
639
|
-
expect(evaluate({ kind: "maxArray", array: { kind: "lit", value: [-10, 0, 10] } })).toBe(10);
|
|
640
|
-
});
|
|
641
|
-
it("maxArray - should return null for empty array", () => {
|
|
642
|
-
expect(evaluate({ kind: "maxArray", array: { kind: "lit", value: [] } })).toBe(null);
|
|
643
|
-
});
|
|
644
|
-
it("maxArray - should return null for non-array input", () => {
|
|
645
|
-
expect(evaluate({ kind: "maxArray", array: { kind: "lit", value: null } })).toBe(null);
|
|
646
|
-
});
|
|
647
|
-
});
|
|
648
|
-
// ============================================================
|
|
649
|
-
// SPEC v2.0.0 §7.2 — String Primitives
|
|
650
|
-
// ============================================================
|
|
651
|
-
describe("String Operations (SPEC v2.0.0)", () => {
|
|
652
|
-
// --- toLowerCase ---
|
|
653
|
-
it("toLowerCase - should convert string to lower case", () => {
|
|
654
|
-
expect(evaluate({ kind: "toLowerCase", str: { kind: "lit", value: "HELLO WORLD" } })).toBe("hello world");
|
|
655
|
-
expect(evaluate({ kind: "toLowerCase", str: { kind: "lit", value: "Hello" } })).toBe("hello");
|
|
656
|
-
expect(evaluate({ kind: "toLowerCase", str: { kind: "lit", value: "already lower" } })).toBe("already lower");
|
|
657
|
-
});
|
|
658
|
-
it("toLowerCase - should coerce non-string input", () => {
|
|
659
|
-
expect(evaluate({ kind: "toLowerCase", str: { kind: "lit", value: 42 } })).toBe("42");
|
|
660
|
-
expect(evaluate({ kind: "toLowerCase", str: { kind: "lit", value: null } })).toBe("");
|
|
661
|
-
expect(evaluate({ kind: "toLowerCase", str: { kind: "lit", value: true } })).toBe("true");
|
|
662
|
-
});
|
|
663
|
-
// --- toUpperCase ---
|
|
664
|
-
it("toUpperCase - should convert string to upper case", () => {
|
|
665
|
-
expect(evaluate({ kind: "toUpperCase", str: { kind: "lit", value: "hello world" } })).toBe("HELLO WORLD");
|
|
666
|
-
expect(evaluate({ kind: "toUpperCase", str: { kind: "lit", value: "Hello" } })).toBe("HELLO");
|
|
667
|
-
expect(evaluate({ kind: "toUpperCase", str: { kind: "lit", value: "ALREADY UPPER" } })).toBe("ALREADY UPPER");
|
|
668
|
-
});
|
|
669
|
-
it("toUpperCase - should coerce non-string input", () => {
|
|
670
|
-
expect(evaluate({ kind: "toUpperCase", str: { kind: "lit", value: 42 } })).toBe("42");
|
|
671
|
-
expect(evaluate({ kind: "toUpperCase", str: { kind: "lit", value: null } })).toBe("");
|
|
672
|
-
});
|
|
673
|
-
// --- strLen ---
|
|
674
|
-
it("strLen - should return string length", () => {
|
|
675
|
-
expect(evaluate({ kind: "strLen", str: { kind: "lit", value: "hello" } })).toBe(5);
|
|
676
|
-
expect(evaluate({ kind: "strLen", str: { kind: "lit", value: "" } })).toBe(0);
|
|
677
|
-
expect(evaluate({ kind: "strLen", str: { kind: "lit", value: "abc def" } })).toBe(7);
|
|
678
|
-
});
|
|
679
|
-
it("strLen - should coerce non-string input", () => {
|
|
680
|
-
expect(evaluate({ kind: "strLen", str: { kind: "lit", value: 12345 } })).toBe(5); // "12345".length
|
|
681
|
-
expect(evaluate({ kind: "strLen", str: { kind: "lit", value: null } })).toBe(0); // "".length
|
|
682
|
-
expect(evaluate({ kind: "strLen", str: { kind: "lit", value: true } })).toBe(4); // "true".length
|
|
683
|
-
});
|
|
684
|
-
});
|
|
685
|
-
// ============================================================
|
|
686
|
-
// SPEC v2.0.0 §7.2 + v2.0.3 — Conversion Primitives
|
|
687
|
-
// ============================================================
|
|
688
|
-
describe("Conversion (SPEC v2.0.0 + v2.0.3)", () => {
|
|
689
|
-
// --- toString (SPEC v2.0.0) ---
|
|
690
|
-
it("toString - should convert values to string", () => {
|
|
691
|
-
expect(evaluate({ kind: "toString", arg: { kind: "lit", value: 42 } })).toBe("42");
|
|
692
|
-
expect(evaluate({ kind: "toString", arg: { kind: "lit", value: true } })).toBe("true");
|
|
693
|
-
expect(evaluate({ kind: "toString", arg: { kind: "lit", value: false } })).toBe("false");
|
|
694
|
-
expect(evaluate({ kind: "toString", arg: { kind: "lit", value: "hello" } })).toBe("hello");
|
|
695
|
-
});
|
|
696
|
-
it("toString - should handle null/undefined", () => {
|
|
697
|
-
expect(evaluate({ kind: "toString", arg: { kind: "lit", value: null } })).toBe("");
|
|
698
|
-
expect(evaluate({ kind: "toString", arg: { kind: "lit", value: undefined } })).toBe("");
|
|
699
|
-
});
|
|
700
|
-
// --- toNumber (SPEC v2.0.3) ---
|
|
701
|
-
it("toNumber - should convert numeric string to number", () => {
|
|
702
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: "42" } })).toBe(42);
|
|
703
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: "3.14" } })).toBe(3.14);
|
|
704
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: "-10" } })).toBe(-10);
|
|
705
|
-
});
|
|
706
|
-
it("toNumber - should return identity for numbers", () => {
|
|
707
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: 42 } })).toBe(42);
|
|
708
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: 0 } })).toBe(0);
|
|
709
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: -3.5 } })).toBe(-3.5);
|
|
710
|
-
});
|
|
711
|
-
it("toNumber - should convert booleans: true=1, false=0", () => {
|
|
712
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: true } })).toBe(1);
|
|
713
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: false } })).toBe(0);
|
|
714
|
-
});
|
|
715
|
-
it("toNumber - should return 0 for null/undefined", () => {
|
|
716
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: null } })).toBe(0);
|
|
717
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: undefined } })).toBe(0);
|
|
718
|
-
});
|
|
719
|
-
it("toNumber - MUST return 0 for non-numeric strings (never NaN)", () => {
|
|
720
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: "hello" } })).toBe(0);
|
|
721
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: "" } })).toBe(0);
|
|
722
|
-
});
|
|
723
|
-
it("toNumber - should return 0 for objects/arrays", () => {
|
|
724
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: { a: 1 } } })).toBe(0);
|
|
725
|
-
expect(evaluate({ kind: "toNumber", arg: { kind: "lit", value: [1, 2] } })).toBe(0);
|
|
726
|
-
});
|
|
727
|
-
// --- toBoolean (SPEC v2.0.3) ---
|
|
728
|
-
it("toBoolean - should return identity for booleans", () => {
|
|
729
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: true } })).toBe(true);
|
|
730
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: false } })).toBe(false);
|
|
731
|
-
});
|
|
732
|
-
it("toBoolean - should convert null/undefined to false", () => {
|
|
733
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: null } })).toBe(false);
|
|
734
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: undefined } })).toBe(false);
|
|
735
|
-
});
|
|
736
|
-
it("toBoolean - should convert numbers: 0=false, all others=true", () => {
|
|
737
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: 0 } })).toBe(false);
|
|
738
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: 1 } })).toBe(true);
|
|
739
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: -1 } })).toBe(true);
|
|
740
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: 42 } })).toBe(true);
|
|
741
|
-
});
|
|
742
|
-
it("toBoolean - should convert strings: empty=false, non-empty=true", () => {
|
|
743
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: "" } })).toBe(false);
|
|
744
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: "hello" } })).toBe(true);
|
|
745
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: "false" } })).toBe(true); // non-empty = true
|
|
746
|
-
});
|
|
747
|
-
it("toBoolean - should convert objects/arrays to true (always truthy)", () => {
|
|
748
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: {} } })).toBe(true);
|
|
749
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: [] } })).toBe(true);
|
|
750
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: { a: 1 } } })).toBe(true);
|
|
751
|
-
expect(evaluate({ kind: "toBoolean", arg: { kind: "lit", value: [1, 2] } })).toBe(true);
|
|
752
|
-
});
|
|
753
|
-
});
|
|
754
|
-
// ============================================================
|
|
755
|
-
// SPEC v2.0.3 §1.1 — String Extensions
|
|
756
|
-
// ============================================================
|
|
757
|
-
describe("String Extensions (SPEC v2.0.3)", () => {
|
|
758
|
-
// --- startsWith ---
|
|
759
|
-
it("startsWith - should check if string starts with prefix", () => {
|
|
760
|
-
expect(evaluate({
|
|
761
|
-
kind: "startsWith",
|
|
762
|
-
str: { kind: "lit", value: "hello world" },
|
|
763
|
-
prefix: { kind: "lit", value: "hello" },
|
|
764
|
-
})).toBe(true);
|
|
765
|
-
expect(evaluate({
|
|
766
|
-
kind: "startsWith",
|
|
767
|
-
str: { kind: "lit", value: "hello world" },
|
|
768
|
-
prefix: { kind: "lit", value: "world" },
|
|
769
|
-
})).toBe(false);
|
|
770
|
-
});
|
|
771
|
-
it("startsWith - should handle empty prefix (always true)", () => {
|
|
772
|
-
expect(evaluate({
|
|
773
|
-
kind: "startsWith",
|
|
774
|
-
str: { kind: "lit", value: "hello" },
|
|
775
|
-
prefix: { kind: "lit", value: "" },
|
|
776
|
-
})).toBe(true);
|
|
777
|
-
});
|
|
778
|
-
it("startsWith - should coerce non-string inputs", () => {
|
|
779
|
-
expect(evaluate({
|
|
780
|
-
kind: "startsWith",
|
|
781
|
-
str: { kind: "lit", value: 12345 },
|
|
782
|
-
prefix: { kind: "lit", value: "123" },
|
|
783
|
-
})).toBe(true);
|
|
784
|
-
expect(evaluate({
|
|
785
|
-
kind: "startsWith",
|
|
786
|
-
str: { kind: "lit", value: null },
|
|
787
|
-
prefix: { kind: "lit", value: "" },
|
|
788
|
-
})).toBe(true); // "" starts with ""
|
|
789
|
-
});
|
|
790
|
-
// --- endsWith ---
|
|
791
|
-
it("endsWith - should check if string ends with suffix", () => {
|
|
792
|
-
expect(evaluate({
|
|
793
|
-
kind: "endsWith",
|
|
794
|
-
str: { kind: "lit", value: "hello world" },
|
|
795
|
-
suffix: { kind: "lit", value: "world" },
|
|
796
|
-
})).toBe(true);
|
|
797
|
-
expect(evaluate({
|
|
798
|
-
kind: "endsWith",
|
|
799
|
-
str: { kind: "lit", value: "hello world" },
|
|
800
|
-
suffix: { kind: "lit", value: "hello" },
|
|
801
|
-
})).toBe(false);
|
|
802
|
-
});
|
|
803
|
-
it("endsWith - should handle empty suffix (always true)", () => {
|
|
804
|
-
expect(evaluate({
|
|
805
|
-
kind: "endsWith",
|
|
806
|
-
str: { kind: "lit", value: "hello" },
|
|
807
|
-
suffix: { kind: "lit", value: "" },
|
|
808
|
-
})).toBe(true);
|
|
809
|
-
});
|
|
810
|
-
it("endsWith - should coerce non-string inputs", () => {
|
|
811
|
-
expect(evaluate({
|
|
812
|
-
kind: "endsWith",
|
|
813
|
-
str: { kind: "lit", value: 12345 },
|
|
814
|
-
suffix: { kind: "lit", value: "45" },
|
|
815
|
-
})).toBe(true);
|
|
816
|
-
});
|
|
817
|
-
// --- strIncludes ---
|
|
818
|
-
it("strIncludes - should check if string contains search", () => {
|
|
819
|
-
expect(evaluate({
|
|
820
|
-
kind: "strIncludes",
|
|
821
|
-
str: { kind: "lit", value: "hello world" },
|
|
822
|
-
search: { kind: "lit", value: "lo wo" },
|
|
823
|
-
})).toBe(true);
|
|
824
|
-
expect(evaluate({
|
|
825
|
-
kind: "strIncludes",
|
|
826
|
-
str: { kind: "lit", value: "hello world" },
|
|
827
|
-
search: { kind: "lit", value: "xyz" },
|
|
828
|
-
})).toBe(false);
|
|
829
|
-
});
|
|
830
|
-
it("strIncludes - should handle empty search (always true)", () => {
|
|
831
|
-
expect(evaluate({
|
|
832
|
-
kind: "strIncludes",
|
|
833
|
-
str: { kind: "lit", value: "hello" },
|
|
834
|
-
search: { kind: "lit", value: "" },
|
|
835
|
-
})).toBe(true);
|
|
836
|
-
});
|
|
837
|
-
it("strIncludes - should coerce non-string inputs", () => {
|
|
838
|
-
expect(evaluate({
|
|
839
|
-
kind: "strIncludes",
|
|
840
|
-
str: { kind: "lit", value: null },
|
|
841
|
-
search: { kind: "lit", value: "" },
|
|
842
|
-
})).toBe(true); // "" includes ""
|
|
843
|
-
});
|
|
844
|
-
// --- indexOf ---
|
|
845
|
-
it("indexOf - should return index of search string", () => {
|
|
846
|
-
expect(evaluate({
|
|
847
|
-
kind: "indexOf",
|
|
848
|
-
str: { kind: "lit", value: "hello world" },
|
|
849
|
-
search: { kind: "lit", value: "world" },
|
|
850
|
-
})).toBe(6);
|
|
851
|
-
expect(evaluate({
|
|
852
|
-
kind: "indexOf",
|
|
853
|
-
str: { kind: "lit", value: "hello world" },
|
|
854
|
-
search: { kind: "lit", value: "hello" },
|
|
855
|
-
})).toBe(0);
|
|
856
|
-
});
|
|
857
|
-
it("indexOf - MUST return -1 when search not found", () => {
|
|
858
|
-
expect(evaluate({
|
|
859
|
-
kind: "indexOf",
|
|
860
|
-
str: { kind: "lit", value: "hello world" },
|
|
861
|
-
search: { kind: "lit", value: "xyz" },
|
|
862
|
-
})).toBe(-1);
|
|
863
|
-
});
|
|
864
|
-
it("indexOf - should find first occurrence", () => {
|
|
865
|
-
expect(evaluate({
|
|
866
|
-
kind: "indexOf",
|
|
867
|
-
str: { kind: "lit", value: "abcabc" },
|
|
868
|
-
search: { kind: "lit", value: "bc" },
|
|
869
|
-
})).toBe(1); // First occurrence at index 1
|
|
870
|
-
});
|
|
871
|
-
it("indexOf - should coerce non-string inputs", () => {
|
|
872
|
-
expect(evaluate({
|
|
873
|
-
kind: "indexOf",
|
|
874
|
-
str: { kind: "lit", value: 12345 },
|
|
875
|
-
search: { kind: "lit", value: "34" },
|
|
876
|
-
})).toBe(2);
|
|
877
|
-
});
|
|
878
|
-
// --- replace ---
|
|
879
|
-
it("replace - MUST replace only first occurrence", () => {
|
|
880
|
-
expect(evaluate({
|
|
881
|
-
kind: "replace",
|
|
882
|
-
str: { kind: "lit", value: "aaa bbb aaa" },
|
|
883
|
-
search: { kind: "lit", value: "aaa" },
|
|
884
|
-
replacement: { kind: "lit", value: "ccc" },
|
|
885
|
-
})).toBe("ccc bbb aaa"); // Only first "aaa" replaced
|
|
886
|
-
});
|
|
887
|
-
it("replace - should return original string if no match", () => {
|
|
888
|
-
expect(evaluate({
|
|
889
|
-
kind: "replace",
|
|
890
|
-
str: { kind: "lit", value: "hello world" },
|
|
891
|
-
search: { kind: "lit", value: "xyz" },
|
|
892
|
-
replacement: { kind: "lit", value: "replaced" },
|
|
893
|
-
})).toBe("hello world");
|
|
894
|
-
});
|
|
895
|
-
it("replace - should handle empty search (prepend replacement)", () => {
|
|
896
|
-
expect(evaluate({
|
|
897
|
-
kind: "replace",
|
|
898
|
-
str: { kind: "lit", value: "hello" },
|
|
899
|
-
search: { kind: "lit", value: "" },
|
|
900
|
-
replacement: { kind: "lit", value: "X" },
|
|
901
|
-
})).toBe("Xhello"); // JS behavior: "hello".replace("", "X") = "Xhello"
|
|
902
|
-
});
|
|
903
|
-
it("replace - should coerce non-string inputs", () => {
|
|
904
|
-
expect(evaluate({
|
|
905
|
-
kind: "replace",
|
|
906
|
-
str: { kind: "lit", value: 12345 },
|
|
907
|
-
search: { kind: "lit", value: "23" },
|
|
908
|
-
replacement: { kind: "lit", value: "XX" },
|
|
909
|
-
})).toBe("1XX45");
|
|
910
|
-
});
|
|
911
|
-
// --- split ---
|
|
912
|
-
it("split - should split string by delimiter", () => {
|
|
913
|
-
expect(evaluate({
|
|
914
|
-
kind: "split",
|
|
915
|
-
str: { kind: "lit", value: "a,b,c" },
|
|
916
|
-
delimiter: { kind: "lit", value: "," },
|
|
917
|
-
})).toEqual(["a", "b", "c"]);
|
|
918
|
-
});
|
|
919
|
-
it("split - MUST always return at least one element", () => {
|
|
920
|
-
// When no delimiter found, returns array with original string
|
|
921
|
-
expect(evaluate({
|
|
922
|
-
kind: "split",
|
|
923
|
-
str: { kind: "lit", value: "hello" },
|
|
924
|
-
delimiter: { kind: "lit", value: "," },
|
|
925
|
-
})).toEqual(["hello"]);
|
|
926
|
-
});
|
|
927
|
-
it("split - should split by empty string into characters", () => {
|
|
928
|
-
expect(evaluate({
|
|
929
|
-
kind: "split",
|
|
930
|
-
str: { kind: "lit", value: "abc" },
|
|
931
|
-
delimiter: { kind: "lit", value: "" },
|
|
932
|
-
})).toEqual(["a", "b", "c"]);
|
|
933
|
-
});
|
|
934
|
-
it("split - should handle empty string input", () => {
|
|
935
|
-
expect(evaluate({
|
|
936
|
-
kind: "split",
|
|
937
|
-
str: { kind: "lit", value: "" },
|
|
938
|
-
delimiter: { kind: "lit", value: "," },
|
|
939
|
-
})).toEqual([""]);
|
|
940
|
-
});
|
|
941
|
-
it("split - should coerce non-string inputs", () => {
|
|
942
|
-
expect(evaluate({
|
|
943
|
-
kind: "split",
|
|
944
|
-
str: { kind: "lit", value: null },
|
|
945
|
-
delimiter: { kind: "lit", value: "," },
|
|
946
|
-
})).toEqual([""]); // toString(null) = ""
|
|
947
|
-
});
|
|
948
|
-
it("split - should return non-empty array for empty string with empty delimiter", () => {
|
|
949
|
-
// JS returns [] for "".split(""), but SPEC requires at least one element
|
|
950
|
-
expect(evaluate({
|
|
951
|
-
kind: "split",
|
|
952
|
-
str: { kind: "lit", value: "" },
|
|
953
|
-
delimiter: { kind: "lit", value: "" },
|
|
954
|
-
})).toEqual([""]);
|
|
955
|
-
});
|
|
956
|
-
});
|
|
957
|
-
// ============================================================
|
|
958
|
-
// SPEC v2.0.3 §1.2 — Collection Extensions
|
|
959
|
-
// ============================================================
|
|
960
|
-
describe("Collection Extensions (SPEC v2.0.3)", () => {
|
|
961
|
-
// --- reverse ---
|
|
962
|
-
it("reverse - should reverse array element order", () => {
|
|
963
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: [1, 2, 3] } })).toEqual([3, 2, 1]);
|
|
964
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: ["a", "b", "c"] } })).toEqual(["c", "b", "a"]);
|
|
965
|
-
});
|
|
966
|
-
it("reverse - should handle empty array", () => {
|
|
967
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: [] } })).toEqual([]);
|
|
968
|
-
});
|
|
969
|
-
it("reverse - should handle single-element array", () => {
|
|
970
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: [42] } })).toEqual([42]);
|
|
971
|
-
});
|
|
972
|
-
it("reverse - should return [] for non-array input", () => {
|
|
973
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: null } })).toEqual([]);
|
|
974
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: "not array" } })).toEqual([]);
|
|
975
|
-
expect(evaluate({ kind: "reverse", array: { kind: "lit", value: 42 } })).toEqual([]);
|
|
976
|
-
});
|
|
977
|
-
// --- unique ---
|
|
978
|
-
it("unique - should remove duplicates keeping first occurrence", () => {
|
|
979
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: [1, 2, 1, 3, 2] } })).toEqual([1, 2, 3]);
|
|
980
|
-
});
|
|
981
|
-
it("unique - MUST preserve first-occurrence order", () => {
|
|
982
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: [3, 1, 2, 1, 3] } })).toEqual([3, 1, 2]);
|
|
983
|
-
});
|
|
984
|
-
it("unique - MUST use strict equality (===)", () => {
|
|
985
|
-
// 1 !== "1" in strict equality
|
|
986
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: [1, "1", 1, "1"] } })).toEqual([1, "1"]);
|
|
987
|
-
// null !== undefined
|
|
988
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: [null, undefined, null] } })).toEqual([null, undefined]);
|
|
989
|
-
});
|
|
990
|
-
it("unique - should handle empty array", () => {
|
|
991
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: [] } })).toEqual([]);
|
|
992
|
-
});
|
|
993
|
-
it("unique - should handle array with no duplicates", () => {
|
|
994
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: [1, 2, 3] } })).toEqual([1, 2, 3]);
|
|
995
|
-
});
|
|
996
|
-
it("unique - should return [] for non-array input", () => {
|
|
997
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: null } })).toEqual([]);
|
|
998
|
-
expect(evaluate({ kind: "unique", array: { kind: "lit", value: "string" } })).toEqual([]);
|
|
999
|
-
});
|
|
1000
|
-
// --- flat ---
|
|
1001
|
-
it("flat - MUST flatten exactly one level", () => {
|
|
1002
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: [[1, 2], [3, 4]] } })).toEqual([1, 2, 3, 4]);
|
|
1003
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: [[1, 2], [3], [4, 5, 6]] } })).toEqual([1, 2, 3, 4, 5, 6]);
|
|
1004
|
-
});
|
|
1005
|
-
it("flat - should NOT flatten deeper than one level", () => {
|
|
1006
|
-
// [[1, [2]], [3]] -> [1, [2], 3] (only one level flattened)
|
|
1007
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: [[1, [2]], [3]] } })).toEqual([1, [2], 3]);
|
|
1008
|
-
});
|
|
1009
|
-
it("flat - should handle mixed nested and non-nested elements", () => {
|
|
1010
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: [1, [2, 3], 4] } })).toEqual([1, 2, 3, 4]);
|
|
1011
|
-
});
|
|
1012
|
-
it("flat - should handle empty array", () => {
|
|
1013
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: [] } })).toEqual([]);
|
|
1014
|
-
});
|
|
1015
|
-
it("flat - should handle array of empty arrays", () => {
|
|
1016
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: [[], [], []] } })).toEqual([]);
|
|
1017
|
-
});
|
|
1018
|
-
it("flat - should return [] for non-array input", () => {
|
|
1019
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: null } })).toEqual([]);
|
|
1020
|
-
expect(evaluate({ kind: "flat", array: { kind: "lit", value: 42 } })).toEqual([]);
|
|
1021
|
-
});
|
|
1022
|
-
});
|
|
1023
|
-
// ============================================================
|
|
1024
|
-
// SPEC v2.0.3 §1.3 — Object Extensions
|
|
1025
|
-
// ============================================================
|
|
1026
|
-
describe("Object Extensions (SPEC v2.0.3)", () => {
|
|
1027
|
-
// --- hasKey ---
|
|
1028
|
-
it("hasKey - should check if key exists in object", () => {
|
|
1029
|
-
expect(evaluate({
|
|
1030
|
-
kind: "hasKey",
|
|
1031
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1032
|
-
key: { kind: "lit", value: "a" },
|
|
1033
|
-
})).toBe(true);
|
|
1034
|
-
expect(evaluate({
|
|
1035
|
-
kind: "hasKey",
|
|
1036
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1037
|
-
key: { kind: "lit", value: "c" },
|
|
1038
|
-
})).toBe(false);
|
|
1039
|
-
});
|
|
1040
|
-
it("hasKey - should detect keys with null/undefined values", () => {
|
|
1041
|
-
expect(evaluate({
|
|
1042
|
-
kind: "hasKey",
|
|
1043
|
-
obj: { kind: "lit", value: { a: null, b: undefined } },
|
|
1044
|
-
key: { kind: "lit", value: "a" },
|
|
1045
|
-
})).toBe(true);
|
|
1046
|
-
});
|
|
1047
|
-
it("hasKey - should return false for non-object", () => {
|
|
1048
|
-
expect(evaluate({
|
|
1049
|
-
kind: "hasKey",
|
|
1050
|
-
obj: { kind: "lit", value: null },
|
|
1051
|
-
key: { kind: "lit", value: "a" },
|
|
1052
|
-
})).toBe(false);
|
|
1053
|
-
expect(evaluate({
|
|
1054
|
-
kind: "hasKey",
|
|
1055
|
-
obj: { kind: "lit", value: [1, 2, 3] },
|
|
1056
|
-
key: { kind: "lit", value: "0" },
|
|
1057
|
-
})).toBe(false);
|
|
1058
|
-
expect(evaluate({
|
|
1059
|
-
kind: "hasKey",
|
|
1060
|
-
obj: { kind: "lit", value: 42 },
|
|
1061
|
-
key: { kind: "lit", value: "a" },
|
|
1062
|
-
})).toBe(false);
|
|
1063
|
-
});
|
|
1064
|
-
// --- pick ---
|
|
1065
|
-
it("pick - should select only listed keys", () => {
|
|
1066
|
-
expect(evaluate({
|
|
1067
|
-
kind: "pick",
|
|
1068
|
-
obj: { kind: "lit", value: { a: 1, b: 2, c: 3 } },
|
|
1069
|
-
keys: { kind: "lit", value: ["a", "c"] },
|
|
1070
|
-
})).toEqual({ a: 1, c: 3 });
|
|
1071
|
-
});
|
|
1072
|
-
it("pick - should skip keys that do not exist", () => {
|
|
1073
|
-
expect(evaluate({
|
|
1074
|
-
kind: "pick",
|
|
1075
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1076
|
-
keys: { kind: "lit", value: ["a", "x", "y"] },
|
|
1077
|
-
})).toEqual({ a: 1 });
|
|
1078
|
-
});
|
|
1079
|
-
it("pick - MUST ignore non-string keys in keys array", () => {
|
|
1080
|
-
expect(evaluate({
|
|
1081
|
-
kind: "pick",
|
|
1082
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1083
|
-
keys: { kind: "lit", value: ["a", 42, null, "b"] },
|
|
1084
|
-
})).toEqual({ a: 1, b: 2 });
|
|
1085
|
-
});
|
|
1086
|
-
it("pick - should return {} for non-object", () => {
|
|
1087
|
-
expect(evaluate({
|
|
1088
|
-
kind: "pick",
|
|
1089
|
-
obj: { kind: "lit", value: null },
|
|
1090
|
-
keys: { kind: "lit", value: ["a"] },
|
|
1091
|
-
})).toEqual({});
|
|
1092
|
-
});
|
|
1093
|
-
it("pick - should treat non-array keys as empty array", () => {
|
|
1094
|
-
expect(evaluate({
|
|
1095
|
-
kind: "pick",
|
|
1096
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1097
|
-
keys: { kind: "lit", value: "not-array" },
|
|
1098
|
-
})).toEqual({});
|
|
1099
|
-
});
|
|
1100
|
-
// --- omit ---
|
|
1101
|
-
it("omit - should exclude listed keys", () => {
|
|
1102
|
-
expect(evaluate({
|
|
1103
|
-
kind: "omit",
|
|
1104
|
-
obj: { kind: "lit", value: { a: 1, b: 2, c: 3 } },
|
|
1105
|
-
keys: { kind: "lit", value: ["b"] },
|
|
1106
|
-
})).toEqual({ a: 1, c: 3 });
|
|
1107
|
-
});
|
|
1108
|
-
it("omit - should handle keys that do not exist", () => {
|
|
1109
|
-
expect(evaluate({
|
|
1110
|
-
kind: "omit",
|
|
1111
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1112
|
-
keys: { kind: "lit", value: ["x", "y"] },
|
|
1113
|
-
})).toEqual({ a: 1, b: 2 });
|
|
1114
|
-
});
|
|
1115
|
-
it("omit - MUST ignore non-string keys in keys array", () => {
|
|
1116
|
-
expect(evaluate({
|
|
1117
|
-
kind: "omit",
|
|
1118
|
-
obj: { kind: "lit", value: { a: 1, b: 2, c: 3 } },
|
|
1119
|
-
keys: { kind: "lit", value: ["b", 42, null] },
|
|
1120
|
-
})).toEqual({ a: 1, c: 3 });
|
|
1121
|
-
});
|
|
1122
|
-
it("omit - should return {} for non-object", () => {
|
|
1123
|
-
expect(evaluate({
|
|
1124
|
-
kind: "omit",
|
|
1125
|
-
obj: { kind: "lit", value: null },
|
|
1126
|
-
keys: { kind: "lit", value: ["a"] },
|
|
1127
|
-
})).toEqual({});
|
|
1128
|
-
});
|
|
1129
|
-
it("omit - should treat non-array keys as empty array (return all keys)", () => {
|
|
1130
|
-
expect(evaluate({
|
|
1131
|
-
kind: "omit",
|
|
1132
|
-
obj: { kind: "lit", value: { a: 1, b: 2 } },
|
|
1133
|
-
keys: { kind: "lit", value: "not-array" },
|
|
1134
|
-
})).toEqual({ a: 1, b: 2 });
|
|
1135
|
-
});
|
|
1136
|
-
// --- fromEntries ---
|
|
1137
|
-
it("fromEntries - should convert entries array to object", () => {
|
|
1138
|
-
expect(evaluate({
|
|
1139
|
-
kind: "fromEntries",
|
|
1140
|
-
entries: { kind: "lit", value: [["a", 1], ["b", 2], ["c", 3]] },
|
|
1141
|
-
})).toEqual({ a: 1, b: 2, c: 3 });
|
|
1142
|
-
});
|
|
1143
|
-
it("fromEntries - MUST skip entries that are not 2-element arrays", () => {
|
|
1144
|
-
expect(evaluate({
|
|
1145
|
-
kind: "fromEntries",
|
|
1146
|
-
entries: { kind: "lit", value: [["a", 1], "invalid", [42], ["b", 2]] },
|
|
1147
|
-
})).toEqual({ a: 1, b: 2 });
|
|
1148
|
-
});
|
|
1149
|
-
it("fromEntries - should handle empty array", () => {
|
|
1150
|
-
expect(evaluate({
|
|
1151
|
-
kind: "fromEntries",
|
|
1152
|
-
entries: { kind: "lit", value: [] },
|
|
1153
|
-
})).toEqual({});
|
|
1154
|
-
});
|
|
1155
|
-
it("fromEntries - should return {} for non-array input", () => {
|
|
1156
|
-
expect(evaluate({
|
|
1157
|
-
kind: "fromEntries",
|
|
1158
|
-
entries: { kind: "lit", value: null },
|
|
1159
|
-
})).toEqual({});
|
|
1160
|
-
expect(evaluate({
|
|
1161
|
-
kind: "fromEntries",
|
|
1162
|
-
entries: { kind: "lit", value: "not array" },
|
|
1163
|
-
})).toEqual({});
|
|
1164
|
-
});
|
|
1165
|
-
it("fromEntries - last entry wins for duplicate keys", () => {
|
|
1166
|
-
expect(evaluate({
|
|
1167
|
-
kind: "fromEntries",
|
|
1168
|
-
entries: { kind: "lit", value: [["a", 1], ["a", 2]] },
|
|
1169
|
-
})).toEqual({ a: 2 });
|
|
1170
|
-
});
|
|
1171
|
-
});
|
|
1172
|
-
// ============================================================
|
|
1173
|
-
// Determinism Compliance (§7.5 Requirements)
|
|
1174
|
-
// ============================================================
|
|
1175
|
-
describe("Determinism Compliance", () => {
|
|
1176
|
-
it("replace - determinism: same input produces same output", () => {
|
|
1177
|
-
const expr = {
|
|
1178
|
-
kind: "replace",
|
|
1179
|
-
str: { kind: "lit", value: "aaa bbb aaa" },
|
|
1180
|
-
search: { kind: "lit", value: "aaa" },
|
|
1181
|
-
replacement: { kind: "lit", value: "ccc" },
|
|
1182
|
-
};
|
|
1183
|
-
const result1 = evaluate(expr);
|
|
1184
|
-
const result2 = evaluate(expr);
|
|
1185
|
-
expect(result1).toBe(result2);
|
|
1186
|
-
});
|
|
1187
|
-
it("unique - determinism: same input produces same output with same order", () => {
|
|
1188
|
-
const expr = {
|
|
1189
|
-
kind: "unique",
|
|
1190
|
-
array: { kind: "lit", value: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5] },
|
|
1191
|
-
};
|
|
1192
|
-
const result1 = evaluate(expr);
|
|
1193
|
-
const result2 = evaluate(expr);
|
|
1194
|
-
expect(result1).toEqual(result2);
|
|
1195
|
-
expect(result1).toEqual([3, 1, 4, 5, 9, 2, 6]);
|
|
1196
|
-
});
|
|
1197
|
-
it("all new kinds - totality: always return a value, never throw", () => {
|
|
1198
|
-
// All new kinds must be total - test with null/undefined/wrong-type inputs
|
|
1199
|
-
const totalityTests = [
|
|
1200
|
-
{ kind: "floor", arg: { kind: "lit", value: null } },
|
|
1201
|
-
{ kind: "ceil", arg: { kind: "lit", value: undefined } },
|
|
1202
|
-
{ kind: "round", arg: { kind: "lit", value: "not a number" } },
|
|
1203
|
-
{ kind: "sqrt", arg: { kind: "lit", value: null } },
|
|
1204
|
-
{ kind: "sumArray", array: { kind: "lit", value: null } },
|
|
1205
|
-
{ kind: "minArray", array: { kind: "lit", value: null } },
|
|
1206
|
-
{ kind: "maxArray", array: { kind: "lit", value: null } },
|
|
1207
|
-
{ kind: "toLowerCase", str: { kind: "lit", value: null } },
|
|
1208
|
-
{ kind: "toUpperCase", str: { kind: "lit", value: null } },
|
|
1209
|
-
{ kind: "strLen", str: { kind: "lit", value: null } },
|
|
1210
|
-
{ kind: "toString", arg: { kind: "lit", value: null } },
|
|
1211
|
-
{ kind: "toNumber", arg: { kind: "lit", value: null } },
|
|
1212
|
-
{ kind: "toBoolean", arg: { kind: "lit", value: null } },
|
|
1213
|
-
{ kind: "startsWith", str: { kind: "lit", value: null }, prefix: { kind: "lit", value: null } },
|
|
1214
|
-
{ kind: "endsWith", str: { kind: "lit", value: null }, suffix: { kind: "lit", value: null } },
|
|
1215
|
-
{ kind: "strIncludes", str: { kind: "lit", value: null }, search: { kind: "lit", value: null } },
|
|
1216
|
-
{ kind: "indexOf", str: { kind: "lit", value: null }, search: { kind: "lit", value: null } },
|
|
1217
|
-
{ kind: "replace", str: { kind: "lit", value: null }, search: { kind: "lit", value: null }, replacement: { kind: "lit", value: null } },
|
|
1218
|
-
{ kind: "split", str: { kind: "lit", value: null }, delimiter: { kind: "lit", value: null } },
|
|
1219
|
-
{ kind: "reverse", array: { kind: "lit", value: null } },
|
|
1220
|
-
{ kind: "unique", array: { kind: "lit", value: null } },
|
|
1221
|
-
{ kind: "flat", array: { kind: "lit", value: null } },
|
|
1222
|
-
{ kind: "hasKey", obj: { kind: "lit", value: null }, key: { kind: "lit", value: "a" } },
|
|
1223
|
-
{ kind: "pick", obj: { kind: "lit", value: null }, keys: { kind: "lit", value: ["a"] } },
|
|
1224
|
-
{ kind: "omit", obj: { kind: "lit", value: null }, keys: { kind: "lit", value: ["a"] } },
|
|
1225
|
-
{ kind: "fromEntries", entries: { kind: "lit", value: null } },
|
|
1226
|
-
{ kind: "pow", base: { kind: "lit", value: null }, exponent: { kind: "lit", value: null } },
|
|
1227
|
-
];
|
|
1228
|
-
for (const expr of totalityTests) {
|
|
1229
|
-
// Should not throw — all expressions must be total
|
|
1230
|
-
expect(() => evaluate(expr)).not.toThrow();
|
|
1231
|
-
}
|
|
1232
|
-
});
|
|
1233
|
-
});
|
|
1234
|
-
});
|
|
1235
|
-
//# sourceMappingURL=expr.test.js.map
|