"""Unit tests for the manifest compiler.""" import pytest from src.services.build.manifest_compiler import ( compile_compose, compile_dockerfile, compile_entrypoint, get_manifest_container_user, get_manifest_home_dir, ) @pytest.mark.unit def test_compile_dockerfile_creates_config_dirs_for_user() -> None: """The Dockerfile must pre-create ~/.config and chown it to the container user.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } dockerfile = compile_dockerfile(manifest) assert "groupadd -g 1000 dev" in dockerfile assert "useradd -u 1000 -g 1000 -m -s /bin/bash dev" in dockerfile assert "mkdir -p /home/dev && chown -R dev:dev /home/dev" in dockerfile assert ( "mkdir -p /home/dev/.config && chown -R dev:dev /home/dev/.config" in dockerfile ) assert ( "mkdir -p /home/dev/.local/share && chown -R dev:dev /home/dev/.local/share" in dockerfile ) assert ( "mkdir -p /home/dev/.cache && chown -R dev:dev /home/dev/.cache" in dockerfile ) @pytest.mark.unit def test_compile_dockerfile_no_user_does_not_create_home() -> None: """Without a user config, no home/user setup should be emitted.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", } dockerfile = compile_dockerfile(manifest) assert "useradd" not in dockerfile assert "/home/" not in dockerfile @pytest.mark.unit class TestGetManifestHomeDir: """Tests for get_manifest_home_dir precedence.""" def test_home_directory_in_manifest_wins(self) -> None: manifest = { "home_directory": "/home/custom", "user": {"name": "dev"}, } assert get_manifest_home_dir(manifest) == "/home/custom" def test_user_name_derives_home(self) -> None: manifest = {"user": {"name": "dev", "uid": 1000, "gid": 1000}} assert get_manifest_home_dir(manifest) == "/home/dev" def test_root_fallback(self) -> None: manifest = {"base_image": "ubuntu:24.04"} assert get_manifest_home_dir(manifest) == "/root" def test_empty_home_directory_falls_back(self) -> None: manifest = {"home_directory": "", "user": {"name": "dev"}} assert get_manifest_home_dir(manifest) == "/home/dev" @pytest.mark.unit class TestCompileDockerfileHomeDirectory: """Tests that compile_dockerfile honors manifest.home_directory.""" def test_env_home_and_workdir_use_project_directory(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "workspace_name": "my-app", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } dockerfile = compile_dockerfile(manifest) assert "ENV HOME=/home/custom" in dockerfile assert "WORKDIR /home/custom/my-app" in dockerfile def test_project_directory_created(self) -> None: """When workspace name is known at build time, create the project directory.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "workspace_name": "my-app", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } dockerfile = compile_dockerfile(manifest) assert "mkdir -p /home/custom/my-app" in dockerfile assert "ln -sfn" not in dockerfile def test_runtime_workspace_not_baked_into_image(self) -> None: """When workspace name is a runtime placeholder, do not create literal {{WORKSPACE_NAME}} directories in the image.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "mounts": [{"source_type": "repo", "target": "~/{{WORKSPACE_NAME}}"}], } dockerfile = compile_dockerfile(manifest) assert "{{WORKSPACE_NAME}}" not in dockerfile assert "ln -sfn" not in dockerfile assert "WORKDIR /home/custom/{{WORKSPACE_NAME}}" not in dockerfile assert "WORKDIR /home/custom\n" in dockerfile def test_runtime_working_dir_overrides_home_workdir(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "runtime": {"working_dir": "/app/code"}, } dockerfile = compile_dockerfile(manifest) assert "WORKDIR /app/code" in dockerfile assert "WORKDIR /home/custom" not in dockerfile def test_working_dir_expands_tilde(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "runtime": {"working_dir": "~/code"}, } dockerfile = compile_dockerfile(manifest) assert "WORKDIR /home/custom/code" in dockerfile @pytest.mark.unit class TestCompileComposeHomeDirectory: """Tests that compile_compose uses home_directory for volumes and working_dir.""" def test_default_repo_mount_synthesized(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } variables = { "IMAGE_TAG": "test:latest", "INSTANCE_NAME": "test-instance", "REPO_PATH": "/host/repos/my-app", "WORKSPACE_NAME": "my-app", "TOOL_PORT": 0, "EXTRA_ENV": {}, "EXTRA_VOLUMES": [], } compose = compile_compose(manifest, variables) assert "/host/repos/my-app:/home/custom/my-app" in compose def test_explicit_repo_mount_preserved(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "mounts": [ {"source_type": "repo", "target": "/opt/code", "readonly": True} ], } variables = { "IMAGE_TAG": "test:latest", "INSTANCE_NAME": "test-instance", "REPO_PATH": "/host/repos/my-app", "WORKSPACE_NAME": "my-app", "TOOL_PORT": 0, "EXTRA_ENV": {}, "EXTRA_VOLUMES": [], } compose = compile_compose(manifest, variables) assert "/host/repos/my-app:/opt/code:ro" in compose assert "/host/repos/my-app:/home/custom/my-app" not in compose def test_workspace_name_substituted_in_mount_target(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "mounts": [{"source_type": "repo", "target": "~/{{WORKSPACE_NAME}}"}], } variables = { "IMAGE_TAG": "test:latest", "INSTANCE_NAME": "test-instance", "REPO_PATH": "/host/repos/my-app", "WORKSPACE_NAME": "my-app", "TOOL_PORT": 0, "EXTRA_ENV": {}, "EXTRA_VOLUMES": [], } compose = compile_compose(manifest, variables) assert "/host/repos/my-app:/home/custom/my-app" in compose assert "WORKSPACE_NAME: my-app" in compose def test_working_dir_expands_home(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "runtime": {"working_dir": "$HOME/code"}, } variables = { "IMAGE_TAG": "test:latest", "INSTANCE_NAME": "test-instance", "REPO_PATH": "/host/repos/my-app", "WORKSPACE_NAME": "my-app", "TOOL_PORT": 0, "EXTRA_ENV": {}, "EXTRA_VOLUMES": [], } compose = compile_compose(manifest, variables) assert "working_dir: /home/custom/code" in compose @pytest.mark.unit def test_compile_dockerfile_uses_user_npm_prefix() -> None: """npm global packages must be installed into a user-writable prefix.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "packages": {"npm_global": ["@scope/pkg"]}, } dockerfile = compile_dockerfile(manifest) assert "npm install -g --prefix /home/custom/.npm-global" in dockerfile assert "/home/custom/.npm-global/bin:$PATH" in dockerfile assert "ENV PATH=/home/custom/.npm-global/bin:$PATH" in dockerfile @pytest.mark.unit def test_compile_dockerfile_starts_as_root_and_drops_privileges() -> None: """The Dockerfile must not set USER so the entrypoint starts as root. The entrypoint itself drops privileges to the container user before exec-ing the real command. """ manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } dockerfile = compile_dockerfile(manifest) entrypoint = compile_entrypoint(manifest) assert "USER dev" not in dockerfile assert "exec runuser -u dev -- /bin/bash -il" in entrypoint assert 'exec runuser -u dev -- "$@"' in entrypoint @pytest.mark.unit def test_compile_compose_does_not_pin_root_user() -> None: """The compose service must not override the user to root. The Dockerfile intentionally omits USER so the entrypoint starts as root, fixes mount ownership, and drops privileges internally. Setting a compose-level user would pin docker exec sessions to root even after the entrypoint drops privileges. """ manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } variables = { "IMAGE_TAG": "test:latest", "INSTANCE_NAME": "test-instance", "REPO_PATH": "/host/repos/my-app", "WORKSPACE_NAME": "my-app", "TOOL_PORT": 0, "EXTRA_ENV": {}, "EXTRA_VOLUMES": [], } compose = compile_compose(manifest, variables) assert "user:" not in compose @pytest.mark.unit def test_get_manifest_container_user_returns_name() -> None: """The container user helper returns the manifest user name.""" manifest = { "user": {"name": "dev", "uid": 1000, "gid": 1000}, } assert get_manifest_container_user(manifest) == "dev" @pytest.mark.unit def test_get_manifest_container_user_falls_back_to_uid_gid() -> None: """When the user name is missing, return uid:gid.""" manifest = { "user": {"uid": 1000, "gid": 1000}, } assert get_manifest_container_user(manifest) == "1000:1000" @pytest.mark.unit def test_get_manifest_container_user_returns_none_without_user() -> None: """When no user is declared, return None.""" manifest = {"base_image": "ubuntu:24.04"} assert get_manifest_container_user(manifest) is None @pytest.mark.unit class TestCompileEntrypoint: """Tests for the generated permission-fixing entrypoint.""" def test_entrypoint_creates_home_and_project_directory(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } entrypoint = compile_entrypoint(manifest) assert 'mkdir -p "$HOME_DIR"' in entrypoint assert 'mkdir -p "$WORKSPACE_TARGET"' in entrypoint assert "ln -sfn" not in entrypoint assert 'WORKSPACE_NAME="${WORKSPACE_NAME:-workspace}"' in entrypoint def test_entrypoint_removes_stale_placeholder_directory(self) -> None: """Older images baked in a literal {{WORKSPACE_NAME}} directory.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } entrypoint = compile_entrypoint(manifest) assert 'if [ -d "${HOME_DIR}/{{WORKSPACE_NAME}}" ]; then' in entrypoint assert 'rm -rf "${HOME_DIR}/{{WORKSPACE_NAME}}"' in entrypoint def test_entrypoint_does_not_create_workspace_symlink(self) -> None: """The /workspace compatibility symlink is no longer created.""" manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, } entrypoint = compile_entrypoint(manifest) assert "/workspace" not in entrypoint def test_entrypoint_fixes_mount_owners(self) -> None: manifest = { "base_image": "ubuntu:24.04", "interface_type": "terminal", "home_directory": "/home/custom", "user": {"name": "dev", "uid": 1000, "gid": 1000}, "mounts": [ {"target": "~/.config", "readonly": False}, {"target": "/opt/readonly", "readonly": True}, ], } entrypoint = compile_entrypoint(manifest) assert 'fix_owner "/home/custom/.config"' in entrypoint assert 'fix_owner "/opt/readonly"' not in entrypoint