Skip to content

Commit babcebc

Browse files
committed
test(env-file): enhance JSON environment variable persistence tests
- Added a new test to verify the preservation of native typed values in JSON environment files when unrelated keys are touched during script execution. - Ensured that typed values maintain their original types and are auto-annotated with the correct dataType, validating the integrity of the environment variable persistence process. - Expanded the typed-value inference tests to cover cross-type transitions, confirming that existing dataType annotations are ignored when new values are written.
1 parent bf6e13a commit babcebc

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

packages/bruno-cli/tests/integration/run-typed-persistence.spec.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,83 @@ script:post-response {
434434
expect(written).not.toMatch(/name:\s*host[\s\S]*?type:\s*string/);
435435
}, 60_000);
436436

437+
// JSON natively supports typed values (`"value": 42`, `"value": true`, `"value": {…}`),
438+
// so a JSON env file can seed with real JS types directly — no `dataType` tag needed.
439+
// The script then touches an UNRELATED key, which forces the runtime to echo back the
440+
// full env map. Every seeded typed value must survive that echo intact — same type, same
441+
// value, and `dataType` gets auto-annotated to match the JS type.
442+
it('preserves pre-existing native typed values in --env-file JSON when script touches unrelated keys', async () => {
443+
writeFixtureFile(
444+
path.join(tmpDir, 'bruno.json'),
445+
JSON.stringify({ version: '1', name: 'json-types-collection', type: 'collection' }, null, 2) + '\n'
446+
);
447+
writeFixtureFile(
448+
path.join(tmpDir, 'collection.bru'),
449+
'meta {\n name: json-types-collection\n seq: 1\n}\n'
450+
);
451+
writeFixtureFile(
452+
path.join(tmpDir, 'External.json'),
453+
JSON.stringify({
454+
name: 'External',
455+
variables: [
456+
{ name: 'host', value: baseUrl },
457+
// Native typed values — no dataType tag needed; JSON.parse keeps the JS type.
458+
{ name: 'seedNum', value: 42 },
459+
{ name: 'seedBool', value: true },
460+
{ name: 'seedObj', value: { region: 'eu', port: 3000 } },
461+
{ name: 'seedArr', value: [1, 2, 3] }
462+
]
463+
}, null, 2) + '\n'
464+
);
465+
writeFixtureFile(
466+
path.join(tmpDir, 'touch-unrelated.bru'),
467+
`meta {
468+
name: touch-unrelated
469+
type: http
470+
seq: 1
471+
}
472+
473+
get {
474+
url: {{host}}/ping
475+
body: none
476+
auth: none
477+
}
478+
479+
script:post-response {
480+
bru.setEnvVar("trigger", "x");
481+
}
482+
`
483+
);
484+
485+
const result = await runCli([
486+
'run', 'touch-unrelated.bru',
487+
'--env-file', 'External.json',
488+
'--sandbox', 'developer',
489+
'--noproxy'
490+
]);
491+
492+
if (result.code !== 0) {
493+
throw new Error(
494+
`CLI exited with code ${result.code}.\n--- stdout ---\n${result.stdout}\n--- stderr ---\n${result.stderr}`
495+
);
496+
}
497+
498+
const written = JSON.parse(fs.readFileSync(path.join(tmpDir, 'External.json'), 'utf8'));
499+
const byName = Object.fromEntries(written.variables.map((v) => [v.name, v]));
500+
// Native types preserved AND auto-annotated with dataType matching the JS type.
501+
expect(byName.seedNum).toMatchObject({ value: 42, dataType: 'number' });
502+
expect(byName.seedBool).toMatchObject({ value: true, dataType: 'boolean' });
503+
expect(byName.seedObj).toMatchObject({ value: { region: 'eu', port: 3000 }, dataType: 'object' });
504+
// Arrays are `typeof === 'object'` in JS, so they get dataType: 'object'.
505+
expect(byName.seedArr).toMatchObject({ value: [1, 2, 3], dataType: 'object' });
506+
// String stays string — no dataType added.
507+
expect(byName.host.value).toBe(baseUrl);
508+
expect(byName.host.dataType).toBeUndefined();
509+
// The trigger key the script wrote is also there.
510+
expect(byName.trigger).toMatchObject({ value: 'x' });
511+
expect(byName.trigger.dataType).toBeUndefined();
512+
}, 60_000);
513+
437514
it('persists typed env vars to a --env-file .bru file with @dataType annotations', async () => {
438515
writeFixtureFile(
439516
path.join(tmpDir, 'bruno.json'),

packages/bruno-cli/tests/utils/persist-variables.spec.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
484573
describe('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

Comments
 (0)