@@ -14,6 +14,7 @@ import {
1414 MAX_UNTRUSTED_SCHEMA_BYTES ,
1515 markInputPropertiesAsRequired ,
1616 shortenProperties ,
17+ stripTruncatedEnumsForValidation ,
1718 transformActorInputSchemaProperties ,
1819} from '../../src/tools/actor_input_schema.js' ;
1920import { isActorBlockedUnderPaymentProvider } from '../../src/tools/actor_tool_naming.js' ;
@@ -499,6 +500,95 @@ describe('shortenProperties', () => {
499500 } ) ;
500501} ) ;
501502
503+ describe ( 'stripTruncatedEnumsForValidation' , ( ) => {
504+ it ( 'keeps enum unchanged when it fits comfortably under the cap on both sides' , ( ) => {
505+ const display : Record < string , SchemaProperties > = {
506+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : [ 'a' , 'b' ] } ,
507+ } ;
508+ const raw : Record < string , SchemaProperties > = {
509+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : [ 'a' , 'b' ] } ,
510+ } ;
511+
512+ const result = stripTruncatedEnumsForValidation ( display , raw ) ;
513+
514+ expect ( result . prop1 . enum ) . toEqual ( [ 'a' , 'b' ] ) ;
515+ } ) ;
516+
517+ it ( 'strips enum entirely when it was truncated by shortenProperties' , ( ) => {
518+ const rawEnum = Array . from ( { length : 300 } , ( _ , i ) => `value-${ i } ` ) ;
519+ const display : Record < string , SchemaProperties > = {
520+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : filterAndShortenEnum ( rawEnum ) } ,
521+ } ;
522+ const raw : Record < string , SchemaProperties > = {
523+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : rawEnum } ,
524+ } ;
525+
526+ const result = stripTruncatedEnumsForValidation ( display , raw ) ;
527+
528+ expect ( result . prop1 . enum ) . toBeUndefined ( ) ;
529+ } ) ;
530+
531+ it ( 'strips items.enum entirely when it was truncated (the #1253 shape), leaving other items fields untouched' , ( ) => {
532+ const rawEnum = Array . from ( { length : 300 } , ( _ , i ) => `value-${ i } ` ) ;
533+ const display : Record < string , SchemaProperties > = {
534+ prop1 : {
535+ type : 'array' ,
536+ title : 'Prop 1' ,
537+ description : 'desc' ,
538+ items : { type : 'string' , title : 'Item' , description : 'Item desc' , enum : filterAndShortenEnum ( rawEnum ) } ,
539+ } ,
540+ } ;
541+ const raw : Record < string , SchemaProperties > = {
542+ prop1 : {
543+ type : 'array' ,
544+ title : 'Prop 1' ,
545+ description : 'desc' ,
546+ items : { type : 'string' , title : 'Item' , description : 'Item desc' , enum : rawEnum } ,
547+ } ,
548+ } ;
549+
550+ const result = stripTruncatedEnumsForValidation ( display , raw ) ;
551+
552+ expect ( result . prop1 . items ?. enum ) . toBeUndefined ( ) ;
553+ expect ( result . prop1 . items ?. type ) . toBe ( 'string' ) ;
554+ expect ( result . prop1 . items ?. title ) . toBe ( 'Item' ) ;
555+ } ) ;
556+
557+ it ( 'does not treat empty-string removal as truncation when non-empty count never exceeds the cap' , ( ) => {
558+ const rawEnum = [ 'a' , 'b' , '' , '' , 'c' ] ;
559+ const display : Record < string , SchemaProperties > = {
560+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : filterAndShortenEnum ( rawEnum ) } ,
561+ } ;
562+ const raw : Record < string , SchemaProperties > = {
563+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : rawEnum } ,
564+ } ;
565+
566+ const result = stripTruncatedEnumsForValidation ( display , raw ) ;
567+
568+ expect ( result . prop1 . enum ) . toEqual ( [ 'a' , 'b' , 'c' ] ) ;
569+ } ) ;
570+
571+ it ( 'leaves a property with no counterpart in rawProperties untouched (e.g. the injected waitSecs)' , ( ) => {
572+ const display : Record < string , SchemaProperties > = {
573+ waitSecs : { type : 'integer' , title : 'Wait seconds' , description : 'desc' } ,
574+ } ;
575+
576+ const result = stripTruncatedEnumsForValidation ( display , { } ) ;
577+
578+ expect ( result ) . toEqual ( display ) ;
579+ } ) ;
580+
581+ it ( 'is a safe no-op when rawProperties is empty' , ( ) => {
582+ const display : Record < string , SchemaProperties > = {
583+ prop1 : { type : 'string' , title : 'Prop 1' , description : 'desc' , enum : [ 'a' , 'b' ] } ,
584+ } ;
585+
586+ const result = stripTruncatedEnumsForValidation ( display , { } ) ;
587+
588+ expect ( result ) . toEqual ( display ) ;
589+ } ) ;
590+ } ) ;
591+
502592describe ( 'encodeDotPropertyNames' , ( ) => {
503593 it ( 'should replace dots in property names with -dot-' , ( ) => {
504594 const input = {
@@ -1035,6 +1125,49 @@ describe('buildActorInputSchema + getToolPublicFieldOnly pipeline', () => {
10351125 expect ( schema . properties ?. query ?. description ) . toMatch ( / ^ \* \* R E Q U I R E D \* \* / ) ;
10361126 expect ( schema . properties ?. maxResults ?. description ) . not . toMatch ( / ^ \* \* R E Q U I R E D \* \* / ) ;
10371127 } ) ;
1128+
1129+ // Regression: #1253 — a value cut only by display truncation must still pass AJV.
1130+ it ( 'accepts an enum value dropped by display truncation, while the displayed schema keeps the truncated list' , ( ) => {
1131+ // 'kept-0' is short and sorts first, so it always survives filterAndShortenEnum's cap;
1132+ // 'dropped-value' is padded long enough to land well past the cap.
1133+ const keptValue = 'kept-0' ;
1134+ const droppedValue = 'dropped-value-cut-by-truncation' ;
1135+ const rawEnum = [
1136+ keptValue ,
1137+ ...Array . from ( { length : 300 } , ( _ , i ) => `kept-padding-${ i } -${ 'x' . repeat ( 20 ) } ` ) ,
1138+ droppedValue ,
1139+ ] ;
1140+ const upstream : ActorInputSchema = {
1141+ type : 'object' ,
1142+ properties : {
1143+ categoryFilterWords : {
1144+ type : 'string' ,
1145+ title : 'Category' ,
1146+ description : 'Category filter word.' ,
1147+ enum : rawEnum ,
1148+ } ,
1149+ } ,
1150+ required : [ ] ,
1151+ } ;
1152+
1153+ const { inputSchema } = buildActorInputSchema ( 'compass/crawler-google-places' , upstream , false ) ;
1154+ const displayProperties = inputSchema . properties as Record < string , SchemaProperties > ;
1155+
1156+ // Display schema still shows the truncated enum, unaffected by the fix.
1157+ const displayEnum = displayProperties . categoryFilterWords . enum ;
1158+ expect ( displayEnum ) . toBeDefined ( ) ;
1159+ expect ( displayEnum ! . length ) . toBeGreaterThan ( 0 ) ;
1160+ expect ( displayEnum ! . length ) . toBeLessThan ( rawEnum . length ) ;
1161+ expect ( displayEnum ) . not . toContain ( droppedValue ) ;
1162+
1163+ // AJV accepts the dropped value — the #1253 repro (categoryFilterWords: ['restaurant']).
1164+ const validationSchema = {
1165+ ...inputSchema ,
1166+ properties : stripTruncatedEnumsForValidation ( displayProperties , upstream . properties ) ,
1167+ } ;
1168+ const validate = fixedAjvCompile ( ajv , validationSchema ) ;
1169+ expect ( validate ( { categoryFilterWords : droppedValue } ) ) . toBe ( true ) ;
1170+ } ) ;
10381171} ) ;
10391172
10401173describe ( 'isActorBlockedUnderPaymentProvider' , ( ) => {
0 commit comments