123 lines
5.0 KiB
Python
123 lines
5.0 KiB
Python
"""Tests for hermes_cli.runtime_env — the single PATH/env assembler."""
|
|
|
|
import os
|
|
|
|
from hermes_cli import runtime_env as re_mod
|
|
from hermes_cli import runtime_registry as rr
|
|
|
|
|
|
def _provision(tmp_path, name, rel_bin, version="1.0.0", path_dirs=None,
|
|
path_order=None):
|
|
"""Create a fake tool binary + record its fact."""
|
|
binary = tmp_path / rel_bin
|
|
binary.parent.mkdir(parents=True, exist_ok=True)
|
|
binary.write_text("#!/bin/sh\n")
|
|
facts = rr.load_facts(tmp_path)
|
|
facts[name] = rr.RuntimeFact(version=version, path=rel_bin, path_dirs=path_dirs)
|
|
rr.save_facts(facts, tmp_path, path_order=path_order)
|
|
|
|
|
|
class TestManagedPathDirs:
|
|
def test_empty_when_unprovisioned(self, tmp_path):
|
|
assert re_mod.managed_path_dirs(tmp_path) == []
|
|
|
|
def test_the_recorded_assembly_order_wins_over_insertion_order(self, tmp_path):
|
|
"""The order is data the provisioner derived from the pin table,
|
|
not the order facts happened to be written in."""
|
|
order = ["node", "uv"]
|
|
_provision(tmp_path, "uv", "uv/uv", path_order=order)
|
|
_provision(tmp_path, "node", "node/bin/node", path_order=order)
|
|
|
|
dirs = re_mod.managed_path_dirs(tmp_path)
|
|
|
|
assert dirs == [tmp_path / "node" / "bin", tmp_path / "uv"]
|
|
|
|
def test_an_extender_is_assembled_before_what_it_extends(self, tmp_path):
|
|
"""npm's dir has to precede node's, or node's bundled npm wins.
|
|
The edge is declared once in the pin table; this asserts the
|
|
assembler honours what was derived from it."""
|
|
pins = {
|
|
"node": {"version": "26.7.0", "files": {}},
|
|
"npm": {"version": "12.0.2", "extends": ["node"], "files": {}},
|
|
}
|
|
order = rr.path_order(pins)
|
|
_provision(tmp_path, "node", "node/bin/node", path_order=order)
|
|
_provision(tmp_path, "npm", "npm/bin/npm", path_order=order)
|
|
|
|
dirs = re_mod.managed_path_dirs(tmp_path)
|
|
|
|
assert dirs == [tmp_path / "npm" / "bin", tmp_path / "node" / "bin"]
|
|
|
|
def test_vanished_binary_contributes_nothing(self, tmp_path):
|
|
_provision(tmp_path, "node", "node/bin/node")
|
|
(tmp_path / "node" / "bin" / "node").unlink()
|
|
assert re_mod.managed_path_dirs(tmp_path) == []
|
|
|
|
def test_path_dirs_override_spreads_multiple_dirs(self, tmp_path):
|
|
# PortableGit shape: cmd + bin + usr/bin, fact points at cmd/git.exe.
|
|
for d in ("git/cmd", "git/bin", "git/usr/bin"):
|
|
(tmp_path / d).mkdir(parents=True)
|
|
(tmp_path / "git" / "cmd" / "git.exe").write_text("")
|
|
facts = {
|
|
"git": rr.RuntimeFact(
|
|
version="2.55.0",
|
|
path="git/cmd/git.exe",
|
|
path_dirs=["git/cmd", "git/bin", "git/usr/bin"],
|
|
)
|
|
}
|
|
rr.save_facts(facts, tmp_path)
|
|
dirs = re_mod.managed_path_dirs(tmp_path)
|
|
assert dirs == [
|
|
tmp_path / "git" / "cmd",
|
|
tmp_path / "git" / "bin",
|
|
tmp_path / "git" / "usr" / "bin",
|
|
]
|
|
|
|
|
|
class TestManagedToolEnv:
|
|
def test_npm_cache_only_when_node_managed(self, tmp_path):
|
|
assert re_mod.managed_tool_env(tmp_path) == {}
|
|
_provision(tmp_path, "node", "node/bin/node")
|
|
env = re_mod.managed_tool_env(tmp_path)
|
|
assert env["npm_config_cache"] == str(tmp_path / "cache" / "npm")
|
|
|
|
|
|
class TestWithManagedRuntimes:
|
|
def test_prepends_to_path_front(self, tmp_path):
|
|
_provision(tmp_path, "node", "node/bin/node")
|
|
env = re_mod.with_managed_runtimes({"PATH": "/usr/bin"}, tmp_path)
|
|
parts = env["PATH"].split(os.pathsep)
|
|
assert parts[0] == str(tmp_path / "node" / "bin")
|
|
assert parts[-1] == "/usr/bin"
|
|
|
|
def test_no_tools_means_untouched_env(self, tmp_path):
|
|
base = {"PATH": "/usr/bin", "FOO": "bar"}
|
|
assert re_mod.with_managed_runtimes(base, tmp_path) == base
|
|
|
|
def test_caller_env_not_mutated(self, tmp_path):
|
|
_provision(tmp_path, "node", "node/bin/node")
|
|
base = {"PATH": "/usr/bin"}
|
|
re_mod.with_managed_runtimes(base, tmp_path)
|
|
assert base == {"PATH": "/usr/bin"}
|
|
|
|
def test_respects_lowercase_path_key(self, tmp_path):
|
|
# POSIX env vars are case-sensitive but some Windows-shaped
|
|
# callers carry 'Path' — the assembler must extend THAT key, not
|
|
# add a second one.
|
|
_provision(tmp_path, "node", "node/bin/node")
|
|
env = re_mod.with_managed_runtimes({"Path": "C:\\Windows"}, tmp_path)
|
|
assert "PATH" not in env or env.get("Path") is not None
|
|
assert env["Path"].startswith(str(tmp_path / "node" / "bin"))
|
|
|
|
def test_tool_env_defaults_do_not_clobber_caller(self, tmp_path):
|
|
_provision(tmp_path, "node", "node/bin/node")
|
|
env = re_mod.with_managed_runtimes(
|
|
{"PATH": "/usr/bin", "npm_config_cache": "/custom"}, tmp_path
|
|
)
|
|
assert env["npm_config_cache"] == "/custom"
|
|
|
|
def test_default_env_is_os_environ_copy(self, tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_RT_SENTINEL", "yes")
|
|
env = re_mod.with_managed_runtimes(None, tmp_path)
|
|
assert env["HERMES_RT_SENTINEL"] == "yes"
|