@@ -200,9 +200,12 @@ describe('validateExpression (ADR-0032)', () => {
200200 expect ( validateExpression ( 'value' , 'record.amount * 2 - 50' , schema ) . warnings ) . toHaveLength ( 0 ) ;
201201 } ) ;
202202
203- it ( 'does NOT warn on a date field compared to today()/daysFromNow()' , ( ) => {
203+ it ( 'does NOT warn on a date field with an ORDERING comparison (they hydrate at runtime)' , ( ) => {
204+ // Ordering ops fault → the engine's string-hydration retry fires → they work.
205+ // (Equality `==`/`!=` does NOT — that is the #3183 silent-miss, covered in its
206+ // own block below; this tier-4 check leaves it to the #3183 guardrail.)
204207 expect ( validateExpression ( 'predicate' , 'record.due <= daysFromNow(30)' , schema ) . warnings ) . toHaveLength ( 0 ) ;
205- expect ( validateExpression ( 'predicate' , 'record.due = = today()' , schema ) . warnings ) . toHaveLength ( 0 ) ;
208+ expect ( validateExpression ( 'predicate' , 'record.due > = today()' , schema ) . warnings ) . toHaveLength ( 0 ) ;
206209 } ) ;
207210
208211 it ( 'does NOT warn on a select field ordered against a number (option values may be numeric codes)' , ( ) => {
@@ -320,3 +323,61 @@ describe('inferExpressionType — coarse value-type of a formula', () => {
320323 expect ( inferExpressionType ( 'undeclared_field + 1' ) ) . toBe ( 'unknown' ) ; // bare ref, no fields given
321324 } ) ;
322325} ) ;
326+
327+ // #3183 — a `Field.date` reads back as a "YYYY-MM-DD" string, and cel-js's
328+ // equality hard-codes `string == <timestamp>` to false, so `record.due_date ==
329+ // today()` silently never matches. Advisory warning that guides the author to a
330+ // working idiom. Derived from the shared `fieldTypes` hint (type === 'date').
331+ describe ( 'temporal date-equality guardrail (#3183)' , ( ) => {
332+ const schema = {
333+ objectName : 'task' ,
334+ fields : [ 'due_date' , 'status' ] as const ,
335+ fieldTypes : { due_date : 'date' , status : 'text' } ,
336+ scope : 'record' ,
337+ } as const ;
338+ const dateWarns = ( src : string , s : Record < string , unknown > = schema ) =>
339+ validateExpression ( 'predicate' , src , s as never ) . warnings
340+ . filter ( w => / c a l e n d a r - d a y \( d a t e \) f i e l d / . test ( w . message ) ) ;
341+
342+ it ( 'warns on `dateField == today()` (field on the left)' , ( ) => {
343+ const r = validateExpression ( 'predicate' , 'record.due_date == today()' , schema ) ;
344+ expect ( r . ok ) . toBe ( true ) ; // advisory — never fails the build
345+ expect ( r . errors ) . toHaveLength ( 0 ) ;
346+ const w = r . warnings . filter ( x => / c a l e n d a r - d a y \( d a t e \) f i e l d / . test ( x . message ) ) ;
347+ expect ( w ) . toHaveLength ( 1 ) ;
348+ expect ( w [ 0 ] . message ) . toMatch ( / d a t e \( r e c o r d \. d u e _ d a t e \) = = t o d a y \( \) / ) ;
349+ } ) ;
350+
351+ it ( 'warns on `today() == dateField` and on daysFromNow/daysAgo/now' , ( ) => {
352+ expect ( dateWarns ( 'today() == record.due_date' ) ) . toHaveLength ( 1 ) ;
353+ expect ( dateWarns ( 'record.due_date == daysFromNow(3)' ) ) . toHaveLength ( 1 ) ;
354+ expect ( dateWarns ( 'record.due_date != daysAgo(7)' ) ) . toHaveLength ( 1 ) ;
355+ expect ( dateWarns ( 'record.due_date == now()' ) ) . toHaveLength ( 1 ) ;
356+ } ) ;
357+
358+ it ( 'warns on a bare date-field ref (flattened flow-condition scope)' , ( ) => {
359+ const flat = { objectName : 'task' , fields : [ 'due_date' ] , fieldTypes : { due_date : 'date' } } ;
360+ expect ( dateWarns ( 'due_date == today()' , flat ) ) . toHaveLength ( 1 ) ;
361+ } ) ;
362+
363+ it ( 'does NOT warn on the working idioms' , ( ) => {
364+ expect ( dateWarns ( 'date(record.due_date) == today()' ) ) . toHaveLength ( 0 ) ;
365+ expect ( dateWarns ( 'record.due_date >= today() && record.due_date <= today()' ) ) . toHaveLength ( 0 ) ;
366+ expect ( dateWarns ( 'daysBetween(today(), record.due_date) == 0' ) ) . toHaveLength ( 0 ) ;
367+ } ) ;
368+
369+ it ( 'does NOT warn on ordering comparisons (they fault→hydrate and work)' , ( ) => {
370+ expect ( dateWarns ( 'record.due_date >= today()' ) ) . toHaveLength ( 0 ) ;
371+ expect ( dateWarns ( 'record.due_date < daysFromNow(30)' ) ) . toHaveLength ( 0 ) ;
372+ } ) ;
373+
374+ it ( 'does NOT warn on a non-date field, or when fieldTypes is absent' , ( ) => {
375+ expect ( dateWarns ( 'record.status == today()' ) ) . toHaveLength ( 0 ) ; // status is text, not date
376+ expect ( dateWarns ( 'record.due_date == today()' , { objectName : 'task' , fields : [ 'due_date' ] , scope : 'record' } ) )
377+ . toHaveLength ( 0 ) ; // no fieldTypes → check skipped
378+ } ) ;
379+
380+ it ( 'de-duplicates repeated references to the same field' , ( ) => {
381+ expect ( dateWarns ( 'record.due_date == today() || record.due_date == daysFromNow(1)' ) ) . toHaveLength ( 1 ) ;
382+ } ) ;
383+ } ) ;
0 commit comments