|
1 | 1 | import { describe, it, expect } from 'vitest'; |
2 | | -import { validateExpression, introspectScope, expectedDialect } from './validate'; |
| 2 | +import { validateExpression, introspectScope, expectedDialect, inferExpressionType } from './validate'; |
3 | 3 |
|
4 | 4 | describe('validateExpression (ADR-0032)', () => { |
5 | 5 | describe('predicates (CEL)', () => { |
@@ -165,3 +165,45 @@ describe('validateExpression (ADR-0032)', () => { |
165 | 165 | }); |
166 | 166 | }); |
167 | 167 | }); |
| 168 | + |
| 169 | +describe('inferExpressionType — coarse value-type of a formula', () => { |
| 170 | + // The host object's fields, so a bare `<field>` reference resolves the same as |
| 171 | + // `record.<field>` (a stored formula may be written either way). |
| 172 | + const fields = ['start_date', 'end_date', 'amount', 'rate', 'first', 'last', 'name', 'items']; |
| 173 | + |
| 174 | + it('infers number for a computed-number formula (the leave_days repro)', () => { |
| 175 | + // daysBetween(...): int, int + 1 → int → number. The exact case a "total |
| 176 | + // leave days" dashboard card needs a SUM measure derived for. |
| 177 | + expect(inferExpressionType('daysBetween(start_date, end_date) + 1', { fields })).toBe('number'); |
| 178 | + expect(inferExpressionType('daysBetween(record.start_date, record.end_date) + 1')).toBe('number'); |
| 179 | + expect(inferExpressionType('amount * 0.1', { fields })).toBe('number'); // dyn * double → double |
| 180 | + expect(inferExpressionType('round(amount)', { fields })).toBe('number'); |
| 181 | + expect(inferExpressionType('len(items)', { fields })).toBe('number'); |
| 182 | + }); |
| 183 | + |
| 184 | + it('accepts the canonical Expression envelope as input', () => { |
| 185 | + expect(inferExpressionType({ dialect: 'cel', source: 'amount * 0.1' }, { fields })).toBe('number'); |
| 186 | + }); |
| 187 | + |
| 188 | + it('infers text / boolean / date for non-numeric formulas', () => { |
| 189 | + expect(inferExpressionType('upper(name)', { fields })).toBe('text'); |
| 190 | + expect(inferExpressionType('rate >= 0.5', { fields })).toBe('boolean'); |
| 191 | + expect(inferExpressionType('today()')).toBe('date'); |
| 192 | + }); |
| 193 | + |
| 194 | + it('is conservative — an ambiguous (dyn) result is unknown, never number', () => { |
| 195 | + // `first + last` could be string concatenation OR numeric addition; with two |
| 196 | + // untyped operands cel-js yields `dyn`, so we must NOT call it a number (else |
| 197 | + // a dataset would SUM a text formula). This is the safety property. |
| 198 | + expect(inferExpressionType('first + last', { fields })).toBe('unknown'); |
| 199 | + expect(inferExpressionType('amount + rate', { fields })).toBe('unknown'); |
| 200 | + }); |
| 201 | + |
| 202 | + it('returns unknown for empty, absent, or un-type-checkable expressions', () => { |
| 203 | + expect(inferExpressionType('')).toBe('unknown'); |
| 204 | + expect(inferExpressionType(null)).toBe('unknown'); |
| 205 | + expect(inferExpressionType(undefined)).toBe('unknown'); |
| 206 | + expect(inferExpressionType('no_such_fn(amount)', { fields })).toBe('unknown'); // no overload |
| 207 | + expect(inferExpressionType('undeclared_field + 1')).toBe('unknown'); // bare ref, no fields given |
| 208 | + }); |
| 209 | +}); |
0 commit comments