Files
hermes-agent/tests/ci/test_desktop_bundle_smoke.py
ethernet 9a746d407b test(ci): drop desktop bundle smoke tests that only read the workflow
Five tests loaded desktop-bundled-release.yml and asserted on its shape or
evaluated its `if:` expressions with a mini evaluator. They restate the YAML,
so they fail on correct edits and pass on subtly wrong ones. The tests that
execute the workflow's shell steps against a fake R2 stay.
2026-10-02 15:10:43 -04:00

268 lines
15 KiB
Python

"""Replay artifact handoffs and publication against a fake R2."""
import hashlib
import json
import shutil
import subprocess
import xml.etree.ElementTree as ET
import zipfile
from pathlib import Path
import hermes_yaml
import pytest
from tests.ci.desktop_release_roles import (
CANARY_TAG as TAG, NATIVE_TARGETS, SHA, native_builds, needs_of, phase_result, selection_gates,
smoke_callers, stage_step, termux_builder, universal_assembler, updater_publishers,
)
from tests.ci.test_commit_build_staging import ROOT, shell_step
from tests.ci.test_desktop_release_tag_admission import _child_env, _workflow
from tests.scripts.test_release_r2 import r2_server # noqa: F401
def smoke_workflow():
return hermes_yaml.safe_load((ROOT / '.github/workflows/desktop-bundle-smoke.yml').read_text(encoding='utf-8-sig'))
def smoke_fetch_script():
"""The public artifact fetch every smoke runner shares."""
scripts = {step['run'] for job in smoke_workflow()['jobs'].values()
for step in job.get('steps', []) if step.get('id') == 'artifact'}
(script,) = scripts
return script
def transport_env(tmp_path, server, *, commit=False):
return dict(HERMES_PAYLOAD_TAG='' if commit else TAG, HERMES_BUILD_COMMIT=SHA if commit else '',
RELEASE_TAG='' if commit else TAG, RELEASE_COMMIT=SHA, COMMIT_BUILD=str(commit).lower(),
PUBLIC_BASE=f'http://127.0.0.1:{server.server_port}/hermes-releases',
CLOUDFLARE_R2_PUBLIC_URL=f'http://127.0.0.1:{server.server_port}/hermes-releases',
CLOUDFLARE_R2_ACCOUNT_ID='loopback', CLOUDFLARE_R2_ACCESS_KEY_ID='test-inert',
CLOUDFLARE_R2_SECRET_ACCESS_KEY='test-inert', CLOUDFLARE_R2_BUCKET='hermes-releases',
RELEASE_PHASE='', SMOKE_ROOT=str(tmp_path / 'smoke'), GITHUB_OUTPUT=str(tmp_path / 'output'))
@pytest.mark.parametrize('commit', [False, True])
@pytest.mark.parametrize('platform,fmt', [('darwin', 'dmg'), ('darwin', 'zip'), ('win32', 'msix'), ('win32', 'msixbundle')])
@pytest.mark.parametrize('arch', ['arm64', 'x64'])
def test_public_smoke_fetches_the_receipt_bound_native_format(tmp_path, r2_server, commit, platform, fmt, arch):
env = transport_env(tmp_path, r2_server, commit=commit)
release = tmp_path / 'apps/desktop/release'
release.mkdir(parents=True)
suffix = f'mac-{arch}.{fmt}' if platform == 'darwin' else ('win.msixbundle' if fmt == 'msixbundle' else f'win-{arch}.msix')
filename = f'HermesBundled-0.28.0-{suffix}'
payload = b'transport fixture only: not a deployable package'
(release / filename).write_bytes(payload)
(release / ('Store-' + filename)).write_bytes(b'not eligible')
universal = fmt == 'msixbundle'
jobs = _workflow()['jobs']
producer = universal_assembler(jobs) if universal else native_builds(jobs)[(f'{platform}-{arch}', 'commit')]
if platform == 'darwin':
# The producer stages all of its formats together; smoke selects one.
for ext in ('dmg', 'zip', 'zip.blockmap'):
(release / f'HermesBundled-0.28.0-mac-{arch}.{ext}').write_bytes(payload)
(release / f'{arch}-canary-mac.yml').write_bytes(payload)
staged = shell_step(tmp_path, r2_server, '', '', {**env, 'TARGET': f'{platform}-{arch}'},
script=stage_step(jobs[producer])['run'])
assert staged.returncode == 0, staged.stdout + staged.stderr
assert all('/canary/' not in key for key in r2_server.store)
script = smoke_fetch_script()
# Deliberately remove every storage credential before executing public fetch.
public_env = {key: '' if key.startswith('CLOUDFLARE_') else value for key, value in env.items()}
public_env.update(PLATFORM=platform, ARCH=arch, FORMAT=fmt)
r2_server.requests.clear()
result = shell_step(tmp_path, r2_server, '', '', public_env, script=script)
assert result.returncode == 0, result.stdout + result.stderr
selected = Path((tmp_path / 'output').read_text(encoding='utf-8-sig').strip().removeprefix('path='))
assert selected.name == filename and selected.read_bytes() == payload
witness = json.loads((tmp_path / 'smoke/out/download.json').read_text(encoding='utf-8-sig'))
assert witness['commit'] == SHA and witness['artifact']['sha256'] == hashlib.sha256(payload).hexdigest()
assert not any(file.name.startswith('Store-') for file in selected.parent.iterdir())
assert all(method == 'GET' and 'Authorization' not in headers for method, _, headers in r2_server.requests)
@pytest.mark.parametrize('fault', ['missing', 'ambiguous', 'wrong-commit', 'corrupt'])
def test_download_faults_never_export_an_accepted_artifact(tmp_path, r2_server, fault):
env = transport_env(tmp_path, r2_server, commit=True)
filename = 'HermesBundled-0.28.0-win-x64.msix'
payload = b'inert integrity fixture'
row = {'path': filename, 'size': len(payload), 'sha256': hashlib.sha256(payload).hexdigest()}
receipt = {'schema': 2, 'commit': 'b' * 40 if fault == 'wrong-commit' else SHA,
'name': 'win32-x64', 'files': [row]}
prefix = f'releases/commit/{SHA}/'
r2_server.store[prefix + filename] = (b'corrupted' if fault == 'corrupt' else payload, '"e"')
if fault == 'missing':
receipt['files'] = [{**row, 'path': 'Store-' + filename}]
elif fault == 'ambiguous':
second = 'HermesBundled-0.29.0-win-x64.msix'
receipt['files'].append({**row, 'path': second})
r2_server.store[prefix + second] = (payload, '"e"')
r2_server.store[prefix + 'handoff-win32-x64.json'] = (json.dumps(receipt).encode(), '"e"')
script = smoke_fetch_script()
result = shell_step(tmp_path, r2_server, '', '', {**env, 'PLATFORM': 'win32', 'ARCH': 'x64', 'FORMAT': 'msix'}, script=script)
assert result.returncode != 0
assert not (tmp_path / 'output').exists()
assert not (tmp_path / 'smoke/out/download.json').exists()
def test_stable_phase_result_requires_smoke_but_preserves_other_phases(tmp_path, r2_server):
jobs = _workflow()['jobs']
gates, smokes = selection_gates(jobs), smoke_callers(jobs)
group_jobs = {target: [gates[target], smokes[target]] for target in NATIVE_TARGETS}
group_jobs['win32-bundle'] = [universal_assembler(jobs)]
group_jobs['termux'] = [termux_builder(jobs)]
verdict = phase_result(jobs)
members = {member for group in group_jobs.values() for member in group}
# What the verdict needs beyond admission and the build groups is the
# publication itself; the Store submission follows the file publication.
(publish,) = [name for name in needs_of(jobs[verdict]) if name not in members | {'validate'}
and not set(needs_of(jobs[name])) - {'validate'}]
(store,) = [name for name in needs_of(jobs[verdict]) if publish in needs_of(jobs[name])]
def run_phase(phase, selected, failed=None, skip_tests=False):
needs = {name: {'result': 'success'} for name in needs_of(jobs[verdict])}
needs['validate']['outputs'] = {group: ('true' if group in selected else 'false')
for group in group_jobs}
for group, group_members in group_jobs.items():
if group not in selected:
for member in group_members:
needs[member]['result'] = 'skipped'
if skip_tests:
for smoke in smokes.values():
needs[smoke]['result'] = 'skipped'
if failed:
needs[failed]['result'] = 'cancelled'
(step,) = [step for step in jobs[verdict]['steps'] if 'SKIP_TESTS' in (step.get('env') or {})]
return shell_step(tmp_path, r2_server, '', '',
{'RELEASE_NEEDS': json.dumps(needs), 'RELEASE_PHASE': phase,
'SKIP_TESTS': 'true' if skip_tests else 'false'}, script=step['run'])
every = set(group_jobs)
assert run_phase('candidate', every).returncode == 0
# A claim that skipped tests runs no smoke, and every build still counts.
assert run_phase('candidate', every, skip_tests=True).returncode == 0
assert run_phase('candidate', every, failed=gates['win32-x64'], skip_tests=True).returncode != 0
assert run_phase('publish', every).returncode == 0
assert run_phase('publish', every - {'termux'}).returncode == 0
for group in group_jobs:
# A group that was not selected is not a failure of the phase.
assert run_phase('candidate', every - {group}).returncode == 0, group
for failed in (smokes['darwin-arm64'], gates['win32-x64'], termux_builder(jobs)):
assert run_phase('candidate', every, failed=failed).returncode != 0, failed
assert run_phase('candidate', every, failed=publish).returncode == 0
# A partial selection still judges the groups it selected.
assert run_phase('candidate', {'darwin-arm64'}).returncode == 0
assert run_phase('candidate', {'darwin-arm64'}, failed=smokes['darwin-arm64']).returncode != 0
assert run_phase('publish', set(), failed=store).returncode != 0
def test_canary_publisher_consumes_staged_bytes_and_writes_pointer_last(tmp_path, r2_server):
tag = 'v0.28.1+canary.20260818T101010Z'
env = {**transport_env(tmp_path, r2_server), 'HERMES_DESKTOP_VARIANT': 'bundled',
'HERMES_PAYLOAD_TAG': tag, 'RELEASE_TAG': tag}
# Use the real assembly identity derivation with a stable base available.
for file in ['scripts/msix-shared.mjs', 'scripts/release-content-types.json',
'apps/desktop/product-identity.cjs', 'apps/desktop/package.json']:
target = tmp_path / file
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / file, target)
def git(*args, date='2026-08-18T08:10:10Z'):
subprocess.run(['git', *args], cwd=tmp_path, check=True, capture_output=True,
env=_child_env(GIT_AUTHOR_NAME='Fixture', GIT_AUTHOR_EMAIL='fixture@example.invalid',
GIT_COMMITTER_NAME='Fixture', GIT_COMMITTER_EMAIL='fixture@example.invalid',
GIT_AUTHOR_DATE=date, GIT_COMMITTER_DATE=date,
GIT_CONFIG_GLOBAL=str(tmp_path / 'no-git-config'), GIT_CONFIG_NOSYSTEM='1'))
def assembly_identity():
result = subprocess.run(['node', '--input-type=module', '-e',
'import {appIdentity} from "./scripts/msix-shared.mjs";'
'console.log(JSON.stringify(appIdentity(process.cwd()+"/apps/desktop")));'],
cwd=tmp_path, env=_child_env(**env), check=True, capture_output=True, text=True)
return json.loads(result.stdout)
git('init', '-q')
git('-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-qm', 'stable base')
git('tag', 'v0.28.0')
assembled = assembly_identity()
version = assembled['version']
release = tmp_path / 'apps/desktop/release'
release.mkdir(parents=True)
filename = f"{assembled['name']}-{version}-win.msixbundle"
bundle = release / filename
with zipfile.ZipFile(bundle, 'w') as archive:
archive.writestr('AppxMetadata/AppxBundleManifest.xml',
'<Bundle><Identity Name="NousResearch.HermesBundledCanary" '
'Publisher="CN=Nous Research Inc., O=Nous Research Inc., L=Austin, S=Texas, C=US" '
f'Version="{version}"/></Bundle>')
tested_bytes = bundle.read_bytes()
jobs = _workflow()['jobs']
publisher = updater_publishers(jobs)['win32']
staged = shell_step(tmp_path, r2_server, '', '', env, script=stage_step(jobs[universal_assembler(jobs)])['run'])
assert staged.returncode == 0, staged.stdout + staged.stderr
assert all(key.startswith(f'releases/tag/{tag}/') for key in r2_server.store)
# A newer stable becomes visible after assembly but cannot alter the
# timestamp-derived canary identity.
git('-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-qm', 'new stable', date='2026-08-18T11:10:10Z')
git('tag', 'v0.28.1')
assert assembly_identity()['version'] == version
for name in ('Retrieve the tested universal bundle', 'Publish identical tested bytes without rebuilding'):
result = shell_step(tmp_path, r2_server, publisher, name, env)
assert result.returncode == 0, result.stdout + result.stderr
assert (tmp_path / 'staged' / filename).read_bytes() == tested_bytes
assert r2_server.store[f'releases/win32/canary/{filename}'][0] == tested_bytes
writes = [path for method, path, _ in r2_server.requests if method == 'PUT']
assert writes[-1].endswith('/canary.appinstaller')
descriptor = ET.fromstring(r2_server.store['releases/win32/canary/canary.appinstaller'][0])
main_bundle = descriptor.find('{*}MainBundle')
assert main_bundle is not None
assert descriptor.get('Version') == main_bundle.get('Version') == version
# The publication job must refuse an ambiguous envelope, even when a
# caller accidentally broadens the receipt selector in future.
(tmp_path / 'staged/HermesBundled-0.29.0.0-win.msixbundle').write_bytes(tested_bytes)
before = len(writes)
refused = shell_step(tmp_path, r2_server, publisher,
'Publish identical tested bytes without rebuilding', env)
assert refused.returncode != 0
assert len([row for row in r2_server.requests if row[0] == 'PUT']) == before
(tmp_path / 'staged/HermesBundled-0.29.0.0-win.msixbundle').unlink()
# Filename, tag base, and baked identity must still agree. Reading the
# accepted assembly version is not permission to trust arbitrary metadata.
staged_bundle = tmp_path / 'staged' / filename
for field, wrong in [('Version', '0.28.1.0'), ('Name', 'NousResearch.Other'), ('Publisher', 'CN=Other')]:
with zipfile.ZipFile(bundle) as archive:
manifest = ET.fromstring(archive.read('AppxMetadata/AppxBundleManifest.xml'))
native = manifest.find('Identity')
assert native is not None
native.set(field, wrong)
with zipfile.ZipFile(staged_bundle, 'w') as archive:
archive.writestr('AppxMetadata/AppxBundleManifest.xml', ET.tostring(manifest))
refused = shell_step(tmp_path, r2_server, publisher,
'Publish identical tested bytes without rebuilding', env)
assert refused.returncode != 0, field
assert len([row for row in r2_server.requests if row[0] == 'PUT']) == before
staged_bundle.write_bytes(tested_bytes)
for wrong in ['HermesBundled-0.29.0.0-win.msixbundle', 'HermesBundled-0.28.1.65536-win.msixbundle',
'HermesBundled-0.28.1-canary.20260818101010-win.msixbundle']:
renamed = staged_bundle.rename(staged_bundle.with_name(wrong))
refused = shell_step(tmp_path, r2_server, publisher,
'Publish identical tested bytes without rebuilding', env)
assert refused.returncode != 0, wrong
assert len([row for row in r2_server.requests if row[0] == 'PUT']) == before
renamed.rename(staged_bundle)