@@ -1495,6 +1495,122 @@ describe('validateToolInput - Absent Optional Fields in Nested Objects (GitHub #
14951495 } ) ;
14961496} ) ;
14971497
1498+ describe ( 'validateToolInput - Value-Based Null Detection (GitHub #14476)' , ( ) => {
1499+ // These tests verify the fix for https://github.com/mastra-ai/mastra/issues/14476
1500+ // The null detection in Step 5 should check the actual value at the failing path
1501+ // rather than relying on error message string matching (e.g., checking for 'null'
1502+ // in the message). This ensures null values are detected even when validators
1503+ // return messages like "must be string" or "must be object".
1504+
1505+ it ( 'should detect null values even when error message does not contain "null"' , ( ) => {
1506+ // Use a custom refinement whose error message deliberately avoids "null".
1507+ // This simulates non-Zod Standard Schema validators (e.g. JSON Schema)
1508+ // that report errors like "must be string" instead of "received null".
1509+ const schema = z . object ( {
1510+ name : z . string ( ) ,
1511+ description : z
1512+ . string ( )
1513+ . optional ( )
1514+ . superRefine ( ( val , ctx ) => {
1515+ if ( typeof val !== 'string' && val !== undefined ) {
1516+ ctx . addIssue ( { code : z . ZodIssueCode . custom , message : 'must be a valid string value' } ) ;
1517+ }
1518+ } ) ,
1519+ tags : z
1520+ . array ( z . string ( ) )
1521+ . optional ( )
1522+ . superRefine ( ( val , ctx ) => {
1523+ if ( ! Array . isArray ( val ) && val !== undefined ) {
1524+ ctx . addIssue ( { code : z . ZodIssueCode . custom , message : 'expected an array of strings' } ) ;
1525+ }
1526+ } ) ,
1527+ } ) ;
1528+
1529+ // LLM sends null for optional fields — error messages won't contain "null"
1530+ const input = { name : 'test' , description : null , tags : null } ;
1531+
1532+ const result = validateToolInput ( schema , input ) ;
1533+
1534+ expect ( result . error ) . toBeUndefined ( ) ;
1535+ expect ( result . data ) . toEqual ( { name : 'test' } ) ;
1536+ } ) ;
1537+
1538+ it ( 'should handle null in nested optional fields with non-null error messages' , ( ) => {
1539+ // Custom refinements that produce errors without "null" in the message
1540+ const schema = z . object ( {
1541+ config : z . object ( {
1542+ timeout : z
1543+ . number ( )
1544+ . optional ( )
1545+ . superRefine ( ( val , ctx ) => {
1546+ if ( typeof val !== 'number' && val !== undefined ) {
1547+ ctx . addIssue ( { code : z . ZodIssueCode . custom , message : 'must be a numeric value' } ) ;
1548+ }
1549+ } ) ,
1550+ retries : z
1551+ . number ( )
1552+ . optional ( )
1553+ . superRefine ( ( val , ctx ) => {
1554+ if ( typeof val !== 'number' && val !== undefined ) {
1555+ ctx . addIssue ( { code : z . ZodIssueCode . custom , message : 'must be a numeric value' } ) ;
1556+ }
1557+ } ) ,
1558+ label : z
1559+ . string ( )
1560+ . optional ( )
1561+ . superRefine ( ( val , ctx ) => {
1562+ if ( typeof val !== 'string' && val !== undefined ) {
1563+ ctx . addIssue ( { code : z . ZodIssueCode . custom , message : 'must be a valid string value' } ) ;
1564+ }
1565+ } ) ,
1566+ } ) ,
1567+ } ) ;
1568+
1569+ const input = {
1570+ config : {
1571+ timeout : null ,
1572+ retries : null ,
1573+ label : null ,
1574+ } ,
1575+ } ;
1576+
1577+ const result = validateToolInput ( schema , input ) ;
1578+
1579+ expect ( result . error ) . toBeUndefined ( ) ;
1580+ expect ( result . data ) . toEqual ( { config : { } } ) ;
1581+ } ) ;
1582+
1583+ it ( 'should still preserve null for .nullable() fields' , ( ) => {
1584+ const schema = z . object ( {
1585+ name : z . string ( ) ,
1586+ deletedAt : z . string ( ) . nullable ( ) ,
1587+ note : z . string ( ) . optional ( ) ,
1588+ } ) ;
1589+
1590+ const input = { name : 'test' , deletedAt : null , note : null } ;
1591+
1592+ const result = validateToolInput ( schema , input ) ;
1593+
1594+ expect ( result . error ) . toBeUndefined ( ) ;
1595+ expect ( result . data ) . toEqual ( { name : 'test' , deletedAt : null } ) ;
1596+ } ) ;
1597+
1598+ it ( 'should not misidentify non-null values at failing paths as null-related' , ( ) => {
1599+ const schema = z . object ( {
1600+ count : z . number ( ) ,
1601+ name : z . string ( ) ,
1602+ } ) ;
1603+
1604+ // Invalid types but not null - these should NOT be treated as null-related
1605+ const input = { count : 'not-a-number' , name : 123 } ;
1606+
1607+ const result = validateToolInput ( schema , input ) ;
1608+
1609+ // Should still fail validation (can't fix by stripping)
1610+ expect ( result . error ) . toBeDefined ( ) ;
1611+ } ) ;
1612+ } ) ;
1613+
14981614describe ( 'validateToolInput - Undefined to Null Conversion (GitHub #11457)' , ( ) => {
14991615 // These tests verify the fix for https://github.com/mastra-ai/mastra/issues/11457
15001616 // When schemas are processed through OpenAI compat layers, .optional() is converted
0 commit comments