diff --git a/core/wren/src/wren/context.py b/core/wren/src/wren/context.py index 175ffc671..db07901c8 100644 --- a/core/wren/src/wren/context.py +++ b/core/wren/src/wren/context.py @@ -76,9 +76,20 @@ See https://docs.getwren.ai/oss/engine/get_started/installation for full setup. def _snake_to_camel(name: str) -> str: - """Convert snake_case to camelCase.""" - parts = name.split("_") - return parts[0] + "".join(w.capitalize() for w in parts[1:]) + """Convert snake_case to camelCase, preserving any leading underscores. + + Keys like ``_instructions`` are sentinel carriers consumed downstream + under that exact name. The naive ``"_instructions".split("_")`` yields + ``["", "instructions"]`` → ``"Instructions"`` — the leading underscore is + lost and the first real word is capitalized, mangling the key. Strip and + re-attach the leading underscore run so it round-trips. + """ + stripped = name.lstrip("_") + prefix = name[: len(name) - len(stripped)] + parts = stripped.split("_") + if not stripped: + return name + return prefix + parts[0] + "".join(w.capitalize() for w in parts[1:]) def _convert_keys(obj: Any) -> Any: diff --git a/core/wren/tests/unit/test_context.py b/core/wren/tests/unit/test_context.py index 9536f4a31..3bef80df5 100644 --- a/core/wren/tests/unit/test_context.py +++ b/core/wren/tests/unit/test_context.py @@ -43,6 +43,24 @@ def test_snake_to_camel(): assert _snake_to_camel("name") == "name" +def test_snake_to_camel_preserves_leading_underscore(): + # Sentinel keys like `_instructions` must keep their leading underscore + # and not capitalize the first real word (the naive split mangled this + # into "Instructions"). + assert _snake_to_camel("_instructions") == "_instructions" + assert _snake_to_camel("_dbt_tests") == "_dbtTests" + assert _snake_to_camel("__double") == "__double" + + +def test_convert_keys_does_not_mangle_nested_instructions(): + # A nested `_instructions` carrier must survive the camelCase pass intact; + # the top-level pop workarounds elsewhere don't protect nested ones. + obj = {"models": [{"name": "m", "_instructions": "do x"}]} + result = _convert_keys(obj) + assert result["models"][0]["_instructions"] == "do x" + assert "Instructions" not in result["models"][0] + + def test_convert_keys_nested(): obj = { "table_reference": {"catalog": "c", "schema_name": "s"},