From db77c4b557a59856db2c6943c5a2a62fce22ca85 Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Tue, 18 Nov 2025 10:02:33 +0000 Subject: [PATCH 1/2] Add $length operator to ConditionalRouter for array length comparisons --- src/services/conditionalRouter.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/services/conditionalRouter.ts b/src/services/conditionalRouter.ts index ee272f363..a2e5837cb 100644 --- a/src/services/conditionalRouter.ts +++ b/src/services/conditionalRouter.ts @@ -23,6 +23,7 @@ enum Operator { In = '$in', NotIn = '$nin', Regex = '$regex', + Length = '$length', // Logical Operators And = '$and', @@ -125,6 +126,20 @@ export class ConditionalRouter { } catch (e) { return false; } + case Operator.Length: + if (!Array.isArray(value)) return false; + // compareValue could be a number or an object like {$gt: 5} + if (typeof compareValue === 'number') { + if (value.length !== compareValue) return false; + } else if ( + typeof compareValue === 'object' && + compareValue !== null + ) { + // Recursively evaluate the length with other operators + if (!this.evaluateOperator(compareValue, value.length)) + return false; + } + break; default: throw new Error( `Unsupported operator used in the query router: ${op}` From 293886810460169faf635a0dbcfa845d61d6e08c Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Tue, 18 Nov 2025 13:00:13 +0000 Subject: [PATCH 2/2] Add comprehensive tests for $length operator in ConditionalRouter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test basic equality matching with $length - Test nested operators ($gt, $lt, $gte, $lte, $eq, $ne) - Test edge cases (empty arrays, non-arrays, metadata arrays) - Test complex conditional queries with $and - Add regression tests for existing operators - All 16 tests passing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/services/conditionalRouter.test.ts | 512 ++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 tests/unit/src/services/conditionalRouter.test.ts diff --git a/tests/unit/src/services/conditionalRouter.test.ts b/tests/unit/src/services/conditionalRouter.test.ts new file mode 100644 index 000000000..94d3e4be9 --- /dev/null +++ b/tests/unit/src/services/conditionalRouter.test.ts @@ -0,0 +1,512 @@ +import { ConditionalRouter } from '../../../../src/services/conditionalRouter'; +import { StrategyModes, Targets } from '../../../../src/types/requestBody'; + +interface RouterContext { + metadata?: Record; + params?: Record; + url?: { + pathname: string; + }; +} + +describe('ConditionalRouter', () => { + describe('$length operator', () => { + it('should match when array length equals the specified number', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: 3 } }, + then: 'target-1', + }, + ], + }, + targets: [{ name: 'target-1', virtualKey: 'vk1' }], + }; + + const context = { + params: { + messages: ['msg1', 'msg2', 'msg3'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-1'); + expect(result.index).toBe(0); + }); + + it('should not match when array length does not equal the specified number', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: 3 } }, + then: 'target-1', + }, + ], + default: 'target-2', + }, + targets: [ + { name: 'target-1', virtualKey: 'vk1' }, + { name: 'target-2', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-2'); + }); + + it('should return false when value is not an array', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: 3 } }, + then: 'target-1', + }, + ], + default: 'target-2', + }, + targets: [ + { name: 'target-1', virtualKey: 'vk1' }, + { name: 'target-2', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: 'not-an-array', + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-2'); + }); + + it('should work with nested $gt operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $gt: 5 } } }, + then: 'target-long-conversation', + }, + ], + default: 'target-short-conversation', + }, + targets: [ + { name: 'target-long-conversation', virtualKey: 'vk1' }, + { name: 'target-short-conversation', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2', 'msg3', 'msg4', 'msg5', 'msg6'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-long-conversation'); + }); + + it('should work with nested $lt operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $lt: 3 } } }, + then: 'target-short', + }, + ], + default: 'target-long', + }, + targets: [ + { name: 'target-short', virtualKey: 'vk1' }, + { name: 'target-long', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-short'); + }); + + it('should work with nested $gte operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $gte: 3 } } }, + then: 'target-1', + }, + ], + default: 'target-2', + }, + targets: [ + { name: 'target-1', virtualKey: 'vk1' }, + { name: 'target-2', virtualKey: 'vk2' }, + ], + }; + + const contextEqual = { + params: { + messages: ['msg1', 'msg2', 'msg3'], + }, + }; + + const contextGreater = { + params: { + messages: ['msg1', 'msg2', 'msg3', 'msg4'], + }, + }; + + const routerEqual = new ConditionalRouter(config, contextEqual); + expect(routerEqual.resolveTarget().name).toBe('target-1'); + + const routerGreater = new ConditionalRouter(config, contextGreater); + expect(routerGreater.resolveTarget().name).toBe('target-1'); + }); + + it('should work with nested $lte operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $lte: 3 } } }, + then: 'target-1', + }, + ], + default: 'target-2', + }, + targets: [ + { name: 'target-1', virtualKey: 'vk1' }, + { name: 'target-2', virtualKey: 'vk2' }, + ], + }; + + const contextEqual = { + params: { + messages: ['msg1', 'msg2', 'msg3'], + }, + }; + + const contextLess = { + params: { + messages: ['msg1', 'msg2'], + }, + }; + + const routerEqual = new ConditionalRouter(config, contextEqual); + expect(routerEqual.resolveTarget().name).toBe('target-1'); + + const routerLess = new ConditionalRouter(config, contextLess); + expect(routerLess.resolveTarget().name).toBe('target-1'); + }); + + it('should work with nested $eq operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $eq: 3 } } }, + then: 'target-1', + }, + ], + default: 'target-2', + }, + targets: [ + { name: 'target-1', virtualKey: 'vk1' }, + { name: 'target-2', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2', 'msg3'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-1'); + }); + + it('should work with nested $ne operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $ne: 3 } } }, + then: 'target-1', + }, + ], + default: 'target-2', + }, + targets: [ + { name: 'target-1', virtualKey: 'vk1' }, + { name: 'target-2', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-1'); + }); + + it('should work with empty arrays', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: 0 } }, + then: 'target-empty', + }, + ], + default: 'target-not-empty', + }, + targets: [ + { name: 'target-empty', virtualKey: 'vk1' }, + { name: 'target-not-empty', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: [], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-empty'); + }); + + it('should work with $length in complex conditional queries', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { + $and: [ + { 'params.messages': { $length: { $gt: 2 } } }, + { 'metadata.user': { $eq: 'premium' } }, + ], + }, + then: 'target-premium-long', + }, + { + query: { + 'params.messages': { $length: { $gt: 2 } }, + }, + then: 'target-long', + }, + ], + default: 'target-default', + }, + targets: [ + { name: 'target-premium-long', virtualKey: 'vk1' }, + { name: 'target-long', virtualKey: 'vk2' }, + { name: 'target-default', virtualKey: 'vk3' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2', 'msg3', 'msg4'], + }, + metadata: { + user: 'premium', + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-premium-long'); + }); + + it('should fall back to default when $length condition does not match', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'params.messages': { $length: { $gt: 10 } } }, + then: 'target-very-long', + }, + ], + default: 'target-default', + }, + targets: [ + { name: 'target-very-long', virtualKey: 'vk1' }, + { name: 'target-default', virtualKey: 'vk2' }, + ], + }; + + const context = { + params: { + messages: ['msg1', 'msg2', 'msg3'], + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-default'); + }); + + it('should work with metadata arrays', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'metadata.tags': { $length: 2 } }, + then: 'target-two-tags', + }, + ], + default: 'target-other', + }, + targets: [ + { name: 'target-two-tags', virtualKey: 'vk1' }, + { name: 'target-other', virtualKey: 'vk2' }, + ], + }; + + const context: RouterContext = { + metadata: { + tags: ['tag1', 'tag2'], + }, + }; + + const router = new ConditionalRouter(config, context as any); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-two-tags'); + }); + }); + + describe('other operators (existing functionality)', () => { + it('should work with $eq operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'metadata.user': { $eq: 'test-user' } }, + then: 'target-1', + }, + ], + }, + targets: [{ name: 'target-1', virtualKey: 'vk1' }], + }; + + const context = { + metadata: { + user: 'test-user', + }, + }; + + const router = new ConditionalRouter(config, context); + const result = router.resolveTarget(); + + expect(result.name).toBe('target-1'); + }); + + it('should throw error for unsupported operator', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'metadata.user': { $unsupported: 'value' } }, + then: 'target-1', + }, + ], + }, + targets: [{ name: 'target-1', virtualKey: 'vk1' }], + }; + + const context = { + metadata: { + user: 'test-user', + }, + }; + + const router = new ConditionalRouter(config, context); + + expect(() => router.resolveTarget()).toThrow( + 'Unsupported operator used in the query router: $unsupported' + ); + }); + + it('should throw error when no conditions matched and no default', () => { + const config: Targets = { + strategy: { + mode: StrategyModes.CONDITIONAL, + conditions: [ + { + query: { 'metadata.user': { $eq: 'other-user' } }, + then: 'target-1', + }, + ], + }, + targets: [{ name: 'target-1', virtualKey: 'vk1' }], + }; + + const context = { + metadata: { + user: 'test-user', + }, + }; + + const router = new ConditionalRouter(config, context); + + expect(() => router.resolveTarget()).toThrow( + 'Query router did not resolve to any valid target' + ); + }); + }); +});