Files
hermes-agent/.github/workflows/tests-os.yml
ethernet cd0f97f833 feat(python): pin bundled runtime to 3.14 everywhere (pm, termux lane, CI, installers)
pm python node: 3.14.7+20260901 (freshest python-build-standalone 3.14
build) for the 6 desktop targets; the bionic row moves from the third-party
TUR python3.11 deb to the official termux-main python_3.14.6-1 deb (which
lags PBS by one patch — pinned manually, documented). All 7 digests fetched
from the live sources (PBS release API + termux-main Packages index).
pm/packages.py: main_bin_rel python3.14, deb_package python, bionic fetch
constant, latest_versions guards bionic (no PBS build exists).

termux lane: PYTHON_ABI cp311->cp314, python3.11->python3.14 paths,
libpython3.11.so->3.14, TARGET_ENV 3.11.15->3.14.6 AND sys_platform
linux->android (CPython 3.13+ reports 'android', docs-verified) — linux-
gated markers no longer admit the termux target. runtime_libs.json needs no
change: every python 3.14.6-1 Depends is already staged.

CI: python-version/--python 3.11->3.14 across all 11 workflows incl. the
uv lockfile-check lane. Installers derive the minor from the lock already;
fallbacks bumped. Sandbox images nikolaik/python-nodejs:python3.11-nodejs20
-> python3.14-nodejs22 (tag exists). runtime_repair fall-forward cap now
tracks the <3.15 requires-python window. Docs/README python version claims
updated.
2026-09-07 14:19:18 -04:00

179 lines
7.9 KiB
YAML

name: OS-specific tests
# Runs the tests that can only be trusted on their own host OS.
#
# The main Python suite (.github/workflows/tests.yml) runs on
# ubuntu-latest and covers everything that is either platform-agnostic or
# genuinely Linux-specific. Tests whose subject is macOS- or
# Windows-specific behaviour carry a marker (see the ``_OS_MARKS`` block
# comment in tests/conftest.py) and are SKIPPED on Linux, because faking
# ``sys.platform`` on a Linux runner selects the branch under test without
# reproducing any of the OS behaviour that branch exists for. This workflow
# is where those markers actually execute:
#
# macos → ``-m platforms`` on macos-latest
# windows → ``-m platforms`` on windows-latest-32-core
#
# (tests/conftest.py's ``pytest_collection_modifyitems`` hook skips
# foreign-OS ``platforms(...)`` markers on each host, so one shared ``-m
# platforms`` expression selects "this host's own marked tests" on either
# lane — the helper narrows WHICH FILES get imported first.)
#
# Deliberately NOT sliced. The marked set is small (tens of tests, not
# thousands), so one plain ``pytest`` process per OS is both faster and far
# less machinery than the per-file parallel runner the Linux lane uses.
# Each lane FAILS when it selects zero tests (pytest exit code 5) — a
# renamed marker or bad selector can never report a green job that ran
# nothing.
on:
workflow_call:
inputs:
desktop_updater:
description: >-
Run the Windows desktop-update hand-off integration tests
(tests/test_desktop_update_windows_*.py). These spawn the real
scripts/desktop-update/windows.ps1 and poll its loopback server, so
they carry process-timing noise a shared runner amplifies; the
caller gates them on the classifier's desktop_updater lane so a PR
that never touched that surface cannot be failed by it. Push /
dispatch runs fail open (classifier sets every lane true).
type: boolean
required: false
default: true
permissions:
contents: read
concurrency:
group: tests-os-${{ github.ref }}
cancel-in-progress: true
jobs:
os-tests:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- name: macOS-only tests
runner: macos-latest
marker: macos
- name: Windows-only tests
runner: windows-latest-32-core
marker: windows
steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0
with:
# Pinned for the same reason as the Linux lane: unpinned, setup-uv
# resolves "latest" by fetching a manifest on every job and a
# transient fetch failure fails the whole job.
version: "0.9.28"
enable-cache: true
cache-dependency-glob: |
pyproject.toml
uv.lock
- name: Set up Python 3.14
uses: ./.github/actions/retry
with:
command: uv python install 3.14
- name: Install dependencies
# Same extras as the Linux test lane so an OS-marked test can import
# anything its Linux siblings can. ``[all]`` is deliberately
# Windows/macOS-installable (see the policy comment on the extra in
# pyproject.toml — matrix/python-olm was removed from it precisely
# because it could not build here).
uses: ./.github/actions/retry
with:
command: uv sync --locked --python 3.14 --extra all --extra dev --extra anthropic --extra mistral --extra fal --extra modal --extra daytona --extra hindsight --extra parallel-web
- name: Minimize uv cache
run: uv cache prune --ci
- name: Run ${{ matrix.marker }} tests
# scripts/run_tests.sh — the canonical runner, same as every other
# lane: per-file subprocess isolation (run_tests_parallel.py). It
# natively tolerates per-file empty collections (a platform-gated
# file collects nothing after -m filtering, exit 5, treated as a
# pass for that file) and fails the run itself when NOTHING was
# collected across all files (exit 2) — the zero-tests guard the
# bare-pytest variant implemented by hand with an exit-5 branch.
#
# Selection still narrows WHICH FILES run:
# scripts/ci/list_os_marked_tests.py emits the file list (it exits
# non-zero when the marker matches no file at all); the list rides
# to the runner via ``--files`` so an unrelated ImportError fails
# only ITS file, red and visible, without poisoning the lane's
# other files. ``-m`` stays authoritative for which TESTS run —
# passed after ``--`` so the runner routes it to every per-file
# pytest invocation (the flag REPLACES pyproject's addopts, hence
# repeating ``not integration``).
#
# ``--timeout-method`` needs no override: tests/conftest.py's
# pytest_configure already downgrades the signal-based timer on
# Windows, which has no SIGALRM.
shell: bash
run: |
set -uo pipefail
LIST="${RUNNER_TEMP:-.}/selected-tests.txt"
# Process substitution would hide the helper's exit status, so write
# to a file and check it explicitly.
if ! uv run --no-sync python scripts/ci/list_os_marked_tests.py \
"${{ matrix.marker }}" > "$LIST"; then
echo "::error::could not enumerate ${{ matrix.marker }} test files"
exit 1
fi
if [ ! -s "$LIST" ]; then
echo "::error::empty ${{ matrix.marker }} file list"
exit 1
fi
echo "selected file(s) for ${{ matrix.marker }}:"
cat "$LIST"
# Desktop-update hand-off integration tests spawn the real
# windows.ps1; deselect them unless the PR touched that surface
# (see the workflow_call input). ``--ignore-glob`` keeps the file
# list above intact, so a renamed test file still trips the
# zero-tests guard rather than silently vanishing.
# (bash 3.2 on the macOS runner: an empty array under ``set -u`` is
# an unbound-variable error, hence the ``${arr[@]+...}`` idiom.)
EXTRA_ARGS=()
if [ "${{ inputs.desktop_updater }}" != "true" ]; then
echo "desktop_updater lane off: skipping tests/test_desktop_update_windows_*.py"
EXTRA_ARGS+=(--ignore-glob='*test_desktop_update_windows_*.py')
fi
# ``tr -d '\r'``: on Windows the helper's redirected stdout gains
# CRLF line endings; a stray \r would corrupt the path. The list
# uses the native path-list separator consumed by --files.
#
# Any non-zero exit propagates red: real test failures, or the
# runner's own zero-run guard (every file filtered to empty by
# ``-m`` — "must never pass without running its OS's tests").
SEPARATOR="$(uv run --no-sync python -c 'import os, sys; sys.stdout.write(os.pathsep)')"
FILES="$(tr -d '\r' < "$LIST" | paste -sd "$SEPARATOR" -)"
scripts/run_tests.sh --files "$FILES" -- \
${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"} \
-m "platforms and not integration" \
-v --tb=short
env:
# These files spawn nested PowerShell trees. CPU-count parallelism
# starves cold child startup before the fixture's idle deadline.
HERMES_TEST_WORKERS: ${{ matrix.marker == 'windows' && '8' || '' }}
# Belt-and-suspenders with tests/conftest.py's env blanking: no
# test may reach a real provider API.
OPENROUTER_API_KEY: ""
OPENAI_API_KEY: ""
NOUS_API_KEY: ""