import { test } from 'node:test' import assert from 'node:assert' import * as fc from 'fast-check' import { parse, validateFormula } from '../formula/parser.js' import { evaluate, evaluateAsync } from '../formula/evaluator.js' import { substitute } from '../formula/substitutor.js' // ============================================================================ // Helpers // ============================================================================ function makeContext(overrides: Partial = {}): EvalContext { return { request: { body: null, headers: {}, query: {}, params: {}, cookies: {}, ...overrides.request }, response: { body: null, headers: {}, statusCode: 200, responseTime: 0, ...overrides.response }, previous: overrides.previous } as EvalContext } function evalFormula(formula: string, ctx: EvalContext = makeContext()): unknown { const ast = parse(formula) const result = evaluate(ast.ast, ctx) if (!result.success) throw new Error(result.error) return result.value } async function evalFormulaAsync(formula: string, ctx: EvalContext = makeContext()): Promise { const ast = parse(formula) const result = await evaluateAsync(ast.ast, ctx) if (!result.success) throw new Error(result.error) return result.value } // ============================================================================ // Unit Tests: Parser // ============================================================================ test('parse: literal true', () => { const result = parse('true') assert.deepStrictEqual(result.ast, { type: 'literal', value: true }) }) test('parse: literal false', () => { const result = parse('false') assert.deepStrictEqual(result.ast, { type: 'literal', value: false }) }) test('parse: literal null', () => { const result = parse('null') assert.deepStrictEqual(result.ast, { type: 'literal', value: null }) }) test('parse: literal string with single quotes', () => { const result = parse("'hello world'") assert.deepStrictEqual(result.ast, { type: 'literal', value: 'hello world' }) }) test('parse: literal string with double quotes', () => { const result = parse('"hello world"') assert.deepStrictEqual(result.ast, { type: 'literal', value: 'hello world' }) }) test('parse: literal integer', () => { const result = parse('42') assert.deepStrictEqual(result.ast, { type: 'literal', value: 42 }) }) test('parse: literal negative number', () => { const result = parse('-3.14') assert.deepStrictEqual(result.ast, { type: 'literal', value: -3.14 }) }) test('parse: operation response_body(this)', () => { const result = parse('response_body(this)') assert.deepStrictEqual(result.ast, { type: 'operation', header: 'response_body', parameter: { type: 'this' }, accessor: undefined }) }) test('parse: operation response_payload(this)', () => { const result = parse('response_payload(this)') assert.deepStrictEqual(result.ast, { type: 'operation', header: 'response_payload', parameter: { type: 'this' }, accessor: undefined }) }) test('parse: operation with accessor request_headers(this).x-foo', () => { const result = parse('request_headers(this).x-foo') assert.deepStrictEqual(result.ast, { type: 'operation', header: 'request_headers', parameter: { type: 'this' }, accessor: ['x-foo'] }) }) test('parse: comparison ==', () => { const result = parse('response_code(this) == 200') assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, '==') }) test('parse: comparison !=', () => { const result = parse('response_code(this) != 500') assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, '!=') }) test('parse: comparison <=', () => { const result = parse('response_time(this) <= 1000') assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, '<=') }) test('parse: comparison >=', () => { const result = parse('response_time(this) >= 100') assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, '>=') }) test('parse: comparison <', () => { const result = parse('response_time(this) < 500') assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, '<') }) test('parse: comparison >', () => { const result = parse('response_time(this) > 50') assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, '>') }) test('parse: comparison matches', () => { const result = parse("response_body(this) matches '\\d+'") assert.strictEqual(result.ast.type, 'comparison') assert.strictEqual((result.ast as Extract).op, 'matches') }) test('parse: boolean &&', () => { const result = parse('T && F') assert.strictEqual(result.ast.type, 'boolean') assert.strictEqual((result.ast as Extract).op, '&&') }) test('parse: boolean ||', () => { const result = parse('T || F') assert.strictEqual(result.ast.type, 'boolean') assert.strictEqual((result.ast as Extract).op, '||') }) test('parse: boolean => (implication)', () => { const result = parse('T => F') assert.strictEqual(result.ast.type, 'boolean') assert.strictEqual((result.ast as Extract).op, '=>') }) test('parse: boolean precedence keeps && tighter than ||', () => { const result = parse('T || F && F') assert.strictEqual(result.ast.type, 'boolean') const root = result.ast as Extract assert.strictEqual(root.op, '||') assert.strictEqual(root.right.type, 'boolean') assert.strictEqual((root.right as Extract).op, '&&') }) test('parse: implication is right-associative', () => { const result = parse('T => F => T') assert.strictEqual(result.ast.type, 'boolean') const root = result.ast as Extract assert.strictEqual(root.op, '=>') assert.strictEqual(root.right.type, 'boolean') assert.strictEqual((root.right as Extract).op, '=>') }) test('parse: conditional if/then/else', () => { const result = parse('if T then 1 else 2') assert.strictEqual(result.ast.type, 'conditional') const cond = result.ast as Extract assert.deepStrictEqual(cond.condition, { type: 'literal', value: true }) assert.deepStrictEqual(cond.then, { type: 'literal', value: 1 }) assert.deepStrictEqual(cond.else, { type: 'literal', value: 2 }) }) test('parse: nested conditional with cross-operation call', () => { const result = parse('if status:201 then if T then response_code(GET /users/{userId}) == 200 else F else T') assert.strictEqual(result.ast.type, 'conditional') const root = result.ast as Extract assert.strictEqual(root.then.type, 'conditional') }) test('parse: quantified for/in', () => { const result = parse('for item in response_body(this): item == 1') assert.strictEqual(result.ast.type, 'quantified') const q = result.ast as Extract assert.strictEqual(q.quantifier, 'for') assert.strictEqual(q.variable, 'item') }) test('parse: quantified exists/in', () => { const result = parse('exists item in response_body(this): item == 1') assert.strictEqual(result.ast.type, 'quantified') const q = result.ast as Extract assert.strictEqual(q.quantifier, 'exists') }) test('parse: quantified supports paper-style :- delimiter', () => { const result = parse('for item in response_body(this):- item == 1') assert.strictEqual(result.ast.type, 'quantified') }) test('parse: previous() wrapper', () => { const result = parse('previous(response_code(this))') assert.strictEqual(result.ast.type, 'previous') const prev = result.ast as Extract assert.strictEqual(prev.inner.type, 'operation') }) test('parse: pure GET operation call', () => { const result = parse('response_code(GET /users/{userId}) == 200') assert.strictEqual(result.ast.type, 'comparison') const left = (result.ast as Extract).left assert.strictEqual(left.type, 'operation') assert.deepStrictEqual((left as Extract).parameter, { type: 'call', method: 'GET', path: [ { type: 'text', value: '/users/' }, { type: 'expression', expression: { type: 'variable', name: 'userId', accessor: undefined } }, ], }) }) test('parse: T shorthand for true', () => { const result = parse('T') assert.deepStrictEqual(result.ast, { type: 'literal', value: true }) }) test('parse: F shorthand for false', () => { const result = parse('F') assert.deepStrictEqual(result.ast, { type: 'literal', value: false }) }) test('parse: throws on empty formula', () => { assert.throws(() => parse(''), /Empty formula/) }) test('parse: throws on unexpected token', () => { assert.throws(() => parse('T extra'), /Unexpected token/) }) // ============================================================================ // Unit Tests: Evaluator // ============================================================================ test('evaluate: literal true returns true', () => { const ctx = makeContext() const result = evalFormula('true', ctx) assert.strictEqual(result, true) }) test('evaluate: literal false returns false', () => { const ctx = makeContext() const result = evalFormula('false', ctx) assert.strictEqual(result, false) }) test('evaluate: operation resolves response_code', () => { const ctx = makeContext({ response: { statusCode: 201, body: null, headers: {}, responseTime: 0 } }) const result = evalFormula('response_code(this)', ctx) assert.strictEqual(result, 201) }) test('evaluate: operation resolves response_body with accessor', () => { const ctx = makeContext({ response: { body: { id: 42 }, headers: {}, statusCode: 200, responseTime: 0 } }) const result = evalFormula('response_body(this).id', ctx) assert.strictEqual(result, 42) }) test('evaluate: response_payload returns plain JSON body', () => { const ctx = makeContext({ response: { body: { id: 'u1' }, headers: {}, statusCode: 200, responseTime: 0 } }) const result = evalFormula('response_payload(this).id', ctx) assert.strictEqual(result, 'u1') }) test('evaluate: response_payload unwraps LDF-style data field', () => { const ctx = makeContext({ response: { body: { data: { id: 'u2' }, controls: { self: { href: '/users/u2' } } }, headers: {}, statusCode: 200, responseTime: 0 } }) const result = evalFormula('response_payload(this).id', ctx) assert.strictEqual(result, 'u2') }) test('evaluate: response_payload falls back for null and primitive bodies', () => { const nullCtx = makeContext({ response: { body: null, headers: {}, statusCode: 200, responseTime: 0 } }) const primitiveCtx = makeContext({ response: { body: 'ok', headers: {}, statusCode: 200, responseTime: 0 } }) assert.strictEqual(evalFormula('response_payload(this)', nullCtx), null) assert.strictEqual(evalFormula('response_payload(this)', primitiveCtx), 'ok') }) test('evaluate: comparison == with numbers', () => { const ctx = makeContext({ response: { statusCode: 200, body: null, headers: {}, responseTime: 0 } }) const result = evalFormula('response_code(this) == 200', ctx) assert.strictEqual(result, true) }) test('evaluate: comparison !=', () => { const ctx = makeContext({ response: { statusCode: 200, body: null, headers: {}, responseTime: 0 } }) const result = evalFormula('response_code(this) != 500', ctx) assert.strictEqual(result, true) }) test('evaluate: comparison < with response_time', () => { const ctx = makeContext({ response: { responseTime: 50, body: null, headers: {}, statusCode: 200 } }) const result = evalFormula('response_time(this) < 100', ctx) assert.strictEqual(result, true) }) test('evaluate: comparison matches with regex', () => { const ctx = makeContext({ response: { body: 'hello123world', headers: {}, statusCode: 200, responseTime: 0 } }) const result = evalFormula("response_body(this) matches '\\d+'", ctx) assert.strictEqual(result, true) }) test('evaluate: boolean && short-circuits correctly', () => { const ctx = makeContext() assert.strictEqual(evalFormula('T && T', ctx), true) assert.strictEqual(evalFormula('T && F', ctx), false) assert.strictEqual(evalFormula('F && T', ctx), false) }) test('evaluate: boolean || works correctly', () => { const ctx = makeContext() assert.strictEqual(evalFormula('T || F', ctx), true) assert.strictEqual(evalFormula('F || F', ctx), false) }) test('evaluate: boolean && short-circuits errors on right branch', () => { const ctx = makeContext() assert.strictEqual(evalFormula('F && previous(response_code(this))', ctx), false) }) test('evaluate: boolean || short-circuits errors on right branch', () => { const ctx = makeContext() assert.strictEqual(evalFormula('T || previous(response_code(this))', ctx), true) }) test('evaluate: boolean => (implication) works correctly', () => { const ctx = makeContext() assert.strictEqual(evalFormula('T => T', ctx), true) assert.strictEqual(evalFormula('T => F', ctx), false) assert.strictEqual(evalFormula('F => T', ctx), true) assert.strictEqual(evalFormula('F => F', ctx), true) }) test('evaluate: implication short-circuits consequent when antecedent is false', () => { const ctx = makeContext() assert.strictEqual(evalFormula('F => previous(response_code(this))', ctx), true) }) test('evaluate: conditional if true then X else Y returns X', () => { const ctx = makeContext() const result = evalFormula('if T then 42 else 0', ctx) assert.strictEqual(result, 42) }) test('evaluate: conditional if false then X else Y returns Y', () => { const ctx = makeContext() const result = evalFormula('if F then 42 else 0', ctx) assert.strictEqual(result, 0) }) test('evaluate: quantified for all items match condition', () => { const ctx = makeContext({ response: { body: [1, 1, 1], headers: {}, statusCode: 200, responseTime: 0 } }) const result = evalFormula('for x in response_body(this): x == 1', ctx) assert.strictEqual(result, true) }) test('evaluate: quantified exists finds matching item', () => { const ctx = makeContext({ response: { body: [1, 2, 3], headers: {}, statusCode: 200, responseTime: 0 } }) const result = evalFormula('exists x in response_body(this): x == 2', ctx) assert.strictEqual(result, true) }) test('evaluate: previous() resolves from previous context', () => { const prevCtx = makeContext({ response: { statusCode: 200, body: null, headers: {}, responseTime: 0 } }) const ctx = makeContext({ response: { statusCode: 500, body: null, headers: {}, responseTime: 0 }, previous: prevCtx }) const result = evalFormula('previous(response_code(this))', ctx) assert.strictEqual(result, 200) }) test('evaluateAsync: pure GET operation call resolves through operation resolver', async () => { const resolverCalls: string[] = [] const ctx: EvalContext = { ...makeContext({ request: { params: { userId: 'user-123' }, body: null, headers: {}, query: {}, cookies: {} } }), operationResolver: { cache: new Map(), execute: async (method, url) => { resolverCalls.push(`${method} ${url}`) return makeContext({ response: { statusCode: 200, body: { id: 'user-123' }, headers: {}, responseTime: 0 } }) }, }, } const result = await evalFormulaAsync('response_code(GET /users/{userId}) == 200', ctx) assert.strictEqual(result, true) assert.deepStrictEqual(resolverCalls, ['GET /users/user-123']) }) test('evaluateAsync: previous() uses before-context for pure GET operation calls', async () => { const beforeCache = new Map([ ['GET /plans/basic', makeContext({ response: { statusCode: 404, body: null, headers: {}, responseTime: 0 } })], ]) const beforeCtx: EvalContext = { ...makeContext({ request: { params: { planId: 'basic' }, body: null, headers: {}, query: {}, cookies: {} } }), operationResolver: { cache: beforeCache, execute: async () => makeContext({ response: { statusCode: 404, body: null, headers: {}, responseTime: 0 } }), }, } const ctx: EvalContext = { ...makeContext({ request: { params: { planId: 'basic' }, body: null, headers: {}, query: {}, cookies: {} } }), before: beforeCtx, operationResolver: { cache: new Map(), execute: async () => makeContext({ response: { statusCode: 200, body: { id: 'basic' }, headers: {}, responseTime: 0 } }), }, } const result = await evalFormulaAsync('previous(response_code(GET /plans/{planId})) == 404', ctx) assert.strictEqual(result, true) }) test('evaluateAsync: previous() can bind path placeholders from current response', async () => { const beforeCalls: string[] = [] const currentCalls: string[] = [] const prefetchedBeforeValue = makeContext({ response: { statusCode: 404, body: null, headers: {}, responseTime: 0 } }) const beforeCache = new Map([['GET /plans/new-plan', prefetchedBeforeValue]]) const beforeCtx: EvalContext = { ...makeContext({ request: { params: { planId: 'basic' }, body: null, headers: {}, query: {}, cookies: {} }, response: { body: null, headers: {}, statusCode: 200, responseTime: 0 }, }), operationResolver: { cache: beforeCache, execute: async (method, url) => { beforeCalls.push(`${method} ${url}`) return makeContext({ response: { statusCode: 404, body: null, headers: {}, responseTime: 0 } }) }, }, } const ctx: EvalContext = { ...makeContext({ request: { params: { planId: 'basic' }, body: null, headers: {}, query: {}, cookies: {} }, response: { body: { id: 'new-plan' }, headers: {}, statusCode: 201, responseTime: 0 }, }), before: beforeCtx, operationResolver: { cache: new Map(), execute: async (method, url) => { currentCalls.push(`${method} ${url}`) return makeContext({ response: { statusCode: 200, body: { id: 'new-plan' }, headers: {}, responseTime: 0 } }) }, }, } const result = await evalFormulaAsync('previous(response_code(GET /plans/{response_body(this).id})) == 404', ctx) assert.strictEqual(result, true) assert.deepStrictEqual(beforeCalls, []) assert.deepStrictEqual(currentCalls, []) }) test('evaluateAsync: previous() fails clearly when pure GET call was not prefetched', async () => { const ctx: EvalContext = { ...makeContext({ request: { params: {}, body: null, headers: {}, query: {}, cookies: {} }, response: { body: { id: 'new-plan' }, headers: {}, statusCode: 201, responseTime: 0 }, }), before: { ...makeContext(), operationResolver: { cache: new Map(), execute: async () => makeContext({ response: { statusCode: 404, body: null, headers: {}, responseTime: 0 } }), }, }, } await assert.rejects( () => evalFormulaAsync('previous(response_code(GET /plans/{response_body(this).id})) == 404', ctx), /not prefetched/ ) }) test('evaluateAsync: boolean || short-circuits pure GET operation calls', async () => { const resolverCalls: string[] = [] const ctx: EvalContext = { ...makeContext({ request: { params: { userId: 'user-123' }, body: null, headers: {}, query: {}, cookies: {} } }), operationResolver: { cache: new Map(), execute: async (method, url) => { resolverCalls.push(`${method} ${url}`) return makeContext({ response: { statusCode: 200, body: {}, headers: {}, responseTime: 0 } }) }, }, } const result = await evalFormulaAsync('T || response_code(GET /users/{userId}) == 200', ctx) assert.strictEqual(result, true) assert.deepStrictEqual(resolverCalls, []) }) test('evaluateAsync: implication skips pure GET consequent when antecedent is false', async () => { const resolverCalls: string[] = [] const ctx: EvalContext = { ...makeContext({ request: { params: { userId: 'user-123' }, body: null, headers: {}, query: {}, cookies: {} } }), operationResolver: { cache: new Map(), execute: async (method, url) => { resolverCalls.push(`${method} ${url}`) return makeContext({ response: { statusCode: 200, body: {}, headers: {}, responseTime: 0 } }) }, }, } const result = await evalFormulaAsync('F => response_code(GET /users/{userId}) == 200', ctx) assert.strictEqual(result, true) assert.deepStrictEqual(resolverCalls, []) }) test('evaluateAsync: nested conditional evaluates cross-operation call', async () => { const resolverCalls: string[] = [] const ctx: EvalContext = { ...makeContext({ request: { params: { userId: 'user-123' }, body: null, headers: {}, query: {}, cookies: {} }, response: { statusCode: 201, body: null, headers: {}, responseTime: 0 }, }), operationResolver: { cache: new Map(), execute: async (method, url) => { resolverCalls.push(`${method} ${url}`) return makeContext({ response: { statusCode: 200, body: {}, headers: {}, responseTime: 0 } }) }, }, } const result = await evalFormulaAsync( 'if status:201 then if T then response_code(GET /users/{userId}) == 200 else F else T', ctx ) assert.strictEqual(result, true) assert.deepStrictEqual(resolverCalls, ['GET /users/user-123']) }) test('evaluate: deeply nested conditionals are supported within stack limits', () => { const depth = 64 let formula = 'T' for (let i = 0; i < depth; i++) { formula = `if T then ${formula} else F` } const result = evalFormula(formula, makeContext()) assert.strictEqual(result, true) }) test('evaluate: variable resolves from request params', () => { const ctx = makeContext({ request: { params: { userId: 42 }, body: null, headers: {}, query: {}, cookies: {} } }) const result = evalFormula('userId', ctx) assert.strictEqual(result, 42) }) test('evaluate: variable with accessor resolves nested property', () => { const ctx = makeContext({ request: { params: { user: { name: 'alice' } }, body: null, headers: {}, query: {}, cookies: {} } }) const result = evalFormula('user.name', ctx) assert.strictEqual(result, 'alice') }) test('evaluate: returns error for missing previous context', () => { const ast = parse('previous(response_code(this))') const result = evaluate(ast.ast, makeContext()) assert.strictEqual(result.success, false) assert.ok((result as { success: false; error: string }).error.includes('No previous context')) }) test('evaluate: returns false for non-array in quantified expression', () => { const ast = parse('for x in response_code(this): x == 1') const result = evaluate(ast.ast, makeContext()) assert.strictEqual(result.success, true) assert.strictEqual((result as { success: true; value: unknown }).value, false) }) test('evaluate: returns false for undefined collection in quantified expression', () => { const ast = parse('for x in response_body(this): x == 1') const result = evaluate(ast.ast, makeContext({ response: { body: undefined, headers: {}, statusCode: 200, responseTime: 0 } })) assert.strictEqual(result.success, true) assert.strictEqual((result as { success: true; value: unknown }).value, false) }) test('evaluate: returns false for error object collection in quantified expression', () => { const ast = parse('for x in response_body(this): x == 1') const result = evaluate(ast.ast, makeContext({ response: { body: { error: 'Chaos error: forced 503' }, headers: {}, statusCode: 200, responseTime: 0 } })) assert.strictEqual(result.success, true) assert.strictEqual((result as { success: true; value: unknown }).value, false) }) test('evaluate: quantified nested in conditional handles undefined gracefully', () => { const ast = parse('if status:503 then for x in response_body(this): x == 1 else true') const result = evaluate(ast.ast, makeContext({ response: { statusCode: 503, body: undefined, headers: {}, responseTime: 0 } })) assert.strictEqual(result.success, true) assert.strictEqual((result as { success: true; value: unknown }).value, false) }) test('evaluate: exists returns false for non-array collection', () => { const ast = parse('exists x in response_body(this): x == 1') const result = evaluate(ast.ast, makeContext({ response: { body: { error: 'fail' }, headers: {}, statusCode: 200, responseTime: 0 } })) assert.strictEqual(result.success, true) assert.strictEqual((result as { success: true; value: unknown }).value, false) }) // ============================================================================ // Unit Tests: Substitutor // ============================================================================ test('substitute: replaces simple parameter', () => { const result = substitute('x == {val}', { val: 42 }) assert.strictEqual(result, "x == 42") }) test('substitute: replaces string parameter with escaping', () => { const result = substitute("x == {val}", { val: "it's" }) assert.strictEqual(result, "x == 'it\\'s'") }) test('substitute: replaces nested path parameter', () => { const result = substitute('x == {t.id}', { t: { id: 99 } }) assert.strictEqual(result, "x == 99") }) test('substitute: replaces null', () => { const result = substitute('x == {val}', { val: null }) assert.strictEqual(result, "x == null") }) test('substitute: replaces boolean', () => { const result = substitute('x == {val}', { val: true }) assert.strictEqual(result, "x == true") }) test('substitute: escapes newline in string', () => { const result = substitute('x == {val}', { val: 'a\nb' }) assert.strictEqual(result, "x == 'a\\nb'") }) test('substitute: escapes tab in string', () => { const result = substitute('x == {val}', { val: 'a\tb' }) assert.strictEqual(result, "x == 'a\\tb'") }) test('substitute: escapes backslash in string', () => { const result = substitute('x == {val}', { val: 'a\\b' }) assert.strictEqual(result, "x == 'a\\\\b'") }) test('substitute: replaces object with JSON string', () => { const result = substitute('x == {val}', { val: { a: 1 } }) assert.strictEqual(result, "x == '{\"a\":1}'") }) test('substitute: throws on missing parameter', () => { assert.throws(() => substitute('x == {val}', {}), /Missing parameters: val/) }) test('substitute: invalid parameter with special chars is not matched and preserved', () => { // Special chars like @ are not matched by PARAM_PATTERN, so the text is preserved as-is const result = substitute('x == {a@b}', { 'a@b': 1 }) assert.strictEqual(result, 'x == {a@b}') }) // ============================================================================ // Property-Based Tests // ============================================================================ const mockContext = makeContext() // Helper to build a simple stringifier for round-trip tests function stringifyNode(node: FormulaNode): string { switch (node.type) { case 'literal': if (node.value === null) return 'null' if (node.value === true) return 'true' if (node.value === false) return 'false' if (typeof node.value === 'number') return String(node.value) if (typeof node.value === 'string') return "'" + node.value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t') + "'" return String(node.value) case 'operation': return `${node.header}(this)${node.accessor ? `.${node.accessor}` : ''}` case 'variable': return `${node.name}${node.accessor ? `.${node.accessor}` : ''}` case 'comparison': return `${stringifyNode(node.left)} ${node.op} ${stringifyNode(node.right)}` case 'boolean': return `${stringifyNode(node.left)} ${node.op} ${stringifyNode(node.right)}` case 'conditional': return `if ${stringifyNode(node.condition)} then ${stringifyNode(node.then)} else ${stringifyNode(node.else)}` case 'quantified': return `${node.quantifier} ${node.variable} in ${node.collection.header}(this): ${stringifyNode(node.body)}` case 'previous': return `previous(${stringifyNode(node.inner)})` default: return '' } } function nodesEqual(a: FormulaNode, b: FormulaNode): boolean { if (a.type !== b.type) return false switch (a.type) { case 'literal': return a.value === (b as typeof a).value case 'operation': return a.header === (b as typeof a).header && JSON.stringify(a.parameter) === JSON.stringify((b as typeof a).parameter) && a.accessor === (b as typeof a).accessor case 'variable': return a.name === (b as typeof a).name && a.accessor === (b as typeof a).accessor case 'comparison': case 'boolean': return a.op === (b as typeof a).op && nodesEqual(a.left, (b as typeof a).left) && nodesEqual(a.right, (b as typeof a).right) case 'conditional': return nodesEqual(a.condition, (b as typeof a).condition) && nodesEqual(a.then, (b as typeof a).then) && nodesEqual(a.else, (b as typeof a).else) case 'quantified': return a.quantifier === (b as typeof a).quantifier && a.variable === (b as typeof a).variable && a.collection.header === (b as typeof a).collection.header && JSON.stringify(a.collection.parameter) === JSON.stringify((b as typeof a).collection.parameter) && a.collection.accessor === (b as typeof a).collection.accessor && nodesEqual(a.body, (b as typeof a).body) case 'previous': return nodesEqual(a.inner, (b as typeof a).inner) default: return false } } // Arbitrary for generating simple formula ASTs const literalArb = fc.oneof( fc.constant(null), fc.boolean(), fc.integer(), fc.string({ minLength: 0, maxLength: 10 }).filter(s => !s.includes("'") && !s.includes('\\') && !s.includes('\n') && !s.includes('\r') && !s.includes('\t')) ).map(v => ({ type: 'literal' as const, value: v })) const operationArb = fc.constantFrom('request_body', 'response_body', 'response_payload', 'response_code', 'request_headers', 'response_headers', 'query_params', 'cookies', 'response_time').map(header => ({ type: 'operation' as const, header: header as 'request_body' | 'response_body' | 'response_payload' | 'response_code' | 'request_headers' | 'response_headers' | 'query_params' | 'cookies' | 'response_time', parameter: { type: 'this' as const }, accessor: undefined as string[] | undefined })) const simpleNodeArb = fc.oneof(literalArb, operationArb) const comparisonArb = fc.tuple(simpleNodeArb, fc.constantFrom('==', '!=', '<=', '>=', '<', '>' as const), simpleNodeArb).map(([left, op, right]) => ({ type: 'comparison' as const, op, left, right })) const booleanArb = fc.tuple(simpleNodeArb, fc.constantFrom('&&', '||' as const), simpleNodeArb).map(([left, op, right]) => ({ type: 'boolean' as const, op, left, right })) const formulaNodeArb = fc.oneof(simpleNodeArb, comparisonArb, booleanArb) test('property: parser round-trip for simple nodes', async () => { await fc.assert( fc.property(formulaNodeArb, (node) => { const str = stringifyNode(node) const parsed = parse(str) return nodesEqual(parsed.ast, node) }), { numRuns: 100 } ) }) test('property: T is always true', async () => { await fc.assert( fc.property(fc.context(), (_ctx) => { const ast = parse('T') const result = evaluate(ast.ast, mockContext) return result.success && result.value === true }), { numRuns: 100 } ) }) test('property: F is always false', async () => { await fc.assert( fc.property(fc.context(), (_ctx) => { const ast = parse('F') const result = evaluate(ast.ast, mockContext) return result.success && result.value === false }), { numRuns: 100 } ) }) test('property: A == A is always true (reflexivity)', async () => { await fc.assert( fc.property(fc.integer(), (n) => { const ast = parse(`${n} == ${n}`) const result = evaluate(ast.ast, mockContext) return result.success && result.value === true }), { numRuns: 100 } ) }) test('property: A && B == B && A (commutativity)', async () => { await fc.assert( fc.property(fc.boolean(), fc.boolean(), (a, b) => { const ast1 = parse(`${a} && ${b}`) const ast2 = parse(`${b} && ${a}`) const result1 = evaluate(ast1.ast, mockContext) const result2 = evaluate(ast2.ast, mockContext) return result1.success && result2.success && result1.value === result2.value }), { numRuns: 100 } ) }) test('property: if true then X else Y == X', async () => { await fc.assert( fc.property(fc.integer(), fc.integer(), (x, y) => { const ast = parse(`if true then ${x} else ${y}`) const result = evaluate(ast.ast, mockContext) return result.success && result.value === x }), { numRuns: 100 } ) }) test('property: if false then X else Y == Y', async () => { await fc.assert( fc.property(fc.integer(), fc.integer(), (x, y) => { const ast = parse(`if false then ${x} else ${y}`) const result = evaluate(ast.ast, mockContext) return result.success && result.value === y }), { numRuns: 100 } ) }) test('property: negation !T == F and !F == T', async () => { await fc.assert( fc.property(fc.boolean(), (a) => { // Negation: !A == if A then F else T const boolLit = a ? 'T' : 'F' const formula = `if ${boolLit} then F else T` const ast = parse(formula) const result = evaluate(ast.ast, mockContext) return result.success && result.value === !a }), { numRuns: 100 } ) }) test('property: conditional identity if A then T else F == A', async () => { await fc.assert( fc.property(fc.boolean(), (a) => { const boolLit = a ? 'T' : 'F' const formula = `if ${boolLit} then T else F` const ast = parse(formula) const result = evaluate(ast.ast, mockContext) return result.success && result.value === a }), { numRuns: 100 } ) }) test('property: substitute preserves non-parameter text', async () => { await fc.assert( fc.property(fc.string({ minLength: 1, maxLength: 20 }).filter(s => !s.includes('{') && !s.includes('}')), (text) => { const result = substitute(text, {}) return result === text }), { numRuns: 100 } ) }) test('property: substitute with numbers produces parseable literals', async () => { await fc.assert( fc.property(fc.integer(), (n) => { const formula = substitute('x == {val}', { val: n }) const ast = parse(formula) const cmp = ast.ast as Extract return ast.ast.type === 'comparison' && cmp.right.type === 'literal' && cmp.right.value === n }), { numRuns: 100 } ) }) // ============================================================================ // Parse Error Messages // ============================================================================ test('parse error: includes position info', () => { try { parse('response_body(this).name == ') assert.fail('should have thrown') } catch (err) { const message = (err as Error).message assert.ok(message.includes('Parse error at position'), 'should include position') assert.ok(message.includes('^'), 'should include pointer') assert.ok(message.includes('Expected'), 'should include expected token') } }) test('parse error: shows unexpected token', () => { try { parse('status == 200 extra') assert.fail('should have thrown') } catch (err) { const message = (err as Error).message assert.ok(message.includes('Unexpected token'), 'should mention unexpected token') assert.ok(message.includes('extra'), 'should show the extra token') } }) test('parse error: unterminated string', () => { try { parse("status == '") assert.fail('should have thrown') } catch (err) { const message = (err as Error).message assert.ok(message.includes('Unterminated string literal'), 'should mention unterminated string') } }) test('parse error: missing this', () => { try { parse('response_body( ).name == "test"') assert.fail('should have thrown') } catch (err) { const message = (err as Error).message assert.ok(message.includes("Expected 'this'"), 'should mention expected this') } }) test('parse error: unknown operation header includes extension guidance', () => { try { parse('route_exists(this).controls.self.href == true') assert.fail('should have thrown') } catch (err) { const message = (err as Error).message assert.ok(message.includes('Unknown operation header "route_exists"')) assert.ok(message.includes('register the extension')) } }) // ============================================================================ // validateFormula: Friendly error messages // ============================================================================ test('validateFormula: returns valid for correct formula', () => { const result = validateFormula('status:200') assert.strictEqual(result.valid, true) if (result.valid) { assert.strictEqual(result.ast.type, 'status') } }) test('validateFormula: returns structured error for bad formula', () => { const result = validateFormula('response_body().name == "test"') assert.strictEqual(result.valid, false) if (!result.valid) { assert.ok(result.error.length > 0) assert.ok(result.position >= 0) assert.ok(result.suggestion.includes('this'), 'should suggest using (this)') } }) test('validateFormula: suggests status format for status errors', () => { const result = validateFormula('status : 200') assert.strictEqual(result.valid, false) if (!result.valid) { assert.ok(result.suggestion.includes('status:200'), 'should suggest no spaces') } }) test('validateFormula: suggests equality operator', () => { const result = validateFormula('response_body(this).name = "test"') assert.strictEqual(result.valid, false) if (!result.valid) { assert.ok(result.suggestion.includes('=='), 'should suggest == operator') } }) // ============================================================================ // Parse Cache Tests // ============================================================================ import { setParseCacheLimit, getParseCacheLimit, clearParseCache } from '../formula/parser.js' test('parse cache: configurable limit', () => { const original = getParseCacheLimit() clearParseCache() setParseCacheLimit(2) assert.strictEqual(getParseCacheLimit(), 2) parse('response_body(this) == 1') parse('response_body(this) == 2') parse('response_body(this) == 3') // First entry should be evicted setParseCacheLimit(1000) clearParseCache() }) test('parse cache: limit 0 disables caching', () => { clearParseCache() setParseCacheLimit(0) parse('response_body(this) == 1') parse('response_body(this) == 1') // Should re-parse setParseCacheLimit(1000) }) test('parse cache: negative limit throws', () => { assert.throws(() => setParseCacheLimit(-1), /non-negative/) }) import type { EvalContext } from '../types.js' import type { FormulaNode } from '../domain/formula.js'