29a12bb102
- Add expand_container_path() helper that resolves ~/ and $HOME/ prefixes
- Add get_manifest_home_dir() to compute /home/{user.name} or /root from manifest
- Set ENV HOME=... and ENV USER=... in generated Dockerfile for runtime compatibility
- Pass home_dir through instance creation and startup pipeline
- Expand mount targets in apply_resolved_profile() for regular profile mounts
- Expand mapping targets in _resolve_git_mount_mappings() for git mounts
- Expand working_directory and volume targets in _modify_compose_file()
- Update _prepare_manifest_instance to return home_dir alongside image tag
- Fetch tool_type early in start_instance to determine home_dir before profile application
Quality gates: pytest 188 passed, frontend typecheck clean
Addresses: home-path-expansion
366 lines
13 KiB
Python
366 lines
13 KiB
Python
"""Unit tests for the manifest compiler."""
|
|
|
|
import pytest
|
|
|
|
from src.services.manifest_compiler import (
|
|
compile_compose,
|
|
compile_dockerfile,
|
|
compile_entrypoint,
|
|
compute_image_tag,
|
|
deep_merge,
|
|
get_manifest_home_dir,
|
|
merge_with_config,
|
|
resolve_base,
|
|
)
|
|
|
|
|
|
class TestResolveBase:
|
|
"""Tests for resolve_base."""
|
|
|
|
def test_returns_manifest_unchanged_when_no_base(self) -> None:
|
|
manifest = {"name": "test", "base_image": "ubuntu:24.04"}
|
|
result = resolve_base(manifest)
|
|
assert result["name"] == "test"
|
|
assert "base_definition_id" not in result
|
|
|
|
|
|
class TestDeepMerge:
|
|
"""Tests for deep_merge."""
|
|
|
|
def test_packages_are_unioned(self) -> None:
|
|
base = {"packages": {"apt": ["curl", "git"]}}
|
|
override = {"packages": {"apt": ["neovim"]}}
|
|
result = deep_merge(base, override)
|
|
assert result["packages"]["apt"] == ["curl", "git", "neovim"]
|
|
|
|
def test_node_version_overrides(self) -> None:
|
|
base = {"packages": {"node": {"version": "18"}}}
|
|
override = {"packages": {"node": {"version": "20"}}}
|
|
result = deep_merge(base, override)
|
|
assert result["packages"]["node"]["version"] == "20"
|
|
|
|
def test_env_is_merged_with_override_winning(self) -> None:
|
|
base = {"env": {"FOO": "base", "BAR": "base"}}
|
|
override = {"env": {"FOO": "override"}}
|
|
result = deep_merge(base, override)
|
|
assert result["env"]["FOO"] == "override"
|
|
assert result["env"]["BAR"] == "base"
|
|
|
|
def test_build_scripts_are_concatenated(self) -> None:
|
|
base = {"scripts": {"build": ["echo base"]}}
|
|
override = {"scripts": {"build": ["echo override"]}}
|
|
result = deep_merge(base, override)
|
|
assert result["scripts"]["build"] == ["echo base", "echo override"]
|
|
|
|
def test_mounts_are_concatenated(self) -> None:
|
|
base = {"mounts": [{"name": "base-mount", "target": "/base"}]}
|
|
override = {"mounts": [{"name": "tool-mount", "target": "/tool"}]}
|
|
result = deep_merge(base, override)
|
|
assert len(result["mounts"]) == 2
|
|
|
|
def test_user_is_overridden_entirely(self) -> None:
|
|
base = {"user": {"name": "base", "uid": 1000}}
|
|
override = {"user": {"name": "tool", "uid": 1001}}
|
|
result = deep_merge(base, override)
|
|
assert result["user"]["name"] == "tool"
|
|
assert result["user"]["uid"] == 1001
|
|
|
|
|
|
class TestCompileDockerfile:
|
|
"""Tests for compile_dockerfile."""
|
|
|
|
def test_includes_from(self) -> None:
|
|
manifest = {"base_image": "ubuntu:24.04", "name": "test"}
|
|
df = compile_dockerfile(manifest)
|
|
assert "FROM ubuntu:24.04" in df
|
|
|
|
def test_installs_apt_packages(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"packages": {"apt": ["curl", "git"]},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "apt-get install -y" in df
|
|
assert "curl" in df
|
|
assert "git" in df
|
|
assert "rm -rf /var/lib/apt/lists/*" in df
|
|
|
|
def test_installs_node(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"packages": {"node": {"version": "20"}},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "nodesource.com/setup_20.x" in df
|
|
|
|
def test_installs_npm_global(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"packages": {"npm_global": ["@scope/pkg"]},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "npm install -g @scope/pkg" in df
|
|
|
|
def test_creates_user(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"user": {"name": "dev", "uid": 1001, "gid": 1001},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "groupadd -g 1001 dev" in df
|
|
assert "useradd -u 1001 -g 1001" in df
|
|
assert "USER dev" in df
|
|
|
|
def test_build_scripts_as_run_commands(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"scripts": {"build": ["echo hello", "echo world"]},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "RUN echo hello" in df
|
|
assert "RUN echo world" in df
|
|
|
|
def test_creates_mount_directories(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"user": {"name": "dev", "uid": 1001, "gid": 1001},
|
|
"mounts": [
|
|
{"name": "ws", "target": "/workspace"},
|
|
{"name": "cfg", "target": "/config"},
|
|
],
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "mkdir -p /workspace /config" in df
|
|
assert "chown -R dev:dev /workspace /config" in df
|
|
|
|
def test_entrypoint_for_startup_scripts(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"scripts": {"startup": ["echo start"]},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert 'ENTRYPOINT ["/usr/local/bin/headquarter-entrypoint"]' in df
|
|
|
|
def test_cmd_from_runtime(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"runtime": {"command": ["/bin/bash", "-il"]},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert 'CMD ["/bin/bash", "-il"]' in df
|
|
|
|
def test_default_cmd_when_no_runtime(self) -> None:
|
|
manifest = {"base_image": "ubuntu:24.04", "name": "test"}
|
|
df = compile_dockerfile(manifest)
|
|
assert 'CMD ["/bin/bash"]' in df
|
|
|
|
def test_sets_home_env_for_user(self) -> None:
|
|
manifest = {
|
|
"base_image": "ubuntu:24.04",
|
|
"name": "test",
|
|
"user": {"name": "dev", "uid": 1001, "gid": 1001},
|
|
}
|
|
df = compile_dockerfile(manifest)
|
|
assert "ENV HOME=/home/dev" in df
|
|
assert "ENV USER=dev" in df
|
|
|
|
def test_no_home_env_without_user(self) -> None:
|
|
manifest = {"base_image": "ubuntu:24.04", "name": "test"}
|
|
df = compile_dockerfile(manifest)
|
|
assert "ENV HOME=" not in df
|
|
assert "ENV USER=" not in df
|
|
|
|
|
|
class TestGetManifestHomeDir:
|
|
"""Tests for get_manifest_home_dir."""
|
|
|
|
def test_with_user_name(self) -> None:
|
|
manifest = {"user": {"name": "dev", "uid": 1001, "gid": 1001}}
|
|
assert get_manifest_home_dir(manifest) == "/home/dev"
|
|
|
|
def test_without_user(self) -> None:
|
|
manifest = {"base_image": "ubuntu:24.04"}
|
|
assert get_manifest_home_dir(manifest) == "/root"
|
|
|
|
def test_with_empty_user_name(self) -> None:
|
|
manifest = {"user": {"name": "", "uid": 1001, "gid": 1001}}
|
|
assert get_manifest_home_dir(manifest) == "/root"
|
|
|
|
|
|
class TestCompileEntrypoint:
|
|
"""Tests for compile_entrypoint."""
|
|
|
|
def test_includes_shebang_and_set_e(self) -> None:
|
|
manifest = {"scripts": {"startup": ["echo hello"]}}
|
|
ep = compile_entrypoint(manifest)
|
|
assert "#!/bin/bash" in ep
|
|
assert "set -e" in ep
|
|
|
|
def test_includes_startup_scripts(self) -> None:
|
|
manifest = {"scripts": {"startup": ["echo hello", "echo world"]}}
|
|
ep = compile_entrypoint(manifest)
|
|
assert "echo hello" in ep
|
|
assert "echo world" in ep
|
|
|
|
def test_ends_with_exec(self) -> None:
|
|
manifest: dict = {"scripts": {"startup": []}}
|
|
ep = compile_entrypoint(manifest)
|
|
assert 'exec "$@"' in ep
|
|
|
|
|
|
class TestCompileCompose:
|
|
"""Tests for compile_compose."""
|
|
|
|
def test_includes_image_and_container_name(self) -> None:
|
|
manifest = {"name": "test", "interface_type": "terminal"}
|
|
vars_dict = {"IMAGE_TAG": "test:v1", "INSTANCE_NAME": "test-1"}
|
|
compose = compile_compose(manifest, vars_dict)
|
|
assert "image: test:v1" in compose
|
|
assert "container_name: test-1" in compose
|
|
|
|
def test_terminal_fields(self) -> None:
|
|
manifest = {
|
|
"name": "test",
|
|
"interface_type": "terminal",
|
|
"runtime": {"stdin_open": True, "tty": True, "working_dir": "/workspace"},
|
|
}
|
|
compose = compile_compose(manifest, {"IMAGE_TAG": "t", "INSTANCE_NAME": "n"})
|
|
assert "stdin_open: true" in compose
|
|
assert "tty: true" in compose
|
|
assert "working_dir: /workspace" in compose
|
|
|
|
def test_web_ports(self) -> None:
|
|
manifest = {
|
|
"name": "test",
|
|
"interface_type": "web",
|
|
"default_port": 8080,
|
|
}
|
|
compose = compile_compose(
|
|
manifest, {"IMAGE_TAG": "t", "INSTANCE_NAME": "n", "TOOL_PORT": "3000"}
|
|
)
|
|
assert "3000:8080" in compose
|
|
|
|
def test_user_override(self) -> None:
|
|
manifest = {
|
|
"name": "test",
|
|
"interface_type": "terminal",
|
|
"user": {"uid": 1001, "gid": 1001},
|
|
}
|
|
compose = compile_compose(manifest, {"IMAGE_TAG": "t", "INSTANCE_NAME": "n"})
|
|
assert "user: 1001:1001" in compose
|
|
|
|
def test_mounts_resolved(self) -> None:
|
|
manifest = {
|
|
"name": "test",
|
|
"interface_type": "terminal",
|
|
"mounts": [
|
|
{"name": "ws", "target": "/workspace", "source_type": "repo"},
|
|
{
|
|
"name": "ssh",
|
|
"target": "/home/user/.ssh",
|
|
"source_type": "ssh_key",
|
|
"readonly": True,
|
|
},
|
|
],
|
|
}
|
|
compose = compile_compose(
|
|
manifest,
|
|
{
|
|
"IMAGE_TAG": "t",
|
|
"INSTANCE_NAME": "n",
|
|
"REPO_PATH": "/repos/myrepo",
|
|
"SSH_PATH": "/keys/ssh",
|
|
},
|
|
)
|
|
assert "/repos/myrepo:/workspace" in compose
|
|
assert "/keys/ssh:/home/user/.ssh:ro" in compose
|
|
|
|
def test_extra_volumes_appended(self) -> None:
|
|
manifest = {"name": "test", "interface_type": "terminal"}
|
|
compose = compile_compose(
|
|
manifest,
|
|
{
|
|
"IMAGE_TAG": "t",
|
|
"INSTANCE_NAME": "n",
|
|
"EXTRA_VOLUMES": [{"source": "/host/x", "target": "/container/x"}],
|
|
},
|
|
)
|
|
assert "/host/x:/container/x" in compose
|
|
|
|
|
|
class TestComputeImageTag:
|
|
"""Tests for compute_image_tag."""
|
|
|
|
def test_is_deterministic(self) -> None:
|
|
manifest = {"name": "test", "packages": {"apt": ["curl"]}}
|
|
tag1 = compute_image_tag("My Tool", manifest)
|
|
tag2 = compute_image_tag("My Tool", manifest)
|
|
assert tag1 == tag2
|
|
|
|
def test_changes_with_content(self) -> None:
|
|
manifest1 = {"name": "test", "packages": {"apt": ["curl"]}}
|
|
manifest2 = {"name": "test", "packages": {"apt": ["wget"]}}
|
|
tag1 = compute_image_tag("test", manifest1)
|
|
tag2 = compute_image_tag("test", manifest2)
|
|
assert tag1 != tag2
|
|
|
|
def test_lowercases_name(self) -> None:
|
|
manifest = {"name": "test"}
|
|
tag = compute_image_tag("My Tool", manifest)
|
|
assert "my-tool" in tag
|
|
|
|
def test_valid_docker_reference(self) -> None:
|
|
manifest = {"name": "test"}
|
|
tag = compute_image_tag("test", manifest)
|
|
assert tag.startswith("headquarter/test-")
|
|
assert tag.endswith(":latest")
|
|
|
|
|
|
class TestMergeWithConfig:
|
|
"""Tests for merge_with_config (ConfigProfile only)."""
|
|
|
|
def test_no_profile_returns_manifest_unchanged(self) -> None:
|
|
manifest = {"name": "test"}
|
|
result = merge_with_config(manifest)
|
|
assert result["name"] == "test"
|
|
assert result["_extra_env"] == {}
|
|
assert result["_extra_volumes"] == []
|
|
|
|
def test_profile_env_vars(self) -> None:
|
|
manifest = {"name": "test"}
|
|
profile = {"environment_variables": {"FOO": "bar"}}
|
|
result = merge_with_config(manifest, profile)
|
|
assert result["_extra_env"]["FOO"] == "bar"
|
|
|
|
def test_profile_mounts(self) -> None:
|
|
manifest = {"name": "test"}
|
|
profile = {"mounts": [{"source": "/host", "target": "/container"}]}
|
|
result = merge_with_config(manifest, profile)
|
|
assert len(result["_extra_volumes"]) == 1
|
|
|
|
def test_profile_port_override(self) -> None:
|
|
manifest = {"name": "test", "default_port": 8080}
|
|
profile = {"hints": {"port_override": 3000}}
|
|
result = merge_with_config(manifest, profile)
|
|
assert result["default_port"] == 3000
|
|
|
|
def test_profile_start_command(self) -> None:
|
|
manifest = {"name": "test", "runtime": {"command": ["/bin/bash"]}}
|
|
profile = {"hints": {"start_command": "/bin/sh"}}
|
|
result = merge_with_config(manifest, profile)
|
|
assert result["runtime"]["command"] == ["/bin/sh"]
|
|
|
|
def test_profile_working_directory(self) -> None:
|
|
manifest = {"name": "test"}
|
|
profile = {"hints": {"working_directory": "/workspace"}}
|
|
result = merge_with_config(manifest, profile)
|
|
assert result["runtime"]["working_dir"] == "/workspace"
|