Skip to content

Commit 826930f

Browse files
added tests
1 parent c4cd3d1 commit 826930f

1 file changed

Lines changed: 321 additions & 0 deletions

File tree

test/engine-evaluate.test.js

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
'use strict'
2+
3+
import Engine from '../src/engine'
4+
import OperatorMap from '../src/operator-map'
5+
6+
describe('Engine.evaluate() - static lightweight evaluation', () => {
7+
describe('basic condition evaluation', () => {
8+
it('evaluates a simple "all" condition as true', async () => {
9+
const conditions = {
10+
all: [
11+
{ fact: 'age', operator: 'greaterThanInclusive', value: 18 },
12+
{ fact: 'status', operator: 'equal', value: 'active' }
13+
]
14+
}
15+
const facts = { age: 25, status: 'active' }
16+
const result = await Engine.evaluate(conditions, facts)
17+
expect(result.results).to.have.lengthOf(1)
18+
expect(result.results[0].result).to.equal(true)
19+
expect(result.failureResults).to.have.lengthOf(0)
20+
expect(result.events).to.have.lengthOf(1)
21+
expect(result.failureEvents).to.have.lengthOf(0)
22+
})
23+
24+
it('evaluates a simple "all" condition as false', async () => {
25+
const conditions = {
26+
all: [
27+
{ fact: 'age', operator: 'greaterThanInclusive', value: 18 },
28+
{ fact: 'status', operator: 'equal', value: 'active' }
29+
]
30+
}
31+
const facts = { age: 16, status: 'active' }
32+
const result = await Engine.evaluate(conditions, facts)
33+
expect(result.results).to.have.lengthOf(0)
34+
expect(result.failureResults).to.have.lengthOf(1)
35+
expect(result.failureResults[0].result).to.equal(false)
36+
expect(result.events).to.have.lengthOf(0)
37+
expect(result.failureEvents).to.have.lengthOf(1)
38+
})
39+
40+
it('evaluates a simple "any" condition as true', async () => {
41+
const conditions = {
42+
any: [
43+
{ fact: 'role', operator: 'equal', value: 'admin' },
44+
{ fact: 'role', operator: 'equal', value: 'moderator' }
45+
]
46+
}
47+
const facts = { role: 'moderator' }
48+
const result = await Engine.evaluate(conditions, facts)
49+
expect(result.results).to.have.lengthOf(1)
50+
expect(result.results[0].result).to.equal(true)
51+
})
52+
53+
it('evaluates a simple "any" condition as false', async () => {
54+
const conditions = {
55+
any: [
56+
{ fact: 'role', operator: 'equal', value: 'admin' },
57+
{ fact: 'role', operator: 'equal', value: 'moderator' }
58+
]
59+
}
60+
const facts = { role: 'user' }
61+
const result = await Engine.evaluate(conditions, facts)
62+
expect(result.failureResults).to.have.lengthOf(1)
63+
expect(result.failureResults[0].result).to.equal(false)
64+
})
65+
66+
it('evaluates a "not" condition as true', async () => {
67+
const conditions = {
68+
not: { fact: 'banned', operator: 'equal', value: true }
69+
}
70+
const facts = { banned: false }
71+
const result = await Engine.evaluate(conditions, facts)
72+
expect(result.results).to.have.lengthOf(1)
73+
expect(result.results[0].result).to.equal(true)
74+
})
75+
76+
it('evaluates a "not" condition as false', async () => {
77+
const conditions = {
78+
not: { fact: 'banned', operator: 'equal', value: true }
79+
}
80+
const facts = { banned: true }
81+
const result = await Engine.evaluate(conditions, facts)
82+
expect(result.failureResults).to.have.lengthOf(1)
83+
})
84+
})
85+
86+
describe('nested conditions', () => {
87+
it('evaluates nested all/any conditions', async () => {
88+
const conditions = {
89+
all: [
90+
{ fact: 'age', operator: 'greaterThanInclusive', value: 18 },
91+
{
92+
any: [
93+
{ fact: 'role', operator: 'equal', value: 'admin' },
94+
{ fact: 'verified', operator: 'equal', value: true }
95+
]
96+
}
97+
]
98+
}
99+
const facts = { age: 25, role: 'user', verified: true }
100+
const result = await Engine.evaluate(conditions, facts)
101+
expect(result.results).to.have.lengthOf(1)
102+
expect(result.results[0].result).to.equal(true)
103+
})
104+
105+
it('evaluates deeply nested conditions', async () => {
106+
const conditions = {
107+
any: [
108+
{
109+
all: [
110+
{ fact: 'tier', operator: 'equal', value: 'premium' },
111+
{ fact: 'balance', operator: 'greaterThan', value: 100 }
112+
]
113+
},
114+
{
115+
all: [
116+
{ fact: 'tier', operator: 'equal', value: 'basic' },
117+
{ fact: 'balance', operator: 'greaterThan', value: 1000 }
118+
]
119+
}
120+
]
121+
}
122+
const facts = { tier: 'basic', balance: 1500 }
123+
const result = await Engine.evaluate(conditions, facts)
124+
expect(result.results).to.have.lengthOf(1)
125+
expect(result.results[0].result).to.equal(true)
126+
})
127+
})
128+
129+
describe('all default operators', () => {
130+
it('supports equal', async () => {
131+
const result = await Engine.evaluate(
132+
{ all: [{ fact: 'x', operator: 'equal', value: 5 }] },
133+
{ x: 5 }
134+
)
135+
expect(result.results).to.have.lengthOf(1)
136+
})
137+
138+
it('supports notEqual', async () => {
139+
const result = await Engine.evaluate(
140+
{ all: [{ fact: 'x', operator: 'notEqual', value: 5 }] },
141+
{ x: 3 }
142+
)
143+
expect(result.results).to.have.lengthOf(1)
144+
})
145+
146+
it('supports in', async () => {
147+
const result = await Engine.evaluate(
148+
{ all: [{ fact: 'x', operator: 'in', value: [1, 2, 3] }] },
149+
{ x: 2 }
150+
)
151+
expect(result.results).to.have.lengthOf(1)
152+
})
153+
154+
it('supports notIn', async () => {
155+
const result = await Engine.evaluate(
156+
{ all: [{ fact: 'x', operator: 'notIn', value: [1, 2, 3] }] },
157+
{ x: 5 }
158+
)
159+
expect(result.results).to.have.lengthOf(1)
160+
})
161+
162+
it('supports contains', async () => {
163+
const result = await Engine.evaluate(
164+
{ all: [{ fact: 'x', operator: 'contains', value: 3 }] },
165+
{ x: [1, 2, 3] }
166+
)
167+
expect(result.results).to.have.lengthOf(1)
168+
})
169+
170+
it('supports doesNotContain', async () => {
171+
const result = await Engine.evaluate(
172+
{ all: [{ fact: 'x', operator: 'doesNotContain', value: 5 }] },
173+
{ x: [1, 2, 3] }
174+
)
175+
expect(result.results).to.have.lengthOf(1)
176+
})
177+
178+
it('supports lessThan', async () => {
179+
const result = await Engine.evaluate(
180+
{ all: [{ fact: 'x', operator: 'lessThan', value: 10 }] },
181+
{ x: 5 }
182+
)
183+
expect(result.results).to.have.lengthOf(1)
184+
})
185+
186+
it('supports greaterThan', async () => {
187+
const result = await Engine.evaluate(
188+
{ all: [{ fact: 'x', operator: 'greaterThan', value: 10 }] },
189+
{ x: 15 }
190+
)
191+
expect(result.results).to.have.lengthOf(1)
192+
})
193+
194+
it('supports lessThanInclusive', async () => {
195+
const result = await Engine.evaluate(
196+
{ all: [{ fact: 'x', operator: 'lessThanInclusive', value: 10 }] },
197+
{ x: 10 }
198+
)
199+
expect(result.results).to.have.lengthOf(1)
200+
})
201+
202+
it('supports greaterThanInclusive', async () => {
203+
const result = await Engine.evaluate(
204+
{ all: [{ fact: 'x', operator: 'greaterThanInclusive', value: 10 }] },
205+
{ x: 10 }
206+
)
207+
expect(result.results).to.have.lengthOf(1)
208+
})
209+
})
210+
211+
describe('options', () => {
212+
it('supports allowUndefinedFacts', async () => {
213+
const conditions = {
214+
all: [{ fact: 'missing', operator: 'equal', value: undefined }]
215+
}
216+
const result = await Engine.evaluate(conditions, {}, { allowUndefinedFacts: true })
217+
expect(result.results).to.have.lengthOf(1)
218+
})
219+
220+
it('rejects undefined facts by default', async () => {
221+
const conditions = {
222+
all: [{ fact: 'missing', operator: 'equal', value: 5 }]
223+
}
224+
try {
225+
await Engine.evaluate(conditions, {})
226+
expect.fail('should have thrown')
227+
} catch (e) {
228+
expect(e.message).to.include('Undefined fact')
229+
}
230+
})
231+
232+
it('supports custom operatorMap', async () => {
233+
const customMap = new OperatorMap({ parent: OperatorMap.shared() })
234+
customMap.addOperator('startsWith', (a, b) => a.startsWith(b))
235+
const conditions = {
236+
all: [{ fact: 'name', operator: 'startsWith', value: 'Jo' }]
237+
}
238+
const result = await Engine.evaluate(conditions, { name: 'John' }, { operatorMap: customMap })
239+
expect(result.results).to.have.lengthOf(1)
240+
})
241+
})
242+
243+
describe('return value structure', () => {
244+
it('returns results, failureResults, events, and failureEvents', async () => {
245+
const conditions = {
246+
all: [{ fact: 'x', operator: 'equal', value: 1 }]
247+
}
248+
const result = await Engine.evaluate(conditions, { x: 1 })
249+
expect(result).to.have.property('results')
250+
expect(result).to.have.property('failureResults')
251+
expect(result).to.have.property('events')
252+
expect(result).to.have.property('failureEvents')
253+
})
254+
255+
it('returns event with type "evaluate"', async () => {
256+
const result = await Engine.evaluate(
257+
{ all: [{ fact: 'x', operator: 'equal', value: 1 }] },
258+
{ x: 1 }
259+
)
260+
expect(result.events[0].type).to.equal('evaluate')
261+
})
262+
})
263+
264+
describe('shared operator registry', () => {
265+
it('OperatorMap.shared() returns the same instance', () => {
266+
const a = OperatorMap.shared()
267+
const b = OperatorMap.shared()
268+
expect(a).to.equal(b)
269+
})
270+
271+
it('shared instance contains all default operators', () => {
272+
const shared = OperatorMap.shared()
273+
expect(shared.get('equal')).to.not.be.null()
274+
expect(shared.get('notEqual')).to.not.be.null()
275+
expect(shared.get('greaterThan')).to.not.be.null()
276+
expect(shared.get('lessThan')).to.not.be.null()
277+
expect(shared.get('in')).to.not.be.null()
278+
expect(shared.get('notIn')).to.not.be.null()
279+
expect(shared.get('contains')).to.not.be.null()
280+
expect(shared.get('doesNotContain')).to.not.be.null()
281+
})
282+
283+
it('shared instance contains all default decorators', () => {
284+
const shared = OperatorMap.shared()
285+
expect(shared.get('someFact:equal')).to.not.be.null()
286+
expect(shared.get('everyFact:equal')).to.not.be.null()
287+
expect(shared.get('not:equal')).to.not.be.null()
288+
expect(shared.get('swap:equal')).to.not.be.null()
289+
})
290+
291+
it('Engine instances inherit shared operators without cloning', () => {
292+
const engine = new Engine()
293+
// Engine operators should have the shared instance as parent
294+
expect(engine.operators.parent).to.equal(OperatorMap.shared())
295+
// Engine can still resolve default operators through the parent chain
296+
expect(engine.operators.get('equal')).to.not.be.null()
297+
expect(engine.operators.get('greaterThan')).to.not.be.null()
298+
})
299+
300+
it('Engine instances can add custom operators without affecting shared', () => {
301+
const engine = new Engine()
302+
engine.addOperator('custom', (a, b) => a === b)
303+
expect(engine.operators.get('custom')).to.not.be.null()
304+
// shared should not have the custom operator
305+
const shared = OperatorMap.shared()
306+
expect(shared.get('custom')).to.be.null()
307+
})
308+
})
309+
310+
describe('empty condition arrays', () => {
311+
it('evaluates empty "all" as true', async () => {
312+
const result = await Engine.evaluate({ all: [] }, {})
313+
expect(result.results).to.have.lengthOf(1)
314+
})
315+
316+
it('evaluates empty "any" as false', async () => {
317+
const result = await Engine.evaluate({ any: [] }, {})
318+
expect(result.failureResults).to.have.lengthOf(1)
319+
})
320+
})
321+
})

0 commit comments

Comments
 (0)