Files
hermes-agent/tests/pm/test_node_sidecar.py
ethernet 66d54ebf51 Merge remote-tracking branch 'origin/main' into ethie/pm-clean
# Conflicts:
#	.github/workflows/docker.yml
#	Dockerfile
#	apps/desktop/src/app/settings/about-settings.tsx
#	docker/stage2-hook.sh
2026-09-24 02:23:44 -04:00

113 lines
5.0 KiB
Python

"""Plugin-local npm installs retain PM's paired Node and consent policy."""
import json
import os
from pathlib import Path
import shutil
import shlex
import pytest
import pm
from pm.package import InstallError, Runner, compose_env
from pm.workspace import install_node_sidecar
_REAL_HERMES_HOME = Path.home() / ".hermes" # Captured before per-test HOME isolation.
def test_no_package_json_never_acquires_npm(tmp_path, monkeypatch):
def unexpected(*args, **kwargs):
pytest.fail("a dependency-free plugin must not acquire npm")
monkeypatch.setattr(pm, "ensure", unexpected)
assert install_node_sidecar(tmp_path) is None
@pytest.mark.parametrize("explicit", [False, True])
def test_acquisition_receives_consent_and_reports_refusal(tmp_path, monkeypatch, explicit):
import importlib
monkeypatch.setattr(importlib.import_module("pm.install"), "lazy_installs_allowed", lambda: True)
(tmp_path / "package.json").write_text('{}')
calls = []
def refused(name, **kwargs):
calls.append((name, kwargs))
raise InstallError(name, "acquisition refused")
monkeypatch.setattr(pm, "ensure", refused)
reason = install_node_sidecar(tmp_path, explicit=explicit)
assert "acquisition refused" in reason
assert calls == [("npm", {"explicit": explicit})]
@pytest.mark.platforms("posix")
def test_real_npm_uses_paired_node_with_empty_ambient_path(tmp_path, monkeypatch):
npm, node = shutil.which("npm"), shutil.which("node")
if not npm or not node:
pytest.skip("npm and node are required")
if any(Path(executable).absolute().is_relative_to(_REAL_HERMES_HOME)
for executable in (npm, node)):
pytest.skip("requires npm and Node outside the real Hermes home")
# npm's real JS entrypoint uses /usr/bin/env node. Its paired Node lives
# in a different PATH entry, just as the two PM packages do.
npm_cli = next(iter(Path(npm).resolve().parent.parent.glob("lib/npm*/bin/npm-cli.js")), Path(npm).resolve())
npm_bin, node_bin = tmp_path / "npm/bin", tmp_path / "node/bin"
npm_bin.mkdir(parents=True)
node_bin.mkdir(parents=True)
wrapper = npm_bin / "npm"
wrapper.write_text(f'#!/bin/sh\nexec node {shlex.quote(str(npm_cli))} "$@"\n')
wrapper.chmod(0o755)
(node_bin / "node").symlink_to(node)
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setattr(Path, "home", lambda: tmp_path)
monkeypatch.setenv("HERMES_HOME", str(tmp_path / "home"))
monkeypatch.setenv("PATH", "")
monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
context = Runner("npm", compose_env([
{"PATH": [str(node_bin)]}, {"PATH": [str(npm_bin)]},
]))
calls = []
def acquire(name, **kwargs):
calls.append((name, kwargs))
return context
monkeypatch.setattr(pm, "ensure", acquire)
dependency, plugin = tmp_path / "side-dep", tmp_path / "plugin"
dependency.mkdir()
plugin.mkdir()
(dependency / "package.json").write_text(json.dumps({"name": "side-dep", "version": "1.0.0", "main": "index.js"}))
(dependency / "index.js").write_text("module.exports = 'isolated';")
manifest = {"name": "plugin", "version": "1.0.0", "dependencies": {"side-dep": "file:../side-dep"}}
(plugin / "package.json").write_text(json.dumps(manifest))
ambient = dict(os.environ)
assert install_node_sidecar(plugin, explicit=True) is None
lock = (plugin / "package-lock.json").read_bytes()
# ci must remove an extraneous directory; another `install` would not
# reliably prove that the lockfile selected the clean-install path.
stale = plugin / "node_modules/stale-file"
stale.write_text("remove me")
assert install_node_sidecar(plugin, explicit=True) is None
assert not stale.exists()
assert (plugin / "package-lock.json").read_bytes() == lock
result = context.run(["node", "-e", "console.log(require('side-dep'))"], cwd=plugin, capture_output=True, text=True, timeout=30)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "isolated"
assert not (tmp_path / "node_modules").exists()
assert dict(os.environ) == ambient
assert calls == [("npm", {"explicit": True})] * 2
(plugin / "package.json").write_text("{malformed")
reason = install_node_sidecar(plugin, explicit=True)
assert "exited" in reason and "JSON" in reason
def test_on_demand_sidecar_install_respects_lazy_refusal(tmp_path, monkeypatch):
(tmp_path / "package.json").write_text('{}')
monkeypatch.setenv("HERMES_DISABLE_LAZY_INSTALLS", "1")
binary = tmp_path / ("npm.cmd" if os.name == "nt" else "npm")
binary.write_text("process boundary fixture")
binary.chmod(0o755)
class InstalledRunner:
env = {"PATH": str(tmp_path)}
def run(self, *args, **kwargs):
pytest.fail("installed npm must not mutate sidecars when on-demand installs are disabled")
monkeypatch.setattr(pm, "ensure", lambda *args, **kwargs: InstalledRunner())
reason = install_node_sidecar(tmp_path)
assert reason and "disabled" in reason