not->toBeNull() ->and($result['content'])->toBe('API_URL:-${SERVICE_URL_YOLO}/api'); $split = splitOnOperatorOutsideNested($result['content']); expect($split)->not->toBeNull() ->and($split['variable'])->toBe('API_URL') ->and($split['operator'])->toBe(':-') ->and($split['default'])->toBe('${SERVICE_URL_YOLO}/api'); }); test('replaceVariables correctly extracts nested variable content', function () { // Before the fix, this would incorrectly extract only up to the first closing brace $result = replaceVariables('${API_URL:-${SERVICE_URL_YOLO}/api}'); // Should extract the full content, not just "${API_URL:-${SERVICE_URL_YOLO" expect($result->value())->toBe('API_URL:-${SERVICE_URL_YOLO}/api') ->and($result->value())->not->toBe('API_URL:-${SERVICE_URL_YOLO'); // Not truncated }); test('nested defaults with path concatenation work', function () { $input = '${REDIS_URL:-${SERVICE_URL_REDIS}/db/0}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['variable'])->toBe('REDIS_URL') ->and($split['default'])->toBe('${SERVICE_URL_REDIS}/db/0'); }); test('deeply nested variables are handled', function () { // Three levels of nesting $input = '${A:-${B:-${C}}}'; $result = extractBalancedBraceContent($input, 0); expect($result['content'])->toBe('A:-${B:-${C}}'); $split = splitOnOperatorOutsideNested($result['content']); expect($split['variable'])->toBe('A') ->and($split['default'])->toBe('${B:-${C}}'); }); test('multiple nested variables in default value', function () { // Default value contains multiple variable references $input = '${API:-${SERVICE_URL}:${SERVICE_PORT}/api}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['variable'])->toBe('API') ->and($split['default'])->toBe('${SERVICE_URL}:${SERVICE_PORT}/api'); }); test('nested variables with different operators', function () { // Nested variable uses different operator $input = '${API_URL:-${SERVICE_URL?error message}/api}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['variable'])->toBe('API_URL') ->and($split['operator'])->toBe(':-') ->and($split['default'])->toBe('${SERVICE_URL?error message}/api'); }); test('backward compatibility with simple variables', function () { // Simple variable without nesting should still work $input = '${VAR}'; $result = replaceVariables($input); expect($result->value())->toBe('VAR'); }); test('backward compatibility with single-level defaults', function () { // Single-level default without nesting $input = '${VAR:-default_value}'; $result = replaceVariables($input); expect($result->value())->toBe('VAR:-default_value'); $split = splitOnOperatorOutsideNested($result->value()); expect($split['variable'])->toBe('VAR') ->and($split['default'])->toBe('default_value'); }); test('backward compatibility with dash operator', function () { $input = '${VAR-default}'; $result = replaceVariables($input); $split = splitOnOperatorOutsideNested($result->value()); expect($split['operator'])->toBe('-'); }); test('backward compatibility with colon question operator', function () { $input = '${VAR:?error message}'; $result = replaceVariables($input); $split = splitOnOperatorOutsideNested($result->value()); expect($split['operator'])->toBe(':?') ->and($split['default'])->toBe('error message'); }); test('backward compatibility with question operator', function () { $input = '${VAR?error}'; $result = replaceVariables($input); $split = splitOnOperatorOutsideNested($result->value()); expect($split['operator'])->toBe('?') ->and($split['default'])->toBe('error'); }); test('SERVICE_URL magic variables in nested defaults', function () { // Real-world scenario: SERVICE_URL_* magic variable used in nested default $input = '${DATABASE_URL:-${SERVICE_URL_POSTGRES}/mydb}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['variable'])->toBe('DATABASE_URL') ->and($split['default'])->toBe('${SERVICE_URL_POSTGRES}/mydb'); // Extract the nested SERVICE_URL variable $nestedResult = extractBalancedBraceContent($split['default'], 0); expect($nestedResult['content'])->toBe('SERVICE_URL_POSTGRES'); }); test('SERVICE_FQDN magic variables in nested defaults', function () { $input = '${API_HOST:-${SERVICE_FQDN_API}}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['default'])->toBe('${SERVICE_FQDN_API}'); $nestedResult = extractBalancedBraceContent($split['default'], 0); expect($nestedResult['content'])->toBe('SERVICE_FQDN_API'); }); test('complex real-world example', function () { // Complex real-world scenario from the bug report $input = '${API_URL:-${SERVICE_URL_YOLO}/api}'; // Step 1: Extract outer variable content $result = extractBalancedBraceContent($input, 0); expect($result['content'])->toBe('API_URL:-${SERVICE_URL_YOLO}/api'); // Step 2: Split on operator $split = splitOnOperatorOutsideNested($result['content']); expect($split['variable'])->toBe('API_URL'); expect($split['operator'])->toBe(':-'); expect($split['default'])->toBe('${SERVICE_URL_YOLO}/api'); // Step 3: Extract nested variable $nestedResult = extractBalancedBraceContent($split['default'], 0); expect($nestedResult['content'])->toBe('SERVICE_URL_YOLO'); // This verifies that: // 1. API_URL should be created with value "${SERVICE_URL_YOLO}/api" // 2. SERVICE_URL_YOLO should be recognized and created as magic variable }); test('empty nested default values', function () { $input = '${VAR:-${NESTED:-}}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['default'])->toBe('${NESTED:-}'); $nestedResult = extractBalancedBraceContent($split['default'], 0); $nestedSplit = splitOnOperatorOutsideNested($nestedResult['content']); expect($nestedSplit['default'])->toBe(''); }); test('nested variables with complex paths', function () { $input = '${CONFIG_URL:-${SERVICE_URL_CONFIG}/v2/config.json}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); expect($split['default'])->toBe('${SERVICE_URL_CONFIG}/v2/config.json'); }); test('operator precedence with nesting', function () { // The first :- at depth 0 should be used, not the one inside nested braces $input = '${A:-${B:-default}}'; $result = extractBalancedBraceContent($input, 0); $split = splitOnOperatorOutsideNested($result['content']); // Should split on first :- (at depth 0) expect($split['variable'])->toBe('A') ->and($split['operator'])->toBe(':-') ->and($split['default'])->toBe('${B:-default}'); // Not split here });