diff --git a/src/services/mcp/envExpansion.test.ts b/src/services/mcp/envExpansion.test.ts new file mode 100644 index 0000000000..b47e853591 --- /dev/null +++ b/src/services/mcp/envExpansion.test.ts @@ -0,0 +1,49 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { expandEnvVarsInString } from './envExpansion.js' + +describe('expandEnvVarsInString', () => { + const saved: Record = {} + const keys = ['ENVEXP_SET', 'ENVEXP_UNSET'] + + beforeEach(() => { + for (const k of keys) saved[k] = process.env[k] + process.env.ENVEXP_SET = 'real' + delete process.env.ENVEXP_UNSET + }) + afterEach(() => { + for (const k of keys) { + if (saved[k] === undefined) delete process.env[k] + else process.env[k] = saved[k] + } + }) + + test('preserves a default value that itself contains ":-"', () => { + // JS String.split(sep, 2) would discard everything after the second ":-". + // bash `${VAR:-a:-b}` yields "a:-b"; the expansion must match. + expect( + expandEnvVarsInString('${ENVEXP_UNSET:-a:-b}').expanded, + ).toBe('a:-b') + }) + + test('expands a set variable and reports no missing vars', () => { + const r = expandEnvVarsInString('${ENVEXP_SET:-fallback}') + expect(r.expanded).toBe('real') + expect(r.missingVars).toEqual([]) + }) + + test('uses the default when the variable is unset', () => { + expect(expandEnvVarsInString('${ENVEXP_UNSET:-fallback}').expanded).toBe( + 'fallback', + ) + }) + + test('supports an empty default', () => { + expect(expandEnvVarsInString('${ENVEXP_UNSET:-}').expanded).toBe('') + }) + + test('reports a missing variable with no default', () => { + const r = expandEnvVarsInString('${ENVEXP_UNSET}') + expect(r.expanded).toBe('${ENVEXP_UNSET}') + expect(r.missingVars).toEqual(['ENVEXP_UNSET']) + }) +}) diff --git a/src/services/mcp/envExpansion.ts b/src/services/mcp/envExpansion.ts index 49255e5c39..a0d941d2c8 100644 --- a/src/services/mcp/envExpansion.ts +++ b/src/services/mcp/envExpansion.ts @@ -14,8 +14,17 @@ export function expandEnvVarsInString(value: string): { const missingVars: string[] = [] const expanded = value.replace(/\$\{([^}]+)\}/g, (match, varContent) => { - // Split on :- to support default values (limit to 2 parts to preserve :- in defaults) - const [varName, defaultValue] = varContent.split(':-', 2) + // Split on the FIRST ':-' to support ${VAR:-default} default values. Note + // String.split(sep, limit) caps the array length and discards the + // remainder — it does not glue the tail back on like a maxsplit — so + // `split(':-', 2)` would truncate a default that itself contains ':-' + // (e.g. ${VAR:-a:-b} -> "a" instead of "a:-b"). Slice at the first ':-' + // instead so any later ':-' stays in the default, matching bash. + const sepIndex = varContent.indexOf(':-') + const varName = + sepIndex === -1 ? varContent : varContent.slice(0, sepIndex) + const defaultValue = + sepIndex === -1 ? undefined : varContent.slice(sepIndex + 2) const envValue = process.env[varName] if (envValue !== undefined) {