From 5e78e33324b087a28e55b0f0c9a1381f92986917 Mon Sep 17 00:00:00 2001
From: "coderabbitai[bot]"
 <136622811+coderabbitai[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 00:03:24 +0000
Subject: [PATCH] CodeRabbit Generated Unit Tests: Add unit tests

---
 tests/__init__.py                             |   0
 tests/__pycache__/__init__.cpython-311.pyc    |   0
 .../conftest.cpython-311-pytest-9.0.3.pyc     |   0
 ...tin_functions.cpython-311-pytest-9.0.3.pyc |   0
 .../test_common.cpython-311-pytest-9.0.3.pyc  |   0
 ...e_interpreter.cpython-311-pytest-9.0.3.pyc |   0
 ...compile_types.cpython-311-pytest-9.0.3.pyc |   0
 ...test_compiler.cpython-311-pytest-9.0.3.pyc |   0
 ...ersion_config.cpython-311-pytest-9.0.3.pyc |   0
 tests/conftest.py                             |  46 ++
 tests/test_builtin_functions.py               | 315 +++++++++++
 tests/test_common.py                          |  90 ++++
 tests/test_compile_interpreter.py             | 247 +++++++++
 tests/test_compile_types.py                   | 341 ++++++++++++
 tests/test_compiler.py                        | 221 ++++++++
 tests/test_version_config.py                  | 510 ++++++++++++++++++
 16 files changed, 1770 insertions(+)
 create mode 100644 tests/__init__.py
 create mode 100644 tests/__pycache__/__init__.cpython-311.pyc
 create mode 100644 tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/__pycache__/test_builtin_functions.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/__pycache__/test_common.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/__pycache__/test_compile_interpreter.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/__pycache__/test_compile_types.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/__pycache__/test_compiler.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/__pycache__/test_version_config.cpython-311-pytest-9.0.3.pyc
 create mode 100644 tests/conftest.py
 create mode 100644 tests/test_builtin_functions.py
 create mode 100644 tests/test_common.py
 create mode 100644 tests/test_compile_interpreter.py
 create mode 100644 tests/test_compile_types.py
 create mode 100644 tests/test_compiler.py
 create mode 100644 tests/test_version_config.py

diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/test_builtin_functions.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_builtin_functions.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/test_common.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_common.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/test_compile_interpreter.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_compile_interpreter.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/test_compile_types.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_compile_types.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/test_compiler.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_compiler.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/__pycache__/test_version_config.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_version_config.cpython-311-pytest-9.0.3.pyc
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..af1b0c7
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,46 @@
+"""
+Shared pytest fixtures for Minecraft-Script tests.
+"""
+import pytest
+import minecraft_script.version_config as vc
+
+
+@pytest.fixture(autouse=True)
+def reset_version_context():
+    """Clear the global version context and profile cache before/after each test."""
+    vc.clear_version_context()
+    vc._profile_cache.clear()
+    yield
+    vc.clear_version_context()
+    vc._profile_cache.clear()
+
+
+@pytest.fixture
+def profile_1204():
+    """Return the real 1.20.4 version profile."""
+    return vc.load_version_profile("1.20.4")
+
+
+@pytest.fixture
+def version_ctx_1204():
+    """Initialize and return a VersionContext for datapack 'test_pack' using 1.20.4."""
+    return vc.init_version_context("test_pack")
+
+
+@pytest.fixture
+def mock_profile():
+    """A minimal profile dict for unit-testing VersionRenderer in isolation."""
+    return {
+        "minecraft_version": "test",
+        "pack_format": 99,
+        "paths": {"function_dir": "functions", "function_tag_dir": "functions"},
+        "constants": {"MY_CONST": "hello"},
+        "templates": {
+            "simple": "just text",
+            "with_var": "value={{myVar}}",
+            "multiline": "line1\nline2\nline3",
+            "with_const": "const={{MY_CONST}}",
+            "conditional": "{{#if flag}}yes{{else}}no{{/if}}",
+            "empty": "",
+        },
+    }
\ No newline at end of file
diff --git a/tests/test_builtin_functions.py b/tests/test_builtin_functions.py
new file mode 100644
index 0000000..22b99bb
--- /dev/null
+++ b/tests/test_builtin_functions.py
@@ -0,0 +1,315 @@
+"""
+Tests for minecraft_script/compiler/builtin_functions.py
+
+Covers the version-context-based rendering for: log, command, give_item,
+set_block, concatenate, append - focusing on command generation correctness
+introduced in this PR.
+"""
+import pytest
+from unittest.mock import MagicMock, patch
+
+from minecraft_script.version_config import init_version_context
+from minecraft_script.compiler.compile_types import (
+    MCSNull, MCSNumber, MCSString, MCSList, MCSFunction, MCSBoolean
+)
+from minecraft_script.compiler.builtin_functions import (
+    log, command, give_item, set_block, concatenate, append
+)
+
+
+# ---------------------------------------------------------------------------
+# Fixtures
+# ---------------------------------------------------------------------------
+
+@pytest.fixture
+def ctx(version_ctx_1204):
+    from minecraft_script.compiler.compile_interpreter import CompileContext
+    return CompileContext("test_fn", top_level=True)
+
+
+@pytest.fixture
+def mock_interp(ctx):
+    interp = MagicMock()
+    interp.datapack_id = "test_pack"
+    interp.click_item_lookup = {}
+    return interp
+
+
+# ---------------------------------------------------------------------------
+# log()
+# ---------------------------------------------------------------------------
+
+class TestLog:
+    def test_returns_null(self, mock_interp, ctx):
+        arg = MCSNumber(ctx)
+        cmds, ret = log(mock_interp, [arg], ctx)
+        assert isinstance(ret, MCSNull)
+
+    def test_single_arg_produces_one_command(self, mock_interp, ctx):
+        arg = MCSNumber(ctx)
+        cmds, _ = log(mock_interp, [arg], ctx)
+        assert len(cmds) == 1
+
+    def test_command_calls_builtins_log(self, mock_interp, ctx):
+        arg = MCSNumber(ctx)
+        cmds, _ = log(mock_interp, [arg], ctx)
+        assert "builtins/log" in cmds[0]
+
+    def test_storage_suffix_contains_arg_storage(self, mock_interp, ctx):
+        arg = MCSNumber(ctx)
+        cmds, _ = log(mock_interp, [arg], ctx)
+        assert arg.get_storage() in cmds[0]
+
+    def test_storage_suffix_contains_arg_nbt(self, mock_interp, ctx):
+        arg = MCSNumber(ctx)
+        cmds, _ = log(mock_interp, [arg], ctx)
+        assert arg.get_nbt() in cmds[0]
+
+    def test_no_args_pads_to_five_empty(self, mock_interp, ctx):
+        cmds, _ = log(mock_interp, [], ctx)
+        # All 5 slots should have empty storage/nbt
+        assert '"s0": ""' in cmds[0]
+        assert '"n4": "none"' in cmds[0]
+
+    def test_five_args_no_padding(self, mock_interp, ctx):
+        args = [MCSNumber(ctx) for _ in range(5)]
+        cmds, _ = log(mock_interp, args, ctx)
+        assert '"n4"' in cmds[0]
+        assert '"n5"' not in cmds[0]
+
+    def test_datapack_id_in_command(self, mock_interp, ctx):
+        arg = MCSString(ctx)
+        cmds, _ = log(mock_interp, [arg], ctx)
+        assert "test_pack" in cmds[0]
+
+
+# ---------------------------------------------------------------------------
+# command()
+# ---------------------------------------------------------------------------
+
+class TestCommand:
+    def test_returns_null(self, mock_interp, ctx):
+        arg = MCSString(ctx)
+        cmds, ret = command(mock_interp, [arg], ctx)
+        assert isinstance(ret, MCSNull)
+
+    def test_command_references_value_storage(self, mock_interp, ctx):
+        arg = MCSString(ctx)
+        cmds, _ = command(mock_interp, [arg], ctx)
+        full = "\n".join(cmds)
+        assert arg.get_storage() in full
+
+    def test_command_calls_builtins_command(self, mock_interp, ctx):
+        arg = MCSString(ctx)
+        cmds, _ = command(mock_interp, [arg], ctx)
+        full = "\n".join(cmds)
+        assert "builtins/command" in full
+
+    def test_command_uses_with_storage(self, mock_interp, ctx):
+        arg = MCSString(ctx)
+        cmds, _ = command(mock_interp, [arg], ctx)
+        full = "\n".join(cmds)
+        assert "with storage" in full
+
+    def test_command_produces_multiple_lines(self, mock_interp, ctx):
+        arg = MCSString(ctx)
+        cmds, _ = command(mock_interp, [arg], ctx)
+        assert len(cmds) >= 2
+
+
+# ---------------------------------------------------------------------------
+# give_item()
+# ---------------------------------------------------------------------------
+
+class TestGiveItem:
+    def test_returns_null(self, mock_interp, ctx):
+        item = MCSString(ctx)
+        cmds, ret = give_item(mock_interp, [item], ctx)
+        assert isinstance(ret, MCSNull)
+
+    def test_item_only_uses_default_components_and_count(self, mock_interp, ctx):
+        item = MCSString(ctx)
+        cmds, _ = give_item(mock_interp, [item], ctx)
+        full = "\n".join(cmds)
+        assert "current.components set value ''" in full
+        assert "current.count set value 1" in full
+
+    def test_with_components_uses_storage(self, mock_interp, ctx):
+        item = MCSString(ctx)
+        comps = MCSString(ctx)
+        cmds, _ = give_item(mock_interp, [item, comps], ctx)
+        full = "\n".join(cmds)
+        assert "current.components set from storage" in full
+        assert comps.get_storage() in full
+
+    def test_with_count_uses_storage(self, mock_interp, ctx):
+        item = MCSString(ctx)
+        comps = MCSString(ctx)
+        count = MCSNumber(ctx)
+        cmds, _ = give_item(mock_interp, [item, comps, count], ctx)
+        full = "\n".join(cmds)
+        assert "current.count set from storage" in full
+        assert count.get_storage() in full
+
+    def test_calls_builtins_give_item(self, mock_interp, ctx):
+        item = MCSString(ctx)
+        cmds, _ = give_item(mock_interp, [item], ctx)
+        full = "\n".join(cmds)
+        assert "builtins/give_item" in full
+
+    def test_item_storage_referenced(self, mock_interp, ctx):
+        item = MCSString(ctx)
+        cmds, _ = give_item(mock_interp, [item], ctx)
+        full = "\n".join(cmds)
+        assert item.get_storage() in full
+        assert item.get_nbt() in full
+
+
+# ---------------------------------------------------------------------------
+# set_block()
+# ---------------------------------------------------------------------------
+
+class TestSetBlock:
+    def test_returns_null(self, mock_interp, ctx):
+        x = MCSNumber(ctx)
+        y = MCSNumber(ctx)
+        z = MCSNumber(ctx)
+        block = MCSString(ctx)
+        cmds, ret = set_block(mock_interp, [x, y, z, block], ctx)
+        assert isinstance(ret, MCSNull)
+
+    def test_setup_contains_x_y_z_block(self, mock_interp, ctx):
+        x = MCSNumber(ctx)
+        y = MCSNumber(ctx)
+        z = MCSNumber(ctx)
+        block = MCSString(ctx)
+        cmds, _ = set_block(mock_interp, [x, y, z, block], ctx)
+        full = "\n".join(cmds)
+        assert "current.x set from storage" in full
+        assert "current.y set from storage" in full
+        assert "current.z set from storage" in full
+        assert "current.block set from storage" in full
+
+    def test_calls_builtins_set_block(self, mock_interp, ctx):
+        x = MCSNumber(ctx)
+        y = MCSNumber(ctx)
+        z = MCSNumber(ctx)
+        block = MCSString(ctx)
+        cmds, _ = set_block(mock_interp, [x, y, z, block], ctx)
+        full = "\n".join(cmds)
+        assert "builtins/set_block" in full
+
+    def test_each_coord_storage_is_referenced(self, mock_interp, ctx):
+        x = MCSNumber(ctx)
+        y = MCSNumber(ctx)
+        z = MCSNumber(ctx)
+        block = MCSString(ctx)
+        cmds, _ = set_block(mock_interp, [x, y, z, block], ctx)
+        full = "\n".join(cmds)
+        assert x.get_nbt() in full
+        assert y.get_nbt() in full
+        assert z.get_nbt() in full
+        assert block.get_nbt() in full
+
+
+# ---------------------------------------------------------------------------
+# concatenate()
+# ---------------------------------------------------------------------------
+
+class TestConcatenate:
+    def test_returns_mcs_string(self, mock_interp, ctx):
+        s1 = MCSString(ctx)
+        s2 = MCSString(ctx)
+        cmds, ret = concatenate(mock_interp, [s1, s2], ctx)
+        assert isinstance(ret, MCSString)
+
+    def test_setup_references_both_strings(self, mock_interp, ctx):
+        s1 = MCSString(ctx)
+        s2 = MCSString(ctx)
+        cmds, _ = concatenate(mock_interp, [s1, s2], ctx)
+        full = "\n".join(cmds)
+        assert s1.get_storage() in full
+        assert s2.get_storage() in full
+
+    def test_setup_calls_macro_function(self, mock_interp, ctx):
+        s1 = MCSString(ctx)
+        s2 = MCSString(ctx)
+        cmds, _ = concatenate(mock_interp, [s1, s2], ctx)
+        full = "\n".join(cmds)
+        # macro path should be referenced with "with storage"
+        assert "with storage" in full
+
+    def test_adds_macro_command_to_interpreter(self, mock_interp, ctx):
+        s1 = MCSString(ctx)
+        s2 = MCSString(ctx)
+        concatenate(mock_interp, [s1, s2], ctx)
+        mock_interp.add_command.assert_called_once()
+        # Macro should contain the string_1 / string_2 macro expansions
+        call_args = mock_interp.add_command.call_args
+        macro_cmd = call_args[0][1]
+        assert "$(string_1)$(string_2)" in macro_cmd
+
+    def test_macro_references_output_storage(self, mock_interp, ctx):
+        s1 = MCSString(ctx)
+        s2 = MCSString(ctx)
+        cmds, result_str = concatenate(mock_interp, [s1, s2], ctx)
+        call_args = mock_interp.add_command.call_args
+        macro_cmd = call_args[0][1]
+        assert result_str.get_storage() in macro_cmd
+        assert result_str.get_nbt() in macro_cmd
+
+
+# ---------------------------------------------------------------------------
+# append()
+# ---------------------------------------------------------------------------
+
+class TestAppend:
+    def test_returns_null(self, mock_interp, ctx):
+        lst = MCSList(ctx)
+        val = MCSNumber(ctx)
+        cmds, ret = append(mock_interp, [lst, val], ctx)
+        assert isinstance(ret, MCSNull)
+
+    def test_setup_references_list(self, mock_interp, ctx):
+        lst = MCSList(ctx)
+        val = MCSNumber(ctx)
+        cmds, _ = append(mock_interp, [lst, val], ctx)
+        full = "\n".join(cmds)
+        assert lst.get_storage() in full
+        assert lst.get_nbt() in full
+
+    def test_setup_increments_length(self, mock_interp, ctx):
+        lst = MCSList(ctx)
+        val = MCSNumber(ctx)
+        cmds, _ = append(mock_interp, [lst, val], ctx)
+        full = "\n".join(cmds)
+        assert ".length" in full
+        # Should increment via scoreboard operation
+        assert "mcs_math" in full
+
+    def test_adds_macro_command_to_interpreter(self, mock_interp, ctx):
+        lst = MCSList(ctx)
+        val = MCSNumber(ctx)
+        append(mock_interp, [lst, val], ctx)
+        mock_interp.add_command.assert_called_once()
+        call_args = mock_interp.add_command.call_args
+        macro_cmd = call_args[0][1]
+        assert "$(index)" in macro_cmd
+
+    def test_macro_references_value_storage(self, mock_interp, ctx):
+        lst = MCSList(ctx)
+        val = MCSNumber(ctx)
+        append(mock_interp, [lst, val], ctx)
+        call_args = mock_interp.add_command.call_args
+        macro_cmd = call_args[0][1]
+        assert val.get_storage() in macro_cmd
+        assert val.get_nbt() in macro_cmd
+
+    def test_setup_calls_macro_with_index(self, mock_interp, ctx):
+        lst = MCSList(ctx)
+        val = MCSString(ctx)
+        cmds, _ = append(mock_interp, [lst, val], ctx)
+        full = "\n".join(cmds)
+        # Should load current index from list.length
+        assert "current.index set from storage" in full
+        assert lst.get_nbt() + ".length" in full
\ No newline at end of file
diff --git a/tests/test_common.py b/tests/test_common.py
new file mode 100644
index 0000000..cec8ea3
--- /dev/null
+++ b/tests/test_common.py
@@ -0,0 +1,90 @@
+"""
+Tests for minecraft_script/common.py
+
+Covers: module_folder (pathlib-based detection), generate_uuid, COMMON_CONFIG loading.
+"""
+import os
+import re
+from pathlib import Path
+
+import pytest
+
+from minecraft_script.common import module_folder, generate_uuid, COMMON_CONFIG, version
+
+
+class TestModuleFolder:
+    def test_module_folder_is_string(self):
+        assert isinstance(module_folder, str)
+
+    def test_module_folder_ends_with_minecraft_script(self):
+        # The module lives in .../minecraft_script/
+        assert module_folder.endswith("minecraft_script")
+
+    def test_module_folder_is_absolute(self):
+        assert os.path.isabs(module_folder)
+
+    def test_module_folder_exists(self):
+        assert os.path.isdir(module_folder)
+
+    def test_module_folder_contains_config_json(self):
+        assert os.path.isfile(os.path.join(module_folder, "config.json"))
+
+    def test_module_folder_uses_pathlib(self):
+        # Verify the path resolution matches what Path would give
+        import minecraft_script.common as cm
+        expected = str(Path(cm.__file__).resolve().parent)
+        assert module_folder == expected
+
+    def test_module_folder_is_cross_platform(self):
+        # Should never contain raw backslashes on any platform
+        # (pathlib normalises separators)
+        assert "\\" not in module_folder or os.sep == "\\"
+
+
+class TestGenerateUuid:
+    def test_returns_string(self):
+        assert isinstance(generate_uuid(), str)
+
+    def test_returns_valid_uuid_format(self):
+        uuid = generate_uuid()
+        pattern = re.compile(
+            r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
+        )
+        assert pattern.match(uuid), f"UUID {uuid!r} does not match expected format"
+
+    def test_generates_unique_values(self):
+        uuids = {generate_uuid() for _ in range(100)}
+        assert len(uuids) == 100
+
+    def test_uuid_length(self):
+        # Standard UUID string: 32 hex chars + 4 dashes = 36 chars
+        assert len(generate_uuid()) == 36
+
+
+class TestCommonConfig:
+    def test_config_is_dict(self):
+        assert isinstance(COMMON_CONFIG, dict)
+
+    def test_config_has_minecraft_version(self):
+        assert "minecraft_version" in COMMON_CONFIG
+
+    def test_config_has_debug_comments(self):
+        assert "debug_comments" in COMMON_CONFIG
+
+    def test_config_has_verbose(self):
+        assert "verbose" in COMMON_CONFIG
+
+    def test_config_has_default_output_path(self):
+        assert "default_output_path" in COMMON_CONFIG
+
+    def test_minecraft_version_is_string(self):
+        assert isinstance(COMMON_CONFIG["minecraft_version"], str)
+
+
+class TestVersion:
+    def test_version_is_string(self):
+        assert isinstance(version, str)
+
+    def test_version_matches_semver_pattern(self):
+        pattern = re.compile(r'^\d+\.\d+\.\d+$')
+        assert pattern.match(version), f"Version {version!r} is not in semver format"
\ No newline at end of file
diff --git a/tests/test_compile_interpreter.py b/tests/test_compile_interpreter.py
new file mode 100644
index 0000000..e98c58b
--- /dev/null
+++ b/tests/test_compile_interpreter.py
@@ -0,0 +1,247 @@
+"""
+Tests for changed parts of minecraft_script/compiler/compile_interpreter.py
+
+Covers: add_comment (list->tuple fix), init_version_context lifecycle inside
+mcs_compile, CompileContext structure.
+"""
+import pytest
+from unittest.mock import patch
+
+import minecraft_script.version_config as vc
+from minecraft_script.common import COMMON_CONFIG
+from minecraft_script.compiler.compile_interpreter import (
+    add_comment, CompileContext, CompileCommands, CompileResult
+)
+
+
+# ---------------------------------------------------------------------------
+# Fixtures
+# ---------------------------------------------------------------------------
+
+@pytest.fixture
+def debug_on(version_ctx_1204):
+    """Ensure debug_comments is True."""
+    old = COMMON_CONFIG.get("debug_comments")
+    COMMON_CONFIG["debug_comments"] = True
+    yield
+    COMMON_CONFIG["debug_comments"] = old
+
+
+@pytest.fixture
+def debug_off(version_ctx_1204):
+    """Ensure debug_comments is False."""
+    old = COMMON_CONFIG.get("debug_comments")
+    COMMON_CONFIG["debug_comments"] = False
+    yield
+    COMMON_CONFIG["debug_comments"] = old
+
+
+# ---------------------------------------------------------------------------
+# add_comment
+# ---------------------------------------------------------------------------
+
+class TestAddComment:
+    def test_tuple_input_debug_on_prepends_comment(self, debug_on):
+        result = add_comment(("cmd1", "cmd2"), "my comment")
+        assert isinstance(result, tuple)
+        assert result[0] == "\n# my comment"
+        assert result[1] == "cmd1"
+        assert result[2] == "cmd2"
+
+    def test_list_input_debug_on_converts_to_tuple_and_prepends(self, debug_on):
+        result = add_comment(["cmd1", "cmd2"], "list comment")
+        assert isinstance(result, tuple)
+        assert result[0] == "\n# list comment"
+        assert "cmd1" in result
+        assert "cmd2" in result
+
+    def test_str_input_debug_on_prepends_comment(self, debug_on):
+        result = add_comment("single_cmd", "str comment")
+        assert isinstance(result, str)
+        assert "# str comment" in result
+        assert "single_cmd" in result
+
+    def test_tuple_input_debug_off_returns_unchanged(self, debug_off):
+        original = ("cmd1", "cmd2")
+        result = add_comment(original, "ignored")
+        assert result == original
+
+    def test_list_input_debug_off_converts_to_tuple(self, debug_off):
+        original = ["cmd1", "cmd2"]
+        result = add_comment(original, "ignored")
+        assert isinstance(result, tuple)
+        assert result == ("cmd1", "cmd2")
+
+    def test_str_input_debug_off_returns_unchanged(self, debug_off):
+        result = add_comment("single_cmd", "ignored")
+        assert result == "single_cmd"
+
+    def test_invalid_type_raises_value_error(self, debug_on):
+        with pytest.raises(ValueError, match="Commands have to be tuple, list, or str"):
+            add_comment(42, "comment")
+
+    def test_invalid_type_none_raises_value_error(self, debug_on):
+        with pytest.raises(ValueError):
+            add_comment(None, "comment")
+
+    def test_empty_tuple_debug_on(self, debug_on):
+        result = add_comment((), "empty")
+        assert isinstance(result, tuple)
+        assert result == ("\n# empty",)
+
+    def test_empty_list_debug_on(self, debug_on):
+        result = add_comment([], "empty list")
+        assert isinstance(result, tuple)
+        assert result[0] == "\n# empty list"
+
+    def test_empty_list_debug_off_returns_empty_tuple(self, debug_off):
+        result = add_comment([], "ignored")
+        assert result == ()
+
+
+# ---------------------------------------------------------------------------
+# CompileContext
+# ---------------------------------------------------------------------------
+
+class TestCompileContext:
+    def test_top_level_context_has_builtins(self, version_ctx_1204):
+        ctx = CompileContext("init", top_level=True)
+        # Check that builtin functions are loaded
+        assert ctx.symbols.symbols.get("log") is not None or ctx.get("log") is not None
+
+    def test_context_has_unique_uuid(self, version_ctx_1204):
+        ctx1 = CompileContext("fn1")
+        ctx2 = CompileContext("fn2")
+        assert ctx1.uuid != ctx2.uuid
+
+    def test_mcfunction_name_user_functions(self, version_ctx_1204):
+        ctx = CompileContext("my_fn")
+        assert ctx.mcfunction_name == "user_functions/my_fn"
+
+    def test_mcfunction_name_code_blocks(self, version_ctx_1204):
+        # No name -> code block
+        ctx = CompileContext()
+        assert ctx.mcfunction_name.startswith("code_blocks/cb_")
+
+    def test_child_context_has_parent_ref(self, version_ctx_1204):
+        parent = CompileContext("parent")
+        child = CompileContext(parent=parent)
+        assert child.parent is parent
+
+    def test_declare_and_get(self, version_ctx_1204):
+        from minecraft_script.compiler.compile_types import MCSNumber
+        ctx = CompileContext("test")
+        num = MCSNumber(ctx)
+        ctx.declare("myvar", num)
+        assert ctx.get("myvar") is num
+
+    def test_get_context_ownership_finds_self(self, version_ctx_1204):
+        from minecraft_script.compiler.compile_types import MCSNumber
+        ctx = CompileContext("owner")
+        num = MCSNumber(ctx)
+        ctx.declare("owned_var", num)
+        assert ctx.get_context_ownership("owned_var") is ctx
+
+    def test_get_context_ownership_traverses_parent(self, version_ctx_1204):
+        from minecraft_script.compiler.compile_types import MCSNumber
+        parent = CompileContext("parent")
+        child = CompileContext(parent=parent)
+        num = MCSNumber(parent)
+        parent.declare("parent_var", num)
+        assert child.get_context_ownership("parent_var") is parent
+
+    def test_undefined_var_raises_name_error(self, version_ctx_1204):
+        ctx = CompileContext("test")
+        with pytest.raises(NameError):
+            ctx.get_context_ownership("undefined_var")
+
+
+# ---------------------------------------------------------------------------
+# CompileCommands
+# ---------------------------------------------------------------------------
+
+class TestCompileCommands:
+    def test_add_command_and_get_file_content(self):
+        cmds = CompileCommands()
+        cmds.add_command("fn1", "cmd_a")
+        cmds.add_command("fn1", "cmd_b")
+        content = cmds.get_file_content("fn1")
+        assert "cmd_a" in content
+        assert "cmd_b" in content
+
+    def test_get_mcs_functions(self):
+        cmds = CompileCommands()
+        cmds.add_command("fn1", "cmd1")
+        cmds.add_command("fn2", "cmd2")
+        fns = cmds.get_mcs_functions()
+        assert "fn1" in fns
+        assert "fn2" in fns
+
+    def test_empty_function_returns_empty_string(self):
+        cmds = CompileCommands()
+        assert cmds.get_file_content("nonexistent") == ""
+
+    def test_commands_joined_with_newline(self):
+        cmds = CompileCommands()
+        cmds.add_command("fn", "line1")
+        cmds.add_command("fn", "line2")
+        content = cmds.get_file_content("fn")
+        assert content == "line1\nline2"
+
+
+# ---------------------------------------------------------------------------
+# CompileResult
+# ---------------------------------------------------------------------------
+
+class TestCompileResult:
+    def test_default_value_is_none(self):
+        result = CompileResult()
+        assert result.get_value() is None
+
+    def test_default_return_is_none(self):
+        result = CompileResult()
+        assert result.get_return() is None
+
+    def test_value_is_returned(self, version_ctx_1204):
+        from minecraft_script.compiler.compile_types import MCSNumber
+        ctx = CompileContext("fn")
+        num = MCSNumber(ctx)
+        result = CompileResult(value=num)
+        assert result.get_value() is num
+
+    def test_return_value_is_returned(self, version_ctx_1204):
+        from minecraft_script.compiler.compile_types import MCSString
+        ctx = CompileContext("fn")
+        s = MCSString(ctx)
+        result = CompileResult(return_value=s)
+        assert result.get_return() is s
+
+
+# ---------------------------------------------------------------------------
+# init_version_context in mcs_compile
+# ---------------------------------------------------------------------------
+
+class TestMcsCompileVersionContextLifecycle:
+    """Verify that mcs_compile initialises the version context with the datapack_id."""
+
+    def test_init_called_with_datapack_id(self):
+        from minecraft_script.compiler.compile_interpreter import mcs_compile
+        import tempfile
+        import minecraft_script.compiler.compile_interpreter as ci
+
+        captured = {}
+
+        original_init = vc.init_version_context
+        def fake_init(datapack_id):
+            captured["datapack_id"] = datapack_id
+            return original_init(datapack_id)
+
+        # Patch the name as it is imported inside compile_interpreter
+        with patch.object(ci, "init_version_context", side_effect=fake_init):
+            try:
+                with tempfile.TemporaryDirectory() as tmp:
+                    mcs_compile([], tmp, "my_datapack")
+            except Exception:
+                pass  # compilation may fail on empty AST - that's ok
+
+        assert captured.get("datapack_id") == "my_datapack"
\ No newline at end of file
diff --git a/tests/test_compile_types.py b/tests/test_compile_types.py
new file mode 100644
index 0000000..5646e60
--- /dev/null
+++ b/tests/test_compile_types.py
@@ -0,0 +1,341 @@
+"""
+Tests for minecraft_script/compiler/compile_types.py
+
+Covers: MCSObject, MCSVariable, MCSList, MCSNull, MCSNumber, MCSString,
+MCSBoolean, MCSUnknown, MCSFunction - focusing on the version-context-based
+command generation introduced in this PR.
+"""
+import pytest
+from unittest.mock import MagicMock
+
+import minecraft_script.version_config as vc
+from minecraft_script.version_config import init_version_context
+from minecraft_script.compiler.compile_types import (
+    MCSObject, MCSVariable, MCSList, MCSNull,
+    MCSNumber, MCSString, MCSBoolean, MCSUnknown,
+    MCSFunction,
+)
+
+
+# ---------------------------------------------------------------------------
+# Helpers / fixtures
+# ---------------------------------------------------------------------------
+
+@pytest.fixture
+def ctx(version_ctx_1204):
+    """Return a minimal CompileContext-like mock for use in type construction."""
+    from minecraft_script.compiler.compile_interpreter import CompileContext
+    return CompileContext("test_fn", top_level=True)
+
+
+@pytest.fixture
+def ctx2(version_ctx_1204):
+    """Return a second CompileContext for set_to_current_cmd tests."""
+    from minecraft_script.compiler.compile_interpreter import CompileContext
+    return CompileContext("test_fn2", top_level=True)
+
+
+# ---------------------------------------------------------------------------
+# MCSObject
+# ---------------------------------------------------------------------------
+
+class TestMCSObject:
+    def test_get_nbt_format(self, ctx):
+        obj = MCSObject(ctx, "number")
+        assert obj.get_nbt().startswith("number.")
+        assert len(obj.get_nbt()) > len("number.")
+
+    def test_get_storage_format(self, ctx):
+        obj = MCSObject(ctx, "string")
+        storage = obj.get_storage()
+        assert storage.startswith("mcs_")
+        assert ctx.uuid in storage
+
+    def test_save_to_storage_cmd_literal_42(self, ctx):
+        obj = MCSObject(ctx, "number")
+        cmd = obj.save_to_storage_cmd(42)
+        assert "data modify storage" in cmd
+        assert obj.get_storage() in cmd
+        assert obj.get_nbt() in cmd
+        assert "42" in cmd
+
+    def test_save_to_storage_cmd_literal_bytes(self, ctx):
+        obj = MCSObject(ctx, "string")
+        cmd = obj.save_to_storage_cmd("0b")
+        assert "set value" in cmd
+        assert "0b" in cmd
+
+    def test_delete_from_storage_cmd(self, ctx):
+        obj = MCSObject(ctx, "number")
+        cmd = obj.delete_from_storage_cmd()
+        assert "data remove storage" in cmd
+        assert obj.get_storage() in cmd
+        assert obj.get_nbt() in cmd
+
+    def test_set_to_current_cmd(self, ctx, ctx2):
+        obj = MCSObject(ctx, "string")
+        cmd = obj.set_to_current_cmd(ctx2)
+        assert "data modify storage" in cmd
+        assert f"mcs_{ctx2.uuid}" in cmd
+        assert "current" in cmd
+        assert obj.get_storage() in cmd
+        assert obj.get_nbt() in cmd
+
+    def test_each_object_has_unique_uuid(self, ctx):
+        obj1 = MCSObject(ctx, "number")
+        obj2 = MCSObject(ctx, "number")
+        assert obj1.uuid != obj2.uuid
+
+    def test_storage_compartment_in_nbt(self, ctx):
+        obj = MCSObject(ctx, "boolean")
+        assert "boolean" in obj.get_nbt()
+
+
+# ---------------------------------------------------------------------------
+# MCSVariable
+# ---------------------------------------------------------------------------
+
+class TestMCSVariable:
+    def test_get_nbt_includes_variable_prefix(self, ctx):
+        var = MCSVariable("x", ctx)
+        assert var.get_nbt() == "variable.x"
+
+    def test_get_storage_matches_context(self, ctx):
+        var = MCSVariable("y", ctx)
+        assert var.get_storage() == f"mcs_{ctx.uuid}"
+
+    def test_set_to_current_cmd_is_correct(self, ctx, ctx2):
+        var = MCSVariable("my_var", ctx)
+        cmd = var.set_to_current_cmd(ctx2)
+        assert "data modify storage" in cmd
+        assert f"mcs_{ctx2.uuid}" in cmd
+        assert "current" in cmd
+        assert "variable.my_var" in cmd
+
+    def test_repr(self, ctx):
+        var = MCSVariable("z", ctx)
+        r = repr(var)
+        assert "MCSVariable" in r
+        assert "z" in r
+
+    def test_different_names_yield_different_nbts(self, ctx):
+        var1 = MCSVariable("alpha", ctx)
+        var2 = MCSVariable("beta", ctx)
+        assert var1.get_nbt() != var2.get_nbt()
+
+
+# ---------------------------------------------------------------------------
+# MCSNull
+# ---------------------------------------------------------------------------
+
+class TestMCSNull:
+    def test_save_to_storage_cmd_uses_0b(self, ctx):
+        null = MCSNull(ctx)
+        cmd = null.save_to_storage_cmd()
+        assert "0b" in cmd
+        assert "data modify storage" in cmd
+
+    def test_set_to_current_cmd_uses_0b(self, ctx, ctx2):
+        null = MCSNull(ctx)
+        cmd = null.set_to_current_cmd(ctx2)
+        assert "0b" in cmd
+        assert f"mcs_{ctx2.uuid}" in cmd
+        assert "current" in cmd
+
+    def test_repr(self, ctx):
+        null = MCSNull(ctx)
+        assert repr(null) == "MCSNull()"
+
+    def test_storage_compartment_is_null(self, ctx):
+        null = MCSNull(ctx)
+        assert "null" in null.get_nbt()
+
+
+# ---------------------------------------------------------------------------
+# MCSNumber
+# ---------------------------------------------------------------------------
+
+class TestMCSNumber:
+    def test_nbt_starts_with_number(self, ctx):
+        num = MCSNumber(ctx)
+        assert num.get_nbt().startswith("number.")
+
+    def test_repr(self, ctx):
+        num = MCSNumber(ctx)
+        assert "MCSNumber" in repr(num)
+
+    def test_delete_cmd_removes_storage(self, ctx):
+        num = MCSNumber(ctx)
+        cmd = num.delete_from_storage_cmd()
+        assert "data remove storage" in cmd
+        assert num.get_nbt() in cmd
+
+
+# ---------------------------------------------------------------------------
+# MCSString
+# ---------------------------------------------------------------------------
+
+class TestMCSString:
+    def test_nbt_starts_with_string(self, ctx):
+        s = MCSString(ctx)
+        assert s.get_nbt().startswith("string.")
+
+    def test_repr(self, ctx):
+        s = MCSString(ctx)
+        assert "MCSString" in repr(s)
+
+    def test_save_cmd_uses_string_compartment(self, ctx):
+        s = MCSString(ctx)
+        cmd = s.save_to_storage_cmd('"test"')
+        assert "string." in cmd
+
+
+# ---------------------------------------------------------------------------
+# MCSBoolean
+# ---------------------------------------------------------------------------
+
+class TestMCSBoolean:
+    def test_nbt_starts_with_boolean(self, ctx):
+        b = MCSBoolean(ctx)
+        assert b.get_nbt().startswith("boolean.")
+
+    def test_repr(self, ctx):
+        b = MCSBoolean(ctx)
+        assert "MCSBoolean" in repr(b)
+
+
+# ---------------------------------------------------------------------------
+# MCSUnknown
+# ---------------------------------------------------------------------------
+
+class TestMCSUnknown:
+    def test_nbt_starts_with_unknown(self, ctx):
+        u = MCSUnknown(ctx)
+        assert u.get_nbt().startswith("unknown.")
+
+    def test_repr(self, ctx):
+        u = MCSUnknown(ctx)
+        assert "MCSUnknown" in repr(u)
+
+
+# ---------------------------------------------------------------------------
+# MCSList
+# ---------------------------------------------------------------------------
+
+class TestMCSList:
+    def test_nbt_starts_with_list(self, ctx):
+        lst = MCSList(ctx)
+        assert lst.get_nbt().startswith("list.")
+
+    def test_repr(self, ctx):
+        lst = MCSList(ctx)
+        assert "MCSList" in repr(lst)
+
+    def test_save_to_storage_cmd_empty_list(self, ctx):
+        lst = MCSList(ctx)
+        cmds = lst.save_to_storage_cmd([])
+        assert isinstance(cmds, list)
+        # Should still have the length command
+        assert len(cmds) == 1
+        assert ".length set value 0" in cmds[0]
+
+    def test_save_to_storage_cmd_single_element(self, ctx):
+        lst = MCSList(ctx)
+        elem = MCSNumber(ctx)
+        cmds = lst.save_to_storage_cmd([elem])
+        assert isinstance(cmds, list)
+        # length cmd + (set_to_current_cmd + element cmd per item)
+        assert len(cmds) == 3  # length + current + element
+        assert ".length set value 1" in cmds[0]
+
+    def test_save_to_storage_cmd_multiple_elements(self, ctx):
+        lst = MCSList(ctx)
+        elems = [MCSNumber(ctx), MCSString(ctx), MCSBoolean(ctx)]
+        cmds = lst.save_to_storage_cmd(elems)
+        assert isinstance(cmds, list)
+        # 1 length + 3 * (1 set_to_current + 1 element) = 7
+        assert len(cmds) == 7
+        assert ".length set value 3" in cmds[0]
+
+    def test_save_to_storage_cmd_element_indices(self, ctx):
+        lst = MCSList(ctx)
+        elems = [MCSNumber(ctx), MCSString(ctx)]
+        cmds = lst.save_to_storage_cmd(elems)
+        # Check index 0 and 1 appear in the element commands
+        full = "\n".join(cmds)
+        nbt = lst.get_nbt()
+        assert f"{nbt}.0" in full
+        assert f"{nbt}.1" in full
+
+
+# ---------------------------------------------------------------------------
+# MCSFunction.call
+# ---------------------------------------------------------------------------
+
+class TestMCSFunction:
+    def test_call_with_no_params_returns_null_and_invoke(self, ctx, version_ctx_1204):
+        """A function with no parameters should return an invoke command only."""
+        from minecraft_script.compiler.compile_interpreter import CompileContext
+        local_ctx = CompileContext("my_fn")
+
+        fnc = MCSFunction.__new__(MCSFunction)
+        fnc.name = "my_fn"
+        fnc.parameter_names = []
+        fnc.local_context = local_ctx
+
+        mock_interp = MagicMock()
+        mock_interp.datapack_id = "test_pack"
+
+        cmds, ret = fnc.call(mock_interp, [], ctx)
+        assert isinstance(ret, MCSNull)
+        assert len(cmds) == 1
+        assert "user_functions/my_fn" in cmds[0]
+
+    def test_call_with_one_param_has_setup_and_invoke(self, ctx, version_ctx_1204):
+        """A function with one parameter should have param setup lines + invoke."""
+        from minecraft_script.compiler.compile_interpreter import CompileContext
+        local_ctx = CompileContext("fn_with_param")
+
+        fnc = MCSFunction.__new__(MCSFunction)
+        fnc.name = "fn_with_param"
+        fnc.parameter_names = ["arg0"]
+        fnc.local_context = local_ctx
+
+        arg = MCSNumber(ctx)
+        mock_interp = MagicMock()
+        mock_interp.datapack_id = "test_pack"
+
+        cmds, ret = fnc.call(mock_interp, [arg], ctx)
+        assert isinstance(ret, MCSNull)
+        # 2 param setup lines + 1 invoke = 3 total
+        assert len(cmds) == 3
+        assert "user_functions/fn_with_param" in cmds[-1]
+
+    def test_call_param_references_argument_storage(self, ctx, version_ctx_1204):
+        from minecraft_script.compiler.compile_interpreter import CompileContext
+        local_ctx = CompileContext("fn_param_check")
+
+        fnc = MCSFunction.__new__(MCSFunction)
+        fnc.name = "fn_param_check"
+        fnc.parameter_names = ["myarg"]
+        fnc.local_context = local_ctx
+
+        arg = MCSString(ctx)
+        mock_interp = MagicMock()
+        mock_interp.datapack_id = "test_pack"
+
+        cmds, _ = fnc.call(mock_interp, [arg], ctx)
+        # The param setup should reference the argument storage/nbt
+        setup_cmds = "\n".join(cmds[:-1])
+        assert arg.get_storage() in setup_cmds
+        assert "myarg" in setup_cmds
+
+    def test_repr(self, ctx):
+        from minecraft_script.compiler.compile_interpreter import CompileContext
+        local_ctx = CompileContext("test_fn_repr")
+        fnc = MCSFunction.__new__(MCSFunction)
+        fnc.name = "test_fn_repr"
+        fnc.parameter_names = []
+        fnc.local_context = local_ctx
+        assert "MCSFunction" in repr(fnc)
+        assert "test_fn_repr" in repr(fnc)
\ No newline at end of file
diff --git a/tests/test_compiler.py b/tests/test_compiler.py
new file mode 100644
index 0000000..a484fc3
--- /dev/null
+++ b/tests/test_compiler.py
@@ -0,0 +1,221 @@
+"""
+Tests for changed parts of minecraft_script/compiler/compiler.py
+
+Covers: Compiler.__init__ version context setup, function_path, version-aware
+directory names, predefined_root for math/builtins/tags.
+"""
+import os
+import pytest
+from pathlib import Path
+from unittest.mock import patch, MagicMock
+
+import minecraft_script.version_config as vc
+from minecraft_script.version_config import init_version_context, predefined_root
+
+
+# ---------------------------------------------------------------------------
+# predefined_root (also tested in test_version_config, but compiler-focused)
+# ---------------------------------------------------------------------------
+
+class TestPredefinedRootForTemplates:
+    def test_math_folder_exists_for_1204(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("math")
+        assert os.path.isdir(path), f"Expected math template dir at {path}"
+
+    def test_builtins_folder_exists_for_1204(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("builtins")
+        assert os.path.isdir(path), f"Expected builtins template dir at {path}"
+
+    def test_tags_folder_exists_for_1204(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("tags")
+        assert os.path.isdir(path), f"Expected tags template dir at {path}"
+
+    def test_math_folder_contains_mcfunction_files(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("math")
+        files = os.listdir(path)
+        assert any(f.endswith(".mcfunction") for f in files)
+
+    def test_math_folder_contains_add(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("math")
+        assert "add.mcfunction" in os.listdir(path)
+
+    def test_builtins_folder_contains_give_item(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("builtins")
+        assert "give_item.mcfunction" in os.listdir(path)
+
+    def test_tags_folder_contains_block_dir(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("tags")
+        assert "block" in os.listdir(path)
+
+    def test_no_collision_json_in_tags_block(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("tags")
+        block_path = os.path.join(path, "block")
+        assert "no_collision.json" in os.listdir(block_path)
+
+
+# ---------------------------------------------------------------------------
+# Compiler.__init__ - version context and path setup
+# ---------------------------------------------------------------------------
+
+class TestCompilerInit:
+    def _make_compiler(self):
+        """Create a Compiler instance without touching the filesystem."""
+        from minecraft_script.compiler.compiler import Compiler
+        return Compiler(
+            ast=[],
+            datapack_name="Test Pack",
+            output_path="/tmp",
+            verbose=False,
+        )
+
+    def test_compiler_sets_function_dir(self):
+        compiler = self._make_compiler()
+        assert compiler.function_dir == "functions"
+
+    def test_compiler_sets_function_tag_dir(self):
+        compiler = self._make_compiler()
+        assert compiler.function_tag_dir == "functions"
+
+    def test_compiler_datapack_id_lowercased(self):
+        compiler = self._make_compiler()
+        assert compiler.datapack_id == "test_pack"
+
+    def test_compiler_initialises_version_context(self):
+        self._make_compiler()
+        ctx = vc.get_version_context()
+        assert ctx is not None
+        assert ctx.datapack_id == "test_pack"
+
+    def test_compiler_version_profile_is_1204(self):
+        compiler = self._make_compiler()
+        assert compiler.version.profile["minecraft_version"] == "1.20.4"
+
+    def test_compiler_pack_format_is_41(self):
+        compiler = self._make_compiler()
+        assert compiler.version.pack_format == "41"
+
+
+# ---------------------------------------------------------------------------
+# Compiler.function_path
+# ---------------------------------------------------------------------------
+
+class TestCompilerFunctionPath:
+    def _make_compiler(self):
+        from minecraft_script.compiler.compiler import Compiler
+        return Compiler(
+            ast=[],
+            datapack_name="My Pack",
+            output_path="/out",
+            verbose=False,
+        )
+
+    def test_function_path_no_parts(self):
+        compiler = self._make_compiler()
+        path = compiler.function_path()
+        assert path == "/out/My Pack/data/my_pack/functions/"
+
+    def test_function_path_single_part(self):
+        compiler = self._make_compiler()
+        path = compiler.function_path("init.mcfunction")
+        assert path == "/out/My Pack/data/my_pack/functions/init.mcfunction"
+
+    def test_function_path_multiple_parts(self):
+        compiler = self._make_compiler()
+        path = compiler.function_path("user_functions", "main.mcfunction")
+        assert path == "/out/My Pack/data/my_pack/functions/user_functions/main.mcfunction"
+
+    def test_function_path_uses_function_dir(self):
+        compiler = self._make_compiler()
+        path = compiler.function_path("foo")
+        # With 1.20.4, function_dir == "functions"
+        assert "/functions/" in path
+
+    def test_function_path_includes_datapack_id(self):
+        compiler = self._make_compiler()
+        path = compiler.function_path("bar")
+        assert "/my_pack/" in path
+
+
+# ---------------------------------------------------------------------------
+# Build template files - mcfunction file content checks
+# ---------------------------------------------------------------------------
+
+class TestBuildTemplateFiles:
+    """Ensure the moved .mcfunction files have the expected content."""
+
+    def _read_template(self, category: str, filename: str) -> str:
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = os.path.join(predefined_root(category), filename)
+        with open(path, "r") as f:
+            return f.read()
+
+    def test_give_item_mcfunction_uses_give_command(self):
+        content = self._read_template("builtins", "give_item.mcfunction")
+        assert "give" in content.lower()
+
+    def test_give_item_mcfunction_references_item_and_count(self):
+        content = self._read_template("builtins", "give_item.mcfunction")
+        assert "$(item)" in content
+        assert "$(count)" in content
+
+    def test_give_item_new_format_uses_components_without_brackets(self):
+        content = self._read_template("builtins", "give_item.mcfunction")
+        # New format: $(components)$(count) without [brackets]
+        assert "[$(components)]" not in content
+
+    def test_add_mcfunction_uses_scoreboard(self):
+        content = self._read_template("math", "add.mcfunction")
+        assert "scoreboard" in content
+
+    def test_command_mcfunction_exists(self):
+        path = os.path.join(predefined_root("builtins"), "command.mcfunction")
+        assert os.path.isfile(path)
+
+    def test_log_mcfunction_exists(self):
+        path = os.path.join(predefined_root("builtins"), "log.mcfunction")
+        assert os.path.isfile(path)
+
+    def test_set_block_mcfunction_exists(self):
+        path = os.path.join(predefined_root("builtins"), "set_block.mcfunction")
+        assert os.path.isfile(path)
+
+    def test_no_collision_json_is_valid_json(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        import json
+        path = os.path.join(predefined_root("tags"), "block", "no_collision.json")
+        with open(path) as f:
+            data = json.load(f)
+        assert "values" in data or "replace" in data or len(data) > 0
+
+    def test_math_template_files_present(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        math_path = predefined_root("math")
+        expected_ops = {
+            "add", "subtract", "multiply", "divide", "modulus",
+            "equals", "greater_than", "less_than",
+            "greater_equals_than", "less_equals_than",
+            "u_add", "u_subtract", "u_not",
+        }
+        files_in_dir = {f.replace(".mcfunction", "") for f in os.listdir(math_path)}
+        assert expected_ops.issubset(files_in_dir), (
+            f"Missing math ops: {expected_ops - files_in_dir}"
+        )
\ No newline at end of file
diff --git a/tests/test_version_config.py b/tests/test_version_config.py
new file mode 100644
index 0000000..a5f6003
--- /dev/null
+++ b/tests/test_version_config.py
@@ -0,0 +1,510 @@
+"""
+Tests for minecraft_script/version_config.py
+
+Covers: VersionRenderer, VersionContext, load_version_profile,
+list_supported_versions, predefined_root, init/get/clear_version_context.
+"""
+import json
+import os
+import pytest
+from pathlib import Path
+from unittest.mock import patch
+
+import minecraft_script.version_config as vc
+from minecraft_script.version_config import (
+    VersionRenderer,
+    VersionContext,
+    load_version_profile,
+    list_supported_versions,
+    predefined_root,
+    init_version_context,
+    get_version_context,
+    clear_version_context,
+    get_minecraft_version,
+)
+
+
+# ---------------------------------------------------------------------------
+# get_minecraft_version
+# ---------------------------------------------------------------------------
+
+class TestGetMinecraftVersion:
+    def test_returns_configured_version(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        assert get_minecraft_version() == "1.20.4"
+
+    def test_reflects_config_change(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "9.9.9"
+        assert get_minecraft_version() == "9.9.9"
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"  # restore
+
+
+# ---------------------------------------------------------------------------
+# load_version_profile
+# ---------------------------------------------------------------------------
+
+class TestLoadVersionProfile:
+    def test_loads_known_version(self):
+        profile = load_version_profile("1.20.4")
+        assert profile["minecraft_version"] == "1.20.4"
+        assert profile["pack_format"] == 41
+
+    def test_profile_has_required_keys(self):
+        profile = load_version_profile("1.20.4")
+        assert "paths" in profile
+        assert "templates" in profile
+        assert "constants" in profile
+
+    def test_unknown_version_raises_file_not_found(self):
+        with pytest.raises(FileNotFoundError, match="Unknown minecraft version"):
+            load_version_profile("0.0.0")
+
+    def test_unknown_version_error_mentions_supported(self):
+        with pytest.raises(FileNotFoundError, match="1.20.4"):
+            load_version_profile("0.0.0")
+
+    def test_caching_returns_same_object(self):
+        p1 = load_version_profile("1.20.4")
+        p2 = load_version_profile("1.20.4")
+        assert p1 is p2
+
+    def test_uses_common_config_version_when_none_given(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        profile = load_version_profile()
+        assert profile["minecraft_version"] == "1.20.4"
+
+    def test_cache_cleared_between_tests(self):
+        # conftest clears cache; this profile load should succeed fresh
+        profile = load_version_profile("1.20.4")
+        assert profile is not None
+
+
+# ---------------------------------------------------------------------------
+# list_supported_versions
+# ---------------------------------------------------------------------------
+
+class TestListSupportedVersions:
+    def test_contains_1204(self):
+        versions = list_supported_versions()
+        assert "1.20.4" in versions
+
+    def test_excludes_index_json(self):
+        versions = list_supported_versions()
+        assert "index" not in versions
+
+    def test_returns_sorted_list(self):
+        versions = list_supported_versions()
+        assert versions == sorted(versions)
+
+    def test_returns_list(self):
+        versions = list_supported_versions()
+        assert isinstance(versions, list)
+
+    def test_nonexistent_versions_dir_returns_empty(self):
+        with patch.object(Path, "is_dir", return_value=False):
+            result = list_supported_versions()
+        assert result == []
+
+
+# ---------------------------------------------------------------------------
+# predefined_root
+# ---------------------------------------------------------------------------
+
+class TestPredefinedRoot:
+    def test_math_root_for_1204(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        path = predefined_root("math")
+        assert path.endswith(os.path.join("math", "1.20.4"))
+
+    def test_explicit_version_override(self):
+        path = predefined_root("math", version="9.9.9")
+        assert path.endswith(os.path.join("math", "9.9.9"))
+
+    def test_builtins_category(self):
+        path = predefined_root("builtins", version="1.20.4")
+        assert "builtins" in path
+        assert "1.20.4" in path
+
+    def test_tags_category(self):
+        path = predefined_root("tags", version="1.20.4")
+        assert "tags" in path
+
+    def test_path_is_string(self):
+        path = predefined_root("math", version="1.20.4")
+        assert isinstance(path, str)
+
+
+# ---------------------------------------------------------------------------
+# VersionRenderer
+# ---------------------------------------------------------------------------
+
+class TestVersionRenderer:
+    def test_render_simple_template(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        result = renderer.render("simple")
+        assert result == "just text"
+
+    def test_render_with_variable(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        result = renderer.render("with_var", myVar="world")
+        assert result == "value=world"
+
+    def test_render_missing_key_raises(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        with pytest.raises(KeyError, match="Template 'nonexistent' not defined"):
+            renderer.render("nonexistent")
+
+    def test_render_strips_surrounding_whitespace(self, mock_profile):
+        mock_profile["templates"]["padded"] = "  hello  "
+        renderer = VersionRenderer(mock_profile)
+        result = renderer.render("padded")
+        assert result == "hello"
+
+    def test_render_lines_splits_on_newline(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        lines = renderer.render_lines("multiline")
+        assert lines == ["line1", "line2", "line3"]
+
+    def test_render_lines_empty_template_returns_empty_list(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        lines = renderer.render_lines("empty")
+        assert lines == []
+
+    def test_compiled_template_is_cached(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        renderer.render("simple")
+        assert "simple" in renderer._compiled
+        # Second call still works
+        assert renderer.render("simple") == "just text"
+
+    def test_conditional_true_branch(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        result = renderer.render("conditional", flag=True)
+        assert result == "yes"
+
+    def test_conditional_false_branch(self, mock_profile):
+        renderer = VersionRenderer(mock_profile)
+        result = renderer.render("conditional", flag=False)
+        assert result == "no"
+
+    def test_missing_templates_key_raises(self):
+        profile_no_templates = {"minecraft_version": "x", "pack_format": 0}
+        renderer = VersionRenderer(profile_no_templates)
+        with pytest.raises(KeyError):
+            renderer.render("anything")
+
+
+# ---------------------------------------------------------------------------
+# VersionContext
+# ---------------------------------------------------------------------------
+
+class TestVersionContext:
+    def test_init_sets_datapack_id(self, profile_1204):
+        ctx = VersionContext("my_pack", profile=profile_1204)
+        assert ctx.datapack_id == "my_pack"
+
+    def test_init_sets_function_dir(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        assert ctx.function_dir == "functions"
+
+    def test_init_sets_function_tag_dir(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        assert ctx.function_tag_dir == "functions"
+
+    def test_init_sets_pack_format_as_string(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        assert ctx.pack_format == "41"
+
+    def test_render_uses_datapack_id_constant(self, profile_1204):
+        ctx = VersionContext("mypack", profile=profile_1204)
+        result = ctx.render("function.call.invoke", functionName="foo")
+        assert "mypack" in result
+        assert "foo" in result
+
+    def test_render_injects_version_constants(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        # click.check uses clickSelectedItemPath constant from profile
+        result = ctx.render("click.check")
+        assert "SelectedItem.tag.mcs_click" in result
+
+    def test_params_merges_constants_with_extra(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        params = ctx._params(extra="value")
+        assert params["extra"] == "value"
+        assert params["datapack_id"] == "dp"
+        assert "clickScoreboardCriterion" in params
+
+    def test_params_extra_overrides_constants(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        params = ctx._params(datapack_id="override")
+        assert params["datapack_id"] == "override"
+
+    def test_render_lines_returns_list(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        lines = ctx.render_lines("control.while.tail", condStorage="s", condNbt="n", loopPath="lp")
+        assert isinstance(lines, list)
+        assert len(lines) > 0
+
+    def test_render_lines_pack_format_placeholder(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        # pack_format must be passed explicitly (same as compiler.py does)
+        result = ctx.render("pack.mcmeta", pack_format=ctx.pack_format)
+        assert "41" in result
+
+    def test_render_datapack_kill_includes_datapack_name(self, profile_1204):
+        ctx = VersionContext("dp", profile=profile_1204)
+        result = ctx.render("datapack.kill", datapackName="my_datapack")
+        assert "my_datapack" in result
+
+    def test_loads_profile_automatically_without_explicit_profile(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        ctx = VersionContext("auto_pack")
+        assert ctx.profile["minecraft_version"] == "1.20.4"
+
+
+# ---------------------------------------------------------------------------
+# Global context management: init / get / clear
+# ---------------------------------------------------------------------------
+
+class TestVersionContextGlobals:
+    def test_get_before_init_raises_runtime_error(self):
+        with pytest.raises(RuntimeError, match="Version context not initialized"):
+            get_version_context()
+
+    def test_init_returns_context(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        ctx = init_version_context("pack")
+        assert isinstance(ctx, VersionContext)
+
+    def test_get_after_init_returns_same_context(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        ctx = init_version_context("pack")
+        assert get_version_context() is ctx
+
+    def test_clear_resets_context(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        init_version_context("pack")
+        clear_version_context()
+        with pytest.raises(RuntimeError):
+            get_version_context()
+
+    def test_init_overwrites_previous_context(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        ctx1 = init_version_context("pack1")
+        ctx2 = init_version_context("pack2")
+        assert get_version_context() is ctx2
+        assert ctx1 is not ctx2
+
+    def test_context_datapack_id_matches_init_arg(self):
+        from minecraft_script.common import COMMON_CONFIG
+        COMMON_CONFIG["minecraft_version"] = "1.20.4"
+        ctx = init_version_context("custom_id")
+        assert ctx.datapack_id == "custom_id"
+
+
+# ---------------------------------------------------------------------------
+# Template rendering - 1.20.4 specific spot checks
+# ---------------------------------------------------------------------------
+
+class TestTemplateRendering1204:
+    """Integration tests ensuring specific 1.20.4 templates render correctly."""
+
+    def test_literal_save(self, version_ctx_1204):
+        result = version_ctx_1204.render("literal.save", storage="mcs_abc", nbt="number.xyz", value=42)
+        assert result == "data modify storage mcs_abc number.xyz set value 42"
+
+    def test_literal_delete(self, version_ctx_1204):
+        result = version_ctx_1204.render("literal.delete", storage="mcs_abc", nbt="number.xyz")
+        assert result == "data remove storage mcs_abc number.xyz"
+
+    def test_literal_set_to_current(self, version_ctx_1204):
+        result = version_ctx_1204.render(
+            "literal.set_to_current", destStorage="mcs_dest", storage="mcs_src", nbt="number.n"
+        )
+        assert "data modify storage mcs_dest current set from storage mcs_src number.n" == result
+
+    def test_variable_declare(self, version_ctx_1204):
+        result = version_ctx_1204.render(
+            "variable.declare",
+            ownerStorage="mcs_owner", name="x", valueStorage="mcs_val", valueNbt="number.n"
+        )
+        assert "variable.x" in result
+        assert "mcs_owner" in result
+
+    def test_variable_assign(self, version_ctx_1204):
+        result = version_ctx_1204.render(
+            "variable.assign",
+            ownerStorage="mcs_owner", name="myVar", valueStorage="mcs_src", valueNbt="string.s"
+        )
+        assert "variable.myVar" in result
+
+    def test_kill_remove_compartment(self, version_ctx_1204):
+        result = version_ctx_1204.render(
+            "kill.remove_compartment", ctxStorage="mcs_abc", compartment="number"
+        )
+        assert result == "data remove storage mcs_abc number"
+
+    def test_function_call_invoke(self, version_ctx_1204):
+        result = version_ctx_1204.render("function.call.invoke", functionName="my_func")
+        assert result == "function test_pack:user_functions/my_func"
+
+    def test_function_call_param(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "function.call.param",
+            localStorage="mcs_local", argStorage="mcs_arg", argNbt="string.s", paramName="foo"
+        )
+        assert len(lines) == 2
+        assert "mcs_local" in lines[0]
+        assert "variable.foo" in lines[1]
+
+    def test_math_binary_renders(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "math.binary",
+            leftStorage="mcs_a", leftNbt="number.x",
+            rightStorage="mcs_b", rightNbt="number.y",
+            operation="add",
+            resultStorage="mcs_r", resultNbt="number.z",
+        )
+        assert any(".a" in l for l in lines)
+        assert any(".b" in l for l in lines)
+        assert any("math/add" in l for l in lines)
+
+    def test_math_unary_renders(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "math.unary",
+            rootStorage="mcs_r", rootNbt="boolean.b",
+            operation="u_not",
+            resultStorage="mcs_out", resultNbt="boolean.b2",
+        )
+        assert any("math/u_not" in l for l in lines)
+
+    def test_control_if_branch_renders(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "control.if.branch",
+            exprStorage="mcs_e", exprNbt="boolean.b",
+            branchStorage="mcs_br", parentStorage="mcs_p",
+            branchPath="code_blocks/cb_abc",
+        )
+        assert any("score" in l for l in lines)
+        assert any("return 0" in l for l in lines)
+
+    def test_control_for_init_renders(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "control.for.init",
+            loopId="abc123",
+            iterableStorage="mcs_it", iterableNbt="list.l",
+            loopPath="code_blocks/loop",
+        )
+        assert any("loop_iter_abc123" in l for l in lines)
+        assert any("loop_end_abc123" in l for l in lines)
+
+    def test_click_check_uses_correct_path(self, version_ctx_1204):
+        result = version_ctx_1204.render("click.check")
+        assert "SelectedItem.tag.mcs_click" in result
+        assert "test_pack:clickable_items/run" in result
+
+    def test_click_run_renders(self, version_ctx_1204):
+        result = version_ctx_1204.render("click.run")
+        assert "test_pack:clickable_items/$(id)" in result
+
+    def test_datapack_init_uses_carrot_on_a_stick(self, version_ctx_1204):
+        result = version_ctx_1204.render("datapack.init")
+        assert "minecraft.used:minecraft.carrot_on_a_stick" in result
+
+    def test_builtin_log_renders(self, version_ctx_1204):
+        suffix = ' {"s0": "mcs_abc", "n0": "number.x"}'
+        result = version_ctx_1204.render("builtin.log", storageSuffix=suffix)
+        assert "test_pack:builtins/log" in result
+        assert suffix in result
+
+    def test_builtin_give_item_with_components(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "builtin.give_item.setup",
+            ctxStorage="mcs_c",
+            itemStorage="mcs_i", itemNbt="string.s",
+            hasComponents=True,
+            componentsStorage="mcs_comp", componentsNbt="string.c",
+            hasCount=False,
+        )
+        full = "\n".join(lines)
+        assert "current.components set from storage mcs_comp" in full
+        assert "current.count set value 1" in full
+
+    def test_builtin_give_item_without_components(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "builtin.give_item.setup",
+            ctxStorage="mcs_c",
+            itemStorage="mcs_i", itemNbt="string.s",
+            hasComponents=False,
+            hasCount=False,
+        )
+        full = "\n".join(lines)
+        assert "current.components set value ''" in full
+
+    def test_pack_mcmeta_has_pack_format(self, version_ctx_1204):
+        result = version_ctx_1204.render("pack.mcmeta", pack_format=version_ctx_1204.pack_format)
+        assert '"pack_format": 41' in result
+
+    def test_index_setup_renders(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "index.setup",
+            ctxStorage="mcs_ctx",
+            keyStorage="mcs_key", keyNbt="number.n",
+            macroPath="code_blocks/macro",
+        )
+        full = "\n".join(lines)
+        assert "current.index" in full
+        assert "code_blocks/macro" in full
+
+    def test_literal_list_length(self, version_ctx_1204):
+        result = version_ctx_1204.render(
+            "literal.list.length", storage="mcs_abc", nbt="list.l", length=5
+        )
+        assert ".length set value 5" in result
+
+    def test_literal_list_element(self, version_ctx_1204):
+        result = version_ctx_1204.render(
+            "literal.list.element", storage="mcs_abc", nbt="list.l", index=2, srcStorage="mcs_src"
+        )
+        assert "list.l.2" in result
+        assert "current" in result
+
+    def test_builtin_give_clickable_simple(self, version_ctx_1204):
+        result = version_ctx_1204.render("builtin.give_clickable.simple", clickId=3)
+        assert "carrot_on_a_stick" in result
+        assert "mcs_click:3" in result
+
+    def test_raycast_block_loop_with_loop_function(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "builtin.raycast_block.loop",
+            raycastId="ray123",
+            hitFunction="my_hit",
+            hasLoopFunction=True,
+            loopFunction="my_loop",
+            loopPath="code_blocks/loop",
+        )
+        full = "\n".join(lines)
+        assert "user_functions/my_hit" in full
+        assert "user_functions/my_loop" in full
+
+    def test_raycast_block_loop_without_loop_function(self, version_ctx_1204):
+        lines = version_ctx_1204.render_lines(
+            "builtin.raycast_block.loop",
+            raycastId="ray123",
+            hitFunction="my_hit",
+            hasLoopFunction=False,
+            loopFunction="",
+            loopPath="code_blocks/loop",
+        )
+        full = "\n".join(lines)
+        assert "# No loop function" in full
+        assert "user_functions/my_hit" in full
