diff --git a/hermes_cli/source_build.py b/hermes_cli/source_build.py index d80eac1f0a..08deb040a3 100644 --- a/hermes_cli/source_build.py +++ b/hermes_cli/source_build.py @@ -80,6 +80,13 @@ def build_source_web(project_root: Path, *, env: dict, icons: Path | None = None "--icons", str(icons), "--out", str(project_root / "hermes_cli/web_dist"), env=env) +def source_frontends(project_root: Path) -> tuple[str, ...]: + """The frontend workspaces this checkout carries. A source slice without them + (python-only installs, the installer's acceptance fixture) has no products + to build; it still publishes commands and runs the maintenance tail.""" + return tuple(name for name in ("ui-tui", "web") if (project_root / name / "package.json").is_file()) + + def build_update_products(project_root: Path, *, desktop: bool) -> None: """Prepare the selected union once; a failed product aborts the update.""" # Both current updates and historical takeover reach this in a fresh target @@ -88,14 +95,19 @@ def build_update_products(project_root: Path, *, desktop: bool) -> None: from hermes_cli.update_stage import publish_stage _warn_configured_features_missing_deps() + frontends = source_frontends(project_root) + if not frontends: + return env = source_build_env(explicit=True) - workspaces = ("ui-tui", "web") + (("apps/desktop",) if desktop else ()) + workspaces = frontends + (("apps/desktop",) if desktop else ()) publish_stage("Updating Node dependencies") prepare_source_dependencies(project_root, workspaces, env=env, explicit=True) - publish_stage("Building the TUI") - build_source_tui(project_root, env=env) - publish_stage("Building the web UI") - build_source_web(project_root, env=env, explicit=True) + if "ui-tui" in frontends: + publish_stage("Building the TUI") + build_source_tui(project_root, env=env) + if "web" in frontends: + publish_stage("Building the web UI") + build_source_web(project_root, env=env, explicit=True) if desktop: from hermes_cli.main_desktop import _install_rebuilt_desktop_app, build_prepared_desktop diff --git a/hermes_cli/stderr_timestamp.py b/hermes_cli/stderr_timestamp.py index bc9d1c9fd0..4beca8fe5b 100644 --- a/hermes_cli/stderr_timestamp.py +++ b/hermes_cli/stderr_timestamp.py @@ -14,6 +14,9 @@ from pathlib import Path from typing import BinaryIO, Sequence, TextIO EXTERNAL_SUPERVISOR_FLAG = "--external-supervisor" +# gateway.restart.GATEWAY_FATAL_CONFIG_EXIT_CODE. This wrapper is a launcher boot +# file: it runs from a source slice and stays stdlib-only. +_GATEWAY_FATAL_CONFIG_EXIT_CODE = 78 _TIMESTAMP_PREFIX = re.compile(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}(?:\s|$)") @@ -97,10 +100,8 @@ def _child_returncode_for_supervisor(command: Sequence[str], returncode: int) -> """ if returncode < 0: return 128 + abs(returncode) - from gateway.restart import GATEWAY_FATAL_CONFIG_EXIT_CODE, map_fatal_config_exit_for_launchd - - if returncode == GATEWAY_FATAL_CONFIG_EXIT_CODE and _is_hermes_gateway_run_argv(command): - return map_fatal_config_exit_for_launchd(returncode) + if returncode == _GATEWAY_FATAL_CONFIG_EXIT_CODE and _is_hermes_gateway_run_argv(command): + return 0 return returncode diff --git a/tests/hermes_cli/test_plugin_historic_compat.py b/tests/hermes_cli/test_plugin_historic_compat.py index 3e2b919924..6e0f1dfb8a 100644 --- a/tests/hermes_cli/test_plugin_historic_compat.py +++ b/tests/hermes_cli/test_plugin_historic_compat.py @@ -97,8 +97,9 @@ import json from pathlib import Path import sys sys.path.insert(0, sys.argv[1]) -# No selected application environment, no PM, and no CLI config/UI reader. -for module in ('pm', 'hermes_cli.config', 'hermes_cli.plugins_cmd'): +# No selected application environment, no PM engine, and no CLI config/UI reader. +# pm.environments/pm.paths are stdlib boot leaves the recovery owner may use. +for module in ('pm.client', 'pm.install', 'pm.store', 'pm.workspace', 'hermes_cli.config', 'hermes_cli.plugins_cmd'): sys.modules[module] = None from hermes_cli import plugins_transaction row = json.loads(sys.stdin.read()) diff --git a/tests/hermes_cli/test_web_memory_provider_setup_install.py b/tests/hermes_cli/test_web_memory_provider_setup_install.py index 3af409b88f..7caea72a02 100644 --- a/tests/hermes_cli/test_web_memory_provider_setup_install.py +++ b/tests/hermes_cli/test_web_memory_provider_setup_install.py @@ -123,6 +123,8 @@ def test_setup_reports_restart_and_preserves_external_steps(tmp_path, monkeypatc if python_failure: raise RuntimeError("Python preparation refused") monkeypatch.setattr("pm.sync_venv", sync) + # A sync selects a new generation the running interpreter has not activated. + monkeypatch.setattr("pm.environments.selected_venv", lambda root: tmp_path / "next-generation") result = mp._install_memory_provider_setup("provider") assert result["ok"] is not python_failure assert result["results"][0]["status"] == ("failed" if python_failure else "restart_required") diff --git a/tests/pm/test_source_update_launch.py b/tests/pm/test_source_update_launch.py index 718cdf1de9..ca6049038a 100644 --- a/tests/pm/test_source_update_launch.py +++ b/tests/pm/test_source_update_launch.py @@ -61,6 +61,15 @@ def source_launch(tmp_path, monkeypatch, isolated_python): (root / "install-stamp.json").write_text( json.dumps({"updateMechanism": "self"}), encoding="utf-8", ) + # The startup heal hands the shared completion tail (launchers, products, + # maintenance) to the checkout's own hermes_cli/source_completion.py. This + # source slice has no products; record the hand-off instead of running it. + (root / "hermes_cli").mkdir() + (root / "hermes_cli" / "source_completion.py").write_text( + "import json, sys\n" + f"open({str(tmp_path / 'completion-calls')!r}, 'a').write(json.dumps(sys.argv[1:]) + '\\n')\n", + encoding="utf-8", + ) (root / "pyproject.toml").write_text( '[project]\nname = "launch-proof"\nversion = "1"\nrequires-python = ">=3.11"\n' '[project.optional-dependencies]\nall = []\nlaunch-extra = []\n' @@ -136,6 +145,9 @@ def test_source_python_pin_update_survives_real_gc(source_launch, tmp_path, monk previous = selected_venv(root) if update == "launch": assert venv_sync.prepare_launch(root, []) == new_python + # The heal finished the whole tail, with update wording, through the checkout's own completion. + calls = [json.loads(line) for line in (tmp_path / "completion-calls").read_text().splitlines()] + assert calls == [["--source", str(root), "--finish-update"]] elif update == "sync": # This is the PM sync -> launcher publication sequence now split across # update_completion._prepare and _complete_selected. The former checkout diff --git a/tests/scripts/test_commit_stamp_identity.py b/tests/scripts/test_commit_stamp_identity.py index e97895ba09..386e6ef2f5 100644 --- a/tests/scripts/test_commit_stamp_identity.py +++ b/tests/scripts/test_commit_stamp_identity.py @@ -15,7 +15,7 @@ def test_stamp_uses_built_commit_even_with_dispatch_sha_and_refuses_mismatch(tmp repo.mkdir() for relative in ('scripts/write_install_stamp.py', 'scripts/releases/commit_build.py', 'hermes_cli/__init__.py', 'hermes_cli/update_channel.py', 'hermes_cli/release_channels.py', - 'pm/environments.py', 'hermes_cli/steward.py', 'hermes_constants.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) diff --git a/tests/scripts/test_fresh_source_install.py b/tests/scripts/test_fresh_source_install.py index cb36afcada..08260e8948 100644 --- a/tests/scripts/test_fresh_source_install.py +++ b/tests/scripts/test_fresh_source_install.py @@ -146,7 +146,7 @@ def test_current_installer_publishes_real_dependencies_and_warm_path(tmp_path, s marker.chmod(0o444) before = facts.read_bytes() try: - run([*command, "--stage", "path"]) + run([*command, "--stage", "products"]) finally: marker.chmod(0o644) assert facts.read_bytes() == before diff --git a/tests/scripts/test_release_build_commit.py b/tests/scripts/test_release_build_commit.py index a5d9f2e28c..25068f6c0d 100644 --- a/tests/scripts/test_release_build_commit.py +++ b/tests/scripts/test_release_build_commit.py @@ -35,7 +35,7 @@ def fixture_repo(tmp_path): 'scripts/releases/r2.py', 'scripts/releases/r2_scope.py', 'scripts/release-content-types.json', 'hermes_cli/__init__.py', 'hermes_cli/update_channel.py', 'hermes_cli/release_channels.py', - 'pm/environments.py', 'hermes_constants.py'): + 'pm/paths.py', 'pm/environments.py', 'hermes_constants.py'): dest = repo / relative dest.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(ROOT / relative, dest) diff --git a/tests/scripts/test_source_build_env.py b/tests/scripts/test_source_build_env.py index 67b53d460d..3961dbcd72 100644 --- a/tests/scripts/test_source_build_env.py +++ b/tests/scripts/test_source_build_env.py @@ -17,7 +17,8 @@ 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", "pm/environments.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