Files
hermes-agent/tests/scripts/test_render_builds_table_commit.py
ethernet c4e2d937f7 fix(desktop): isolate canary and commit package identities
Canary and commit builds must not replace stable or share its desktop
state. Package names alone are insufficient because Electron reads the
product name before main initializes its paths. Pin nonstable userData
before the first lookup, and keep the packaged identity independent of
runtime build variables.

Keep release artifact filenames unchanged. Qualify payload CLI names,
route each nonstable MSIX alias to its own entrypoint, and copy the
immutable desktop provenance into the embedded Python checkout. Only
stable releases can use the official Store identity.

Targeted validation: 75 JavaScript tests passed, 2 platform skips;
15 Python tests passed with file retries disabled. Native Windows SDK
manifest proof is tracked separately. Full app install, signing and macOS
launch validation are not claimed.
2026-09-11 19:59:38 -04:00

271 lines
12 KiB
Python

"""Commit-mode summary renderer: the receipt-bound expected-binary matrix.
The commit summary is a SEPARATE sink from the release-body tables: it
never reads or edits a GitHub release, and every expected binary gets a
row — Built (receipt + object both present, with a link) or Not built —
so a skipped or interrupted leg can never be a silent omission or a
forged success.
"""
from __future__ import annotations
import importlib.util
import json
import re
import sys
from pathlib import Path
import pytest
_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "render-builds-table.py"
_SPEC = importlib.util.spec_from_file_location("render_builds_table", _SCRIPT)
assert _SPEC and _SPEC.loader
rbt = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(rbt)
COMMIT = "a" * 40
BASE = "https://cdn.example.com"
def _leg_for(basename: str) -> str:
"""The receipt name whose regex matches this artifact basename."""
import re
for _label, leg, pattern in rbt._COMMIT_EXPECTED:
if re.match(pattern, basename):
return leg
raise AssertionError(f"no expected row matches {basename!r}")
# Every produced binary of a fully green commit run (Termux's .deb is a
# nested deb/ path — the workflow stages it under deb/pool/).
_ALL_BASENAMES = [
"HermesBundled-0.28.0-win-x64.msix",
"HermesBundled-0.28.0-win-arm64.msix",
"HermesBundled-0.28.0-win.msixbundle",
"HermesBundled-0.28.0-mac-arm64.dmg",
"HermesBundled-0.28.0-mac-x64.dmg",
"HermesBundled-0.28.0-mac-arm64.zip",
"HermesBundled-0.28.0-mac-x64.zip",
"deb/pool/hermes-agent_0.28.0_aarch64.deb",
]
def _receipts_all_built() -> dict[str, dict | None]:
"""Validated receipts: each leg lists exactly the artifact basenames
the matrix expects from it (the real staged shape)."""
receipts: dict[str, dict | None] = {name: None for name in rbt.COMMIT_RECEIPT_NAMES}
for basename in _ALL_BASENAMES:
leg = _leg_for(basename.rsplit("/", 1)[-1])
receipt = receipts[leg] or {"schema": 2, "commit": COMMIT, "name": leg, "files": []}
receipt["files"].append({"path": basename, "size": 1, "sha256": "0" * 64})
receipts[leg] = receipt
return receipts
def _names(*basenames: str) -> list[str]:
return [f"releases/commit/{COMMIT}/{name}" for name in basenames]
def _all_built_names() -> list[str]:
return _names(*_ALL_BASENAMES)
def test_every_expected_binary_gets_a_row_built_or_not():
receipts = _receipts_all_built()
rows = rbt.commit_expected_rows(_all_built_names(), receipts)
assert len(rows) == len(rbt._COMMIT_EXPECTED)
for row in rows:
assert row["state"] == "built", row
summary = rbt.render_commit_summary(_all_built_names(), BASE, COMMIT, receipts)
assert summary.count("✅ Built") == len(rbt._COMMIT_EXPECTED)
# zip/Termux rows exist in the COMMIT summary (the release-body
# table hides zips on purpose; the commit summary shows every binary).
assert any("ZIP" in row["label"] for row in rows)
assert any("Termux" in row["label"] for row in rows)
assert not any("Store" in row["label"] for row in rows)
assert any("MSIXBUNDLE" in row["label"] for row in rows)
# The universal sideload bundle links correctly.
bundle = next(row for row in rows if "MSIXBUNDLE" in row["label"])
assert bundle["key"].endswith(".msixbundle")
def test_missing_binaries_are_explicit_rows_never_links():
receipts = _receipts_all_built()
names = _names("HermesBundled-0.28.0-win-x64.msix")
summary = rbt.render_commit_summary(names, BASE, COMMIT, receipts)
built = sum(1 for line in summary.splitlines() if "✅ Built" in line)
missing = sum(1 for line in summary.splitlines()
if "❌ Not built" in line and "release leg disabled" not in line)
assert built == 1
assert missing == len(rbt._COMMIT_EXPECTED) - built
# No link for an object that does not exist.
assert "win-arm64.msix)" not in summary
# Failed legs are blamed by name on the RECEIPT-missing rows; without
# blame the row says incomplete.
darwin_down = dict(receipts)
darwin_down["darwin-arm64"] = None
blamed = rbt.render_commit_summary(names, BASE, COMMIT, darwin_down, failed_legs=["build-darwin"])
assert "failed: build-darwin" in blamed
assert "Not built (leg incomplete or upload interrupted)" in rbt.render_commit_summary(names, BASE, COMMIT, darwin_down)
def test_object_without_completion_receipt_never_renders_built():
"""The old draft trusted raw object names: an interrupted upload that
left artifacts but no receipt looked ✅ Built. Now the receipt is the
completion marker."""
receipts = _receipts_all_built()
names = _all_built_names() # every object present...
for leg in ("win32-x64", "termux"):
receipts[leg] = None # ...two legs' receipts absent
summary = rbt.render_commit_summary(names, BASE, COMMIT, receipts)
for row in rbt.commit_expected_rows(names, receipts):
if row["leg"] in ("win32-x64", "termux"):
assert row["state"] != "built"
assert f"| {row['label']} | ✅ Built |" not in summary
else:
assert row["state"] == "built"
def test_orphan_object_with_unrelated_valid_receipt_never_renders_built():
"""An object matching a row's shape, staged under a leg whose VALID
receipt covers only unrelated files, is NOT evidence that the row's
binary was built. The old draft accepted ANY receipt object for the
leg; rows now bind to the receipt's own file paths."""
receipts = _receipts_all_built()
# win32-x64's receipt is valid but lists a DIFFERENT file.
receipts["win32-x64"] = {"schema": 2, "commit": COMMIT, "name": "win32-x64",
"files": [{"path": "metadata-windows-x64.json",
"size": 1, "sha256": "0" * 64}]}
names = _all_built_names() # the orphan win-x64 msix object exists
rows = rbt.commit_expected_rows(names, receipts)
x64 = next(row for row in rows if row["label"] == "Windows x64 (MSIX)")
assert x64["state"] != "built"
assert x64["key"] is None
summary = rbt.render_commit_summary(names, BASE, COMMIT, receipts)
assert "| Windows x64 (MSIX) | ✅ Built |" not in summary
def test_receipt_without_object_is_reported_not_built():
receipts = _receipts_all_built()
summary = rbt.render_commit_summary([], BASE, COMMIT, receipts)
assert "Not built (receipt present but object missing)" in summary
assert "✅ Built" not in summary
def test_ambiguous_or_extra_objects_never_render_a_link():
"""Two objects that BOTH match a row's shape AND are listed by the
leg's receipt are genuinely ambiguous — no link is rendered. An extra
object the receipt does NOT list cannot poison a row whose own
receipt-listed object is unique (it is foreign to the binding)."""
receipts = _receipts_all_built()
# List both versions in win32-x64's receipt (a torn staging would
# leave exactly this shape behind two shape-matching objects).
receipts["win32-x64"]["files"].append(
{"path": "HermesBundled-0.28.1-win-x64.msix", "size": 1, "sha256": "0" * 64})
names = _names(
"HermesBundled-0.28.0-win-x64.msix",
"HermesBundled-0.28.1-win-x64.msix",
)
rows = rbt.commit_expected_rows(names, receipts)
x64 = next(row for row in rows if row["label"] == "Windows x64 (MSIX)")
assert x64["state"] == "ambiguous" and x64["key"] is None
# The extra 0.28.1 object without a receipt listing is NOT ambiguous:
# the receipt's own 0.28.0 binding stays built.
plain = _receipts_all_built()
rows = rbt.commit_expected_rows(
_names("HermesBundled-0.28.0-win-x64.msix",
"HermesBundled-0.28.1-win-x64.msix"), plain)
x64 = next(row for row in rows if row["label"] == "Windows x64 (MSIX)")
assert x64["state"] == "built"
assert x64["key"].endswith("HermesBundled-0.28.0-win-x64.msix")
def test_failed_legs_from_release_needs_seam():
needs = json.dumps({
"build-win32": {"result": "success"},
"build-darwin": {"result": "failure"},
"termux-deb": {"result": "skipped"},
"build-linux": {"result": "failure"},
})
assert rbt.failed_legs_from_release_needs(needs) == ["build-darwin", "build-linux"]
assert rbt.failed_legs_from_release_needs(None) == []
assert rbt.failed_legs_from_release_needs("") == []
assert rbt.failed_legs_from_release_needs("not json") == []
def test_summary_uses_exact_nested_keys_and_lists_the_sideload_bundle():
from scripts.releases import handoff
paths = {
"windows-universal": [
"HermesBundled-0.28.0.0-win.msixbundle",
],
"termux": ["deb/pool build/hermes-agent_0.28.0_aarch64.deb"],
}
receipts = {
leg: {"schema": 2, "commit": COMMIT, "name": leg,
"files": [{"path": path, "size": 1, "sha256": "0" * 64} for path in files]}
for leg, files in paths.items()
}
for leg, receipt in receipts.items():
handoff.validate_commit_receipt(receipt, COMMIT, leg)
names = _names(*(path for files in paths.values() for path in files))
summary = rbt.render_commit_summary(names, BASE + "/downloads", COMMIT, receipts)
from urllib.parse import quote
for key in names:
assert f"]({BASE}/downloads/{quote(key, safe='/')})" in summary
built = [line for line in summary.splitlines() if "✅ Built" in line]
assert len(built) == len(names)
assert not any("Store" in line for line in built)
assert "Linux x64" in summary and "Linux ARM64" in summary
def test_commit_page_matches_the_summary_rows():
"""Two sinks, ONE row set: every link and every status in the step
summary appears on the page, and neither sink drops a missing binary."""
receipts = _receipts_all_built()
names = _all_built_names()
summary = rbt.render_commit_summary(names, BASE, COMMIT, receipts)
page = rbt.render_commit_page(COMMIT, names, BASE, receipts)
links = re.findall(r"\]\((https?://[^)]+)\)", summary)
assert len(links) == len(rbt._COMMIT_EXPECTED)
for url in links:
assert f'href="{url}"' in page
assert page.count("✅ Built") == summary.count("✅ Built") == len(rbt._COMMIT_EXPECTED)
assert page.count("❌ Not built") == summary.count("❌ Not built") == len(rbt._COMMIT_DISABLED)
assert rbt.recorded_build(page) == COMMIT
def test_commit_page_lists_a_missing_binary_without_a_link():
receipts = _receipts_all_built()
receipts["darwin-arm64"] = None
names = _names("HermesBundled-0.28.0-win-x64.msix")
page = rbt.render_commit_page(COMMIT, names, BASE, receipts, failed_legs=["build-darwin"])
assert page.count("<tr>") == len(rbt._COMMIT_EXPECTED) + len(rbt._COMMIT_DISABLED) + 1
assert "failed: build-darwin" in page
# Only the one staged object was built, so it is the only anchor.
assert page.count("<a href=") == 1
def test_commit_run_publishes_the_commit_page(tmp_path, monkeypatch, capsys):
"""The real CLI path: commit mode writes the summary AND the page."""
summary = tmp_path / "summary.md"
uploads: list[tuple[str, str, bool]] = []
receipts = _receipts_all_built()
monkeypatch.setattr(rbt.r2, "list_objects", lambda prefix="": {"keys": _all_built_names()})
monkeypatch.setattr(rbt.handoff, "read_commit_receipt", lambda commit, name: receipts[name])
monkeypatch.setattr(rbt.r2, "put", lambda tag, key, file, key_is_full=False, immutable=False:
uploads.append((key, Path(file).read_text(encoding="utf-8"), key_is_full)))
monkeypatch.setattr(sys, "argv", ["render-builds-table.py", "--summary-commit", COMMIT,
"--summary-out", str(summary), "--r2-base-url", BASE])
assert rbt.main() == 0
assert len(uploads) == 1
key, page, key_is_full = uploads[0]
assert key == f"releases/commit/{COMMIT}/index.html" and key_is_full
assert f"✓ Page {BASE}/releases/commit/{COMMIT}/index.html" in capsys.readouterr().out
written = summary.read_text(encoding="utf-8")
for url in re.findall(r"\]\((https?://[^)]+)\)", written):
assert f'href="{url}"' in page
assert written.count("✅ Built") == page.count("✅ Built") == len(rbt._COMMIT_EXPECTED)