@@ -481,6 +481,95 @@ describe('typed-value inference', () => {
481481 } ) ;
482482} ) ;
483483
484+ // The inference rule (getDataTypeFromValue in @usebruno/common/utils) is purely:
485+ // string → drop dataType (string is the implicit default)
486+ // number → 'number'
487+ // boolean → 'boolean'
488+ // object → 'object' (arrays are typeof 'object' in JS, so they go here too)
489+ // null / undefined → 'string' (treated as no-type-info → drop)
490+ // Existing dataType on disk does NOT influence the inference — only the new JS value's type.
491+ describe ( 'typed-value inference matrix — dataType derived from the new JS value type' , ( ) => {
492+ const { parseEnvironment } = require ( '@usebruno/filestore' ) ;
493+
494+ // Tuple shape: [ label, scriptValue, expectedDataType, expectedValue ]
495+ // - label : human-readable name interpolated into the test title
496+ // - scriptValue : the value `bru.setEnvVar('v', X)` puts in the env map
497+ // - expectedDataType : what `dataType` should end up on disk (or undefined if absent)
498+ // - expectedValue : what the value should round-trip to through the yml parser
499+ it . each ( [
500+ [ 'string' , 'hello' , undefined , 'hello' ] ,
501+ [ 'number' , 42 , 'number' , 42 ] ,
502+ [ 'number (zero)' , 0 , 'number' , 0 ] ,
503+ [ 'boolean (true)' , true , 'boolean' , true ] ,
504+ [ 'boolean (false)' , false , 'boolean' , false ] ,
505+ [ 'object' , { port : 3000 } , 'object' , { port : 3000 } ] ,
506+ [ 'array (typeof object)' , [ 1 , 2 , 3 ] , 'object' , [ 1 , 2 , 3 ] ] ,
507+ // null collapses to '' through yml round-trip; the inference rule still treats it as
508+ // string (no dataType). undefined behaves the same way.
509+ [ 'null (→ string, empty)' , null , undefined , '' ]
510+ ] ) ( 'script writes %s → on-disk dataType is %s' , ( _label , value , expectedDataType , expectedValue ) => {
511+ const filePath = writeFile ( 'inference.yml' ,
512+ 'name: t\nvariables:\n - name: v\n value: original\n'
513+ ) ;
514+ persistVariableUpdates (
515+ { envVariables : { v : value } } ,
516+ { envFile : { path : filePath , format : 'yml' } }
517+ ) ;
518+ const reparsed = parseEnvironment ( fs . readFileSync ( filePath , 'utf8' ) , { format : 'yml' } ) ;
519+ expect ( reparsed . variables [ 0 ] . value ) . toEqual ( expectedValue ) ;
520+ if ( expectedDataType === undefined ) {
521+ expect ( reparsed . variables [ 0 ] . dataType ) . toBeUndefined ( ) ;
522+ } else {
523+ expect ( reparsed . variables [ 0 ] . dataType ) . toBe ( expectedDataType ) ;
524+ }
525+ } ) ;
526+ } ) ;
527+
528+ // Cross-type transitions: the rule above means inference IGNORES the existing dataType on
529+ // disk. Whatever type the script writes is what lands on disk. This matters because a
530+ // user-written `dataType: number` annotation can be silently dropped (or flipped to another
531+ // type) the first time a script writes a different-typed value to that key.
532+ describe ( 'typed-value inference: cross-type transitions ignore the existing on-disk dataType' , ( ) => {
533+ const { parseEnvironment, stringifyEnvironment } = require ( '@usebruno/filestore' ) ;
534+
535+ const seedYmlEnv = ( seedVar ) => stringifyEnvironment (
536+ {
537+ name : 't' ,
538+ variables : [ { name : 'v' , enabled : true , secret : false , ...seedVar } ]
539+ } ,
540+ { format : 'yml' }
541+ ) ;
542+
543+ // Tuple shape: [ label, seed, scriptValue, expected ]
544+ // - label : human-readable name interpolated into the test title
545+ // - seed : { value, dataType? } — variable's initial on-disk state.
546+ // Omit dataType to seed a plain string.
547+ // - scriptValue : the value `bru.setEnvVar('v', X)` puts in the env map after parse
548+ // - expected : { value, dataType } end state on disk after persistVariableUpdates;
549+ // `dataType: undefined` asserts the field is absent on disk
550+ it . each ( [
551+ [ 'number → boolean (annotation flips)' , { value : 42 , dataType : 'number' } , true , { value : true , dataType : 'boolean' } ] ,
552+ [ 'number → object' , { value : 42 , dataType : 'number' } , { x : 1 } , { value : { x : 1 } , dataType : 'object' } ] ,
553+ [ 'boolean → number' , { value : true , dataType : 'boolean' } , 99 , { value : 99 , dataType : 'number' } ] ,
554+ [ 'object → string (annotation dropped)' , { value : { port : 3000 } , dataType : 'object' } , 'now-a-string' , { value : 'now-a-string' , dataType : undefined } ] ,
555+ [ 'string → number (annotation added)' , { value : 'was-a-string' } , 42 , { value : 42 , dataType : 'number' } ]
556+ ] ) ( '%s' , ( _label , seed , scriptValue , expected ) => {
557+ const filePath = writeFile ( 'xform.yml' , seedYmlEnv ( seed ) ) ;
558+ persistVariableUpdates (
559+ { envVariables : { v : scriptValue } } ,
560+ { envFile : { path : filePath , format : 'yml' } }
561+ ) ;
562+ const reparsed = parseEnvironment ( fs . readFileSync ( filePath , 'utf8' ) , { format : 'yml' } ) ;
563+ const entry = reparsed . variables [ 0 ] ;
564+ expect ( entry . value ) . toEqual ( expected . value ) ;
565+ if ( expected . dataType === undefined ) {
566+ expect ( entry . dataType ) . toBeUndefined ( ) ;
567+ } else {
568+ expect ( entry . dataType ) . toBe ( expected . dataType ) ;
569+ }
570+ } ) ;
571+ } ) ;
572+
484573describe ( 'script-driven typed vars: disk content has the right dataType annotations' , ( ) => {
485574 // The .bru serializer emits `@number\n port: 3000` (annotation on its own line) — see
486575 // packages/bruno-lang/v2/src/utils.js serializeAnnotations + serializeVar.
0 commit comments