Production: - agent/bedrock_adapter.py, agent/vertex_adapter.py: pm.ensure_import ran at module import. In any process that imports these modules without a committed PM selection (CI's build_environment test venv, a fresh checkout) that sync rebuilt the dependency environment mid-process and replaced sys.path with a generation missing the caller's own packages (anthropic, aiohttp vanished). The extra is now ensured at first client build / credential request. - plugins/platforms/matrix/adapter.py: a complete install needs no ensure_and_bind round trip; only a partial one syncs. - tools/browser_tool.py: drop the facade's duplicate warm_agent_browser_npx_cache shim; the compat pointer already resolves to browser_tool_install. Test harness: - tests/home_io_guard.py: PATH-entry probes (shutil.which) and the running interpreter's own installation (stdlib reads, realpath ancestry, fixture symlinks into it) are not Hermes state; a patched Path.expanduser must not crash the guard. run_tests.sh no longer filters PATH — the guard owns it. - tests/tui_gateway/conftest.py: import hermes_bootstrap before any file opens a MagicMock hermes_constants window (6 files exited the process at boot). - tests/hermes_cli/conftest.py probe_root: scratch checkouts the import guard probes need hermes_bootstrap.py (the launcher imports it). - tests/pm/_fixtures.py stage_host_python: a copied relocatable python needs its stdlib beside it (No module named 'encodings' on CI). - tests/install/e2e-assets/smoke-env.mjs: dependency-free env shaping so the source-build-env probe runs under bare node (main deleted the Playwright entry it was imported through). - adapt main's new tests to branch seams (model_metadata_http, launch completion tail, CI toolchain exports uv after python, source_launch hermes_cli stub, systemd_notify single marker).
129 lines
6.7 KiB
Python
129 lines
6.7 KiB
Python
"""Source E2E children stamp their own checkout, not the workflow's NEW ref."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
ASSETS = ROOT / "tests/install/e2e-assets"
|
|
|
|
|
|
def _stamp_probe(tmp_path, shell):
|
|
repo = tmp_path / "installed source"
|
|
for relative in (
|
|
"scripts/write_install_stamp.py", "hermes_cli/__init__.py",
|
|
"hermes_cli/update_channel.py", "hermes_cli/release_channels.py",
|
|
"pm/paths.py", "pm/environments.py",
|
|
"hermes_cli/steward.py", "hermes_constants.py",
|
|
):
|
|
dest = repo / relative
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(ROOT / relative, dest)
|
|
env = dict(os.environ, HOME=str(tmp_path), HERMES_HOME=str(tmp_path / "home"),
|
|
GIT_CONFIG_GLOBAL=str(tmp_path / "gitconfig"), GIT_CONFIG_NOSYSTEM="1")
|
|
(tmp_path / "gitconfig").write_text('[url "file:///staged/serve.git"]\n'
|
|
'\tinsteadOf = https://github.com/NousResearch/hermes-agent.git\n',
|
|
encoding="utf-8")
|
|
|
|
def git(*args):
|
|
return subprocess.run(["git", *args], cwd=repo, env=env, check=True,
|
|
capture_output=True, text=True, timeout=30).stdout.strip()
|
|
|
|
git("init", "-q", "-b", "installed")
|
|
git("config", "user.name", "Fixture")
|
|
git("config", "user.email", "fixture@example.invalid")
|
|
git("add", ".")
|
|
git("commit", "-qm", "OLD")
|
|
old = git("rev-parse", "HEAD")
|
|
git("commit", "--allow-empty", "-qm", "NEW")
|
|
new = git("rev-parse", "HEAD")
|
|
# Control: this is the real stamp writer's CI preference, not a source-text assertion.
|
|
stamp_command = [sys.executable, "-I", "-S", str(repo / "scripts/write_install_stamp.py"),
|
|
"--output", str(tmp_path / "contaminated.json"), "--update-mechanism", "self"]
|
|
git("checkout", "-q", "-B", "installed", old)
|
|
subprocess.run(stamp_command, env={**env, "GITHUB_SHA": new}, check=True,
|
|
capture_output=True, text=True, timeout=30)
|
|
assert json.loads((tmp_path / "contaminated.json").read_text(encoding="utf-8-sig"))["commit"] == new
|
|
overrides = dict(GITHUB_SHA=new, GITHUB_REF="refs/heads/workflow", GITHUB_REF_NAME="workflow",
|
|
GITHUB_HEAD_REF="workflow", GITHUB_BASE_REF="main", HERMES_BUILD_COMMIT=new,
|
|
HERMES_PAYLOAD_TAG="v1.2.3", HERMES_PAYLOAD_VERSION="1.2.3",
|
|
HERMES_DESKTOP_VARIANT="bundled")
|
|
env.update(overrides, ASSETS=str(ASSETS), PROBE_PYTHON=sys.executable,
|
|
PROBE_ROOT=str(repo), PROBE_OUT=str(tmp_path / "stamp.json"),
|
|
PROBE_ENV=str(tmp_path / "child-env.json"))
|
|
probe = tmp_path / "probe.py"
|
|
probe.write_text('''import json, os, runpy
|
|
from pathlib import Path
|
|
stamp = runpy.run_path(str(Path(os.environ['PROBE_ROOT']) / 'scripts/write_install_stamp.py'))
|
|
stamp['write_stamp'](os.environ['PROBE_OUT'], update_mechanism='self')
|
|
Path(os.environ['PROBE_ENV']).write_text(json.dumps(dict(os.environ)), encoding='utf-8')
|
|
''', encoding="utf-8")
|
|
env["PROBE_SCRIPT"] = str(probe)
|
|
if shell == "bash":
|
|
command = [shell, "-euc", '''
|
|
source "$ASSETS/source-build-env.sh"
|
|
source_build_env "$PROBE_PYTHON" -I -S "$PROBE_SCRIPT"
|
|
if source_build_env "$PROBE_PYTHON" -I -S -c 'raise SystemExit(23)'; then exit 99; else test "$?" = 23; fi
|
|
"$PROBE_PYTHON" -I -S -c 'import json, os; print(json.dumps(dict(os.environ)))'
|
|
''']
|
|
elif shell == "node":
|
|
env["HERMES_DESKTOP_USER_DATA_DIR"] = str(tmp_path / "user-data")
|
|
env["HERMES_PYTHON_SRC_ROOT"] = str(repo)
|
|
command = [shell, "--input-type=module", "-e", '''
|
|
import { pathToFileURL } from 'node:url';
|
|
import { spawnSync } from 'node:child_process';
|
|
const { updateWindowEnvironment } = await import(pathToFileURL(process.env.ASSETS + '/smoke-env.mjs'));
|
|
const env = updateWindowEnvironment(process.env, process.env.PROBE_ROOT, 'source');
|
|
const result = spawnSync(process.env.PROBE_PYTHON, ['-I', '-S', process.env.PROBE_SCRIPT], { env, stdio: 'inherit' });
|
|
if (result.error || result.status !== 0) throw new Error(`stamp child failed: ${result.error || result.status}`);
|
|
console.log(JSON.stringify(process.env));
|
|
''']
|
|
else:
|
|
command = [shell, "-NoProfile", "-NonInteractive", "-Command", '''
|
|
$ErrorActionPreference = 'Stop'
|
|
. (Join-Path $env:ASSETS 'source-build-env.ps1')
|
|
Invoke-SourceBuild { & $env:PROBE_PYTHON -I -S $env:PROBE_SCRIPT; if ($LASTEXITCODE) { throw 'stamp failed' } }
|
|
try { Invoke-SourceBuild { throw 'child failure' }; throw 'lost exception' }
|
|
catch { if ($_.Exception.Message -ne 'child failure') { throw } }
|
|
& $env:PROBE_PYTHON -I -S -c 'import json, os; print(json.dumps(dict(os.environ)))'
|
|
''']
|
|
for sha in (old, new):
|
|
git("checkout", "-q", "-B", "installed", sha)
|
|
result = subprocess.run(command, env=env, cwd=tmp_path, capture_output=True,
|
|
text=True, encoding="utf-8", timeout=30)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
stamp = json.loads(Path(env["PROBE_OUT"]).read_text(encoding="utf-8-sig"))
|
|
assert (stamp["commit"], stamp["branch"], stamp["source"], stamp["payload"]) == (
|
|
sha, "installed", "local", "bootstrap")
|
|
child = json.loads(Path(env["PROBE_ENV"]).read_text(encoding="utf-8-sig"))
|
|
assert not overrides.keys() & child.keys()
|
|
assert child["GIT_CONFIG_GLOBAL"] == env["GIT_CONFIG_GLOBAL"]
|
|
redirect = subprocess.run(["git", "ls-remote", "--get-url",
|
|
"https://github.com/NousResearch/hermes-agent.git"], env=child,
|
|
cwd=tmp_path, check=True, capture_output=True, text=True, timeout=30)
|
|
assert redirect.stdout.strip() == "file:///staged/serve.git"
|
|
assert child["HERMES_HOME"] == env["HERMES_HOME"]
|
|
parent = json.loads(result.stdout.splitlines()[-1])
|
|
assert {key: parent[key] for key in overrides} == overrides
|
|
|
|
|
|
@pytest.mark.platforms("posix")
|
|
@pytest.mark.parametrize("shell", ["bash", "node"])
|
|
def test_posix_build_children_stamp_old_and_new_without_changing_parent(tmp_path, shell):
|
|
_stamp_probe(tmp_path, shell)
|
|
|
|
|
|
@pytest.mark.platforms("windows", "posix")
|
|
def test_powershell_build_children_stamp_old_and_new_without_changing_parent(tmp_path):
|
|
path = os.pathsep.join(p for p in os.get_exec_path() if ".hermes" not in Path(p).parts)
|
|
shell = shutil.which("pwsh", path=path) or shutil.which("powershell", path=path)
|
|
if not shell:
|
|
if os.name == "nt":
|
|
pytest.fail("native Windows acceptance requires PowerShell")
|
|
pytest.skip("PowerShell is not available on this host")
|
|
_stamp_probe(tmp_path, shell) |