Add a module-scoped `shared_container` fixture to tests/docker/conftest.py
that boots one `sleep infinity` container per test module and tears it
down at module exit. Convert read-only tests that previously used
`docker run --rm --entrypoint sh/cat/test/su` (bypassing s6 to check
static image properties) or `docker run -d` + `docker exec` (starting
identical containers per test) to use `docker exec` on the shared
container instead.
Converted files:
test_immutable_install_permissions.py — 2 throwaway runs → 2 execs
test_license_file_present.py — 1 throwaway run → 1 exec
test_tini_compat_shim.py — 1 throwaway run → 1 exec
test_tui_prebuilt_bundle.py — 2 throwaway runs → 2 execs
test_dump_build_sha.py — 2 throwaway runs → 2 execs
test_immutable_install.py — 3 detached runs → 1 shared + 1 isolated
test_dashboard.py — 2 detached runs → 0 (use shared)
Local profiling shows docker run calls in these 7 files dropped from
~25 to 7 (the 7 are shared_container boots per module + the one test
that needs a restart). Each eliminated `docker run` was paying 1-9s
of s6 cont-init startup; the replacement `docker exec` calls average
0.10s — an ~50x speedup per operation.
Tests that mutate state (restarts, config changes, gateway starts)
still use their own containers via `container_name` + `start_container`.
Two bugs fixed:
1. scripts/run_tests.sh uses env -i (empty environment) and only
passes through a hand-picked set of vars. HERMES_DOCKER_TEST_PROFILE
and HERMES_DOCKER_PROFILE_OUT were not in that list, so the env var
set in the CI workflow was stripped before reaching the pytest
subprocesses — the plugin never activated and no JSON was produced.
2. run_tests_parallel.py spawns each test file in its own subprocess,
so each would write to the same docker-test-profile.json, clobbering
each other. Changed the default output path to include the PID
(docker-test-profile-<pid>.json) and added a CI merge step that
combines all per-PID files into a single docker-test-profile.json
before uploading as an artifact.
Add a pytest profiling plugin (tests/docker/profiling.py) that
instruments every subprocess.run call targeting docker, collecting
per-test and per-subcommand wall-clock timings. Activated via
HERMES_DOCKER_TEST_PROFILE=1 — zero overhead when unset.
The CI docker.yml workflow now enables profiling and uploads the
JSON report as an artifact (docker-test-profile-<arch>), so we can
see exactly which docker operations (run, exec, restart, polling)
dominate the slow CI runs vs. fast local runs.
Output includes:
- Per-subcommand summary (call count, total/avg time, %)
- Top 10 slowest tests by docker operation time
- Top 10 slowest individual docker calls
- Full JSON report for offline analysis
Same fix as the base-env bootstrap tests: init_session's post-failure
non-login probe was overwriting captured["script"] with `true`. Capture
the first (bootstrap) _run_bash call via setdefault.
init_session now fires a follow-up non-login `bash -c true` probe when the
login bootstrap fails, so the two bootstrap-script assertions must capture
the first call only (setdefault), not the probe that overwrote it.
Commit 62a76bd3d (feat: make smart approvals the default, #62661)
changed approvals.mode default from "manual" to "smart". The
TestBlockingApprovalE2E tests did not patch the approval mode, so
check_all_command_guards routed through _smart_approve() first —
calling call_llm() which tried all auxiliary providers, failed (no
API keys in test env), and returned "escalate" before falling through
to the gateway blocking path. The LLM failure cascade took longer
than the test's 2.5s wait window (50 × 0.05s), so the notify
callback had not fired yet when assert len(notified) == 1 ran.
Force {"mode": "manual"} via patch in setup_method/teardown_method,
matching the pattern already used by test_blocking_approval_uses_
canonical_timeout in the same class.
#63621 fixed path quoting, but Ainz's Git for Windows still dies on
`bash -l` itself (`Directory \drivers\etc`). Hermes then fell back to
bash -l *per command*, so every write_file/terminal call failed the same way.
After a failed login snapshot, probe non-login bash -c; if it works, skip
-l for the session. Also skip a stale HERMES_GIT_BASH_PATH that fails a
noprofile probe in favor of %LOCALAPPDATA%\hermes\git portable bash.
Splits the single broad `except Exception` in the compression-lock
acquire path into two handlers: AttributeError/TypeError (version skew —
the lock method is missing, or predates the `ttl_seconds=` kwarg) still
fails OPEN as before, since that's known-safe to proceed without a lock.
Any other exception now fails CLOSED (skips compression this cycle)
instead of treating every failure as "no lock subsystem present" and
letting a second compressor run concurrently and fork the session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reserve API requests before their first await, include every drain-owned work type in runtime status, and pause local cron dispatch while a gateway drain is active.
Closes#63529
Root cause: GatewayRunner._drain_active_agents only waited on
_running_agents + cron in-flight counts. Desktop/API sessions are
tracked solely inside APIServerAdapter (_inflight_agent_runs +
_active_run_agents), so stop/restart logged active_at_start=0 and
systemd SIGKILL'd mid-tool work.
Fix: APIServerAdapter.active_agent_work_count() plus
GatewayRunner._active_api_run_count() folded into the drain wait,
status updates, timeout result, and shutdown logs — same pattern as
_active_cron_job_count for #60432.
Verification: pytest tests/gateway/test_api_server_active_work_drain.py
tests/gateway/test_cron_active_work_drain.py -q → 19 passed
Normalize Anthropic setup-token metadata for every PooledCredential construction path, persist corrected manual entries, heal legacy rows on load without copying global fallback credentials into profiles, and map the contributor email for release attribution.
Reject metadata that would make session queries fail, bound import work, and detach cyclic lineage links. Guard lineage traversal against pre-existing corrupt cycles.
_custom_provider_model_matches() only compared the session model
against the entry's single 'model' field. A custom provider declaring
a multi-model catalog (providers.<name>.models mapping / models list)
whose default model differed from the session model silently failed to
match — dropping the entry's extra_body entirely. Real impact: an
OpenAI custom provider pinning service_tier=flex via extra_body ran
every request at STANDARD tier (~2.3x billing) with zero signal.
- Model matching now accepts the session model when it appears in the
entry's models catalog (dict keys or list), case-insensitive;
single-model 'model' field behavior unchanged; entries with neither
still match everything.
- Usage report ('hermes -z --usage-file') now carries service_tier
(the tier requested via request_overrides.extra_body) so batch
pipelines can audit the billed tier per run.
Validation: 8 new tests; live E2E via real 'hermes -p sweeper -z'
with httpx-level wire capture — service_tier=flex present in the
outgoing /v1/responses body and in the usage report.
#63113 rewrote native drive paths in ShellFileOperations, but init_session
/_wrap_command still embedded C:/... hermes-snap paths from get_temp_dir.
MSYS arg-converts those during bash -l and surfaces Directory \drivers\etc
— including for relative write_file targets, since the wrapper is the fault.
Add _bash_safe_path, override BaseEnvironment._quote_shell_path on
LocalEnvironment (no base→local import), and normalize mixed /c/Users\...
paths in file ops.
Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
The web UI CONFIG_SCHEMA showed ['ask', 'yolo', 'deny'] for the
approvals.mode select field. These don't match any real config values
and 'smart' mode was entirely unreachable from the dashboard.
Correct the options to ['manual', 'smart', 'off'] which match the
values defined and documented in hermes_cli/config.py.
Adds a regression test to TestBuildSchemaFromConfig to pin the correct
option names and guard against future drift.
Fixes#31925