Files
hermes-agent/tests/scripts/test_desktop_preparation.py
ethernet8023 4bc38bceec fix(desktop): hand the admitted checkout back after flavored icon staging
electron-builder resolves every piece of packaging artwork from the workspace
— the app icon, the icon.ico extraResource, the MSIX logo set before-build
stages into build/appx, and the exe identity stamp — so a flavored canary or
commit build copies its rendered icon set over apps/desktop/assets before
packaging.

Those files stopped being gitignored in 4b7229d612, which turned a write that
was invisible into 15 dirty paths. The source custody check then refuses the
checkout twice: inside the same build, at the check that used to sit after the
copy, and on every later build from the first check on a now permanently dirty
tree.

Hold the render at the workspace path for the packaging window only and
restore the admitted files afterwards, whether packaging finished or failed.
The final custody check now asserts the build handed the checkout back.
2026-09-29 12:29:47 -04:00

276 lines
14 KiB
Python

"""Prepared desktop inputs bind a source and reject stale files before building."""
from __future__ import annotations
import json
import os
from pathlib import Path
import subprocess
import sys
import pytest
def _project(tmp_path: Path) -> tuple[Path, str]:
from pm.store import current_target
source = tmp_path / "source with spaces"
source.mkdir()
(source / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="1.2.3"\n', encoding="utf-8")
(source / "package.json").write_text('{"workspaces": ["apps/desktop"]}', encoding="utf-8")
(source / "package-lock.json").write_text('{"packages": {}}', encoding="utf-8")
(source / ".gitignore").write_text(".build/\n", encoding="utf-8")
(source / "pm").mkdir()
(source / "pm/lock.json").write_text(json.dumps({"schema": 1, "packages": {
name: {"version": "1.0.0", "artifacts": {current_target(): {"url": "https://example.test/tool", "sha256": "a" * 64}}}
for name in ("python", "node", "npm")
}}), encoding="utf-8")
subprocess.run(["git", "init", str(source)], check=True, capture_output=True)
subprocess.run(["git", "add", "."], cwd=source, check=True)
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "fixture"], cwd=source, check=True, capture_output=True)
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
return source, commit
def test_stable_build_accepts_the_admitted_commit_before_the_final_tag_exists(tmp_path, monkeypatch):
from scripts.bundles.desktop_inputs import identity_environment
from scripts.bundles.desktop_prepare import BuildRequest
source, commit = _project(tmp_path)
subprocess.run([
"git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
"-c", "tag.gpgSign=false", "tag", "-a", "rc.1-v1.2.4", "-m", "claim",
], cwd=source, check=True, env={**__import__("os").environ,
"GIT_COMMITTER_DATE": "2026-08-29T01:02:03Z"})
monkeypatch.setenv("RELEASE_CLAIM_TAG", "rc.1-v1.2.4")
claim_object = subprocess.check_output(
["git", "rev-parse", "rc.1-v1.2.4"], cwd=source, text=True,
).strip()
monkeypatch.setenv("RELEASE_CLAIM_OBJECT", claim_object)
request = BuildRequest.create(
source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
work=tmp_path / "work", cache=tmp_path / "cache", bundle_env={},
)
assert request.commit == commit
assert request.version == "1.2.4"
# The payload identity stays plain; the attempt ref lives only in the claim env.
assert request.tag == "v1.2.4"
assert request.release_epoch == 1787965323
env = identity_environment(request, "bundled", {"HERMES_RELEASE_EPOCH": "1"})
assert env["HERMES_RELEASE_EPOCH"] == "1787965323"
monkeypatch.setenv("RELEASE_CLAIM_OBJECT", "f" * 40)
with pytest.raises(ValueError, match="exact claim tag object"):
BuildRequest.create(
source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
work=tmp_path / "wrong-work", cache=tmp_path / "wrong-cache", bundle_env={},
)
def test_native_version_capture_uses_admitted_build_environment(tmp_path, monkeypatch):
from scripts.bundles.desktop import capture
monkeypatch.delenv("HERMES_RELEASE_EPOCH", raising=False)
env = {**os.environ, "HERMES_RELEASE_EPOCH": "1787965323"}
script = "import os; print(os.environ['HERMES_RELEASE_EPOCH'])"
assert capture([sys.executable, "-c", script], tmp_path, env) == env["HERMES_RELEASE_EPOCH"]
def test_stable_build_rejects_a_claim_tag_for_another_version(tmp_path, monkeypatch):
from scripts.bundles.desktop_prepare import BuildRequest
source, commit = _project(tmp_path)
subprocess.run([
"git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
"-c", "tag.gpgSign=false", "tag", "-a", "rc.2-v1.2.5", "-m", "claim",
], cwd=source, check=True, env={**__import__("os").environ,
"GIT_COMMITTER_DATE": "2026-08-29T01:02:03Z"})
monkeypatch.setenv("RELEASE_CLAIM_TAG", "rc.2-v1.2.5")
monkeypatch.setenv("RELEASE_CLAIM_OBJECT", subprocess.check_output(
["git", "rev-parse", "rc.2-v1.2.5"], cwd=source, text=True).strip())
with pytest.raises(ValueError, match="exact claim tag"):
BuildRequest.create(
source, tag="v1.2.4", commit=None, release_commit=commit, variant="bundled",
work=tmp_path / "work", cache=tmp_path / "cache", bundle_env={},
)
def test_prepared_input_roundtrip_rejects_mutation_and_foreign_source(tmp_path):
from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop
source, commit = _project(tmp_path)
work = tmp_path / "work"
cache = tmp_path / "cache"
request = BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
from pm.lock import Facts
from pm.store import tree_digest
from pm.registry import get_package
store = cache / "tools"
binaries = {}
for name in ("python", "node", "npm"):
package = get_package(name)
entry = store / package.store_entry("1.0.0", request.target)
binary = package.binary(entry, request.target)
assert binary is not None
binary.parent.mkdir(parents=True)
binary.write_bytes(b"published tool fixture")
binaries[name] = binary
Facts(store / "facts.json").record(name, "1.0.0", entry.name, package.env(entry, request.target), store,
target=request.target, artifacts=["a" * 64], digest=tree_digest(entry))
work.mkdir()
executable, node = binaries["python"], binaries["node"]
icons = work / "icon-environment" / "bin" / "python"
icons.parent.mkdir(parents=True)
icons.write_bytes(b"prepared icon interpreter")
icon_library = work / "icon-environment" / "library"
icon_library.write_bytes(b"prepared library")
native = work / "native"
native.mkdir()
(native / "binding.node").write_bytes(b"prepared native input")
packager = work / "packager.json"
packager.write_text("{}", encoding="utf-8")
prepared = PreparedDesktop.record(request, python=executable, node=node, icon_python=icons,
native=native, packager=packager, payload=None, native_toolchain="fixture-toolchain")
path = work / "prepared.json"
prepared.write(path)
restored = PreparedDesktop.load(path)
restored.validate()
assert restored.request.commit == commit
assert restored.request.version == "1.2.3"
assert "environment" not in json.loads(path.read_text())
icon_library.unlink()
with pytest.raises(ValueError, match="changed|stale|missing"):
restored.validate()
icon_library.write_bytes(b"prepared library")
(native / "binding.node").write_bytes(b"changed")
with pytest.raises(ValueError, match="changed|stale"):
restored.validate()
(native / "binding.node").write_bytes(b"prepared native input")
(source / "pyproject.toml").write_text('[project]\nname="fixture"\nversion="9.9.9"\n', encoding="utf-8")
with pytest.raises(ValueError, match="source|checkout"):
restored.validate()
def test_prepared_paths_cannot_escape_their_owned_roots(tmp_path):
from scripts.bundles.desktop_prepare import BuildRequest, PreparedDesktop
source, commit = _project(tmp_path)
work, cache = tmp_path / "work", tmp_path / "cache"
request = BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
external = tmp_path / "external"
external.write_bytes(b"not preparation-owned")
with pytest.raises(ValueError, match="outside|owned"):
PreparedDesktop.record(request, python=external, node=external, icon_python=external,
native=external, packager=external, payload=None, native_toolchain="fixture-toolchain")
def test_prepared_cli_rejects_missing_result_without_provisioning(tmp_path):
source = Path(__file__).resolve().parents[2]
absent = tmp_path / "absent" / "prepared.json"
result = subprocess.run([sys.executable, str(source / "scripts/bundles/desktop.py"), "--prepared", str(absent)],
cwd=tmp_path, capture_output=True, text=True, timeout=30)
assert result.returncode != 0
assert "preparation" in result.stderr.lower()
assert "unrecognized arguments" not in result.stderr
assert not absent.parent.exists()
def test_source_admission_rejects_dirty_checkout_and_invalid_store_before_writes(tmp_path):
from scripts.bundles.desktop_prepare import BuildRequest
source, commit = _project(tmp_path)
work = tmp_path / "work"
cache = tmp_path / "cache"
for tag, selected in [(None, commit), ("v1.2.0+canary.20260911T120000Z", None)]:
with pytest.raises(ValueError, match="Store.*stable"):
BuildRequest.create(source, tag=tag, commit=selected, variant="store", work=work, cache=cache, bundle_env={})
(source / "package.json").write_text("{}", encoding="utf-8")
(source / "pyproject.toml").write_text('staged fixture contents', encoding="utf-8")
subprocess.run(["git", "add", "pyproject.toml"], cwd=source, check=True)
(source / "generated output").mkdir()
(source / "generated output" / "receipt.json").write_text('untracked fixture contents', encoding="utf-8")
with pytest.raises(ValueError, match="source|checkout") as rejected:
BuildRequest.create(source, tag=None, commit=commit, variant="light", work=work, cache=cache, bundle_env={})
status = subprocess.check_output(
["git", "status", "--porcelain", "--untracked-files=all"], cwd=source, text=True,
).rstrip("\r\n")
assert str(rejected.value).endswith("\n" + status)
assert 'generated output/receipt.json' in str(rejected.value)
assert 'fixture contents' not in str(rejected.value)
assert not work.exists()
assert not cache.exists()
def test_preparation_never_reuses_an_unowned_work_directory(tmp_path):
from scripts.bundles.desktop_prepare import BuildRequest, prepare
source, commit = _project(tmp_path)
work = tmp_path / "someone-elses-files"
work.mkdir()
precious = work / "request.json"
precious.write_text("do not replace", encoding="utf-8")
request = BuildRequest.create(source, tag=None, commit=commit, variant="light",
work=work, cache=tmp_path / "cache", bundle_env={})
with pytest.raises(ValueError, match="owned"):
prepare(request)
assert precious.read_text() == "do not replace"
def test_checkout_lock_excludes_a_second_build_process(tmp_path):
from scripts.bundles.desktop_inputs import build_lock
source, _ = _project(tmp_path)
project = Path(__file__).resolve().parents[2]
probe = (
"import sys; from pathlib import Path; sys.path.insert(0,sys.argv[1]); "
"from scripts.bundles.desktop_inputs import build_lock; "
"lock=build_lock(Path(sys.argv[2])); lock.__enter__(); print('acquired'); lock.__exit__(None,None,None)"
)
command = [sys.executable, "-I", "-c", probe, str(project), str(source)]
with build_lock(source):
blocked = subprocess.run(command, capture_output=True, text=True, timeout=30)
assert blocked.returncode != 0 and "another desktop" in blocked.stderr
released = subprocess.run(command, capture_output=True, text=True, timeout=30)
assert released.returncode == 0, released.stderr
assert released.stdout.strip() == "acquired"
def test_flavored_icon_staging_hands_the_admitted_checkout_back(tmp_path):
from scripts.bundles.desktop import flavored_assets
from scripts.bundles.desktop_prepare import require_source
source, commit = _project(tmp_path)
assets = source / "apps/desktop/assets"
(assets / "appx").mkdir(parents=True)
(assets / "icon.png").write_bytes(b"admitted icon")
(assets / "appx/Square150x150Logo.png").write_bytes(b"admitted tile")
subprocess.run(["git", "add", "."], cwd=source, check=True)
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test",
"commit", "-m", "icons"], cwd=source, check=True, capture_output=True)
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=source, text=True).strip()
rendered = tmp_path / "products/icons"
(rendered / "appx").mkdir(parents=True)
(rendered / "icon.png").write_bytes(b"canary icon")
(rendered / "icon-mac.png").write_bytes(b"canary dock icon")
(rendered / "appx/Square150x150Logo.png").write_bytes(b"canary tile")
# Packaging reads artwork from the workspace path, so the render is live
# there — and the custody check rightly refuses the checkout in that window.
with flavored_assets(rendered, assets):
assert (assets / "icon.png").read_bytes() == b"canary icon"
assert (assets / "icon-mac.png").read_bytes() == b"canary dock icon"
with pytest.raises(ValueError, match="clean source checkout"):
require_source(source, commit)
# A finished or failed package leaves the admitted files back in place, with
# render-only additions removed; a dirty tree breaks the next build.
assert (assets / "icon.png").read_bytes() == b"admitted icon"
assert (assets / "appx/Square150x150Logo.png").read_bytes() == b"admitted tile"
assert not (assets / "icon-mac.png").exists()
require_source(source, commit)
with pytest.raises(RuntimeError, match="packaging failed"):
with flavored_assets(rendered, assets):
raise RuntimeError("packaging failed")
require_source(source, commit)