handle_upload_extract() used Path(s.workspace) as the extraction root,
bypassing HERMES_WEBUI_ATTACHMENT_DIR entirely. Route through
_session_attachment_dir(session_id) so archives land alongside
single-file uploads and session cleanup covers them.
Add tests and CHANGELOG entry.
Ref #2247
PR #2294 added the show_previous_messaging_sessions setting and a "Hide
from list" menu action for external sessions, but tripped 8 tests:
- 4 locale-parity tests (tests/test_{japanese,russian,spanish,chinese}_locale.py)
demand every en key be defined in ja/ru/es/zh blocks. The contributor
only added the 5 new keys to en + ko, leaving ja/ru/es/zh/it/de/zh-TW/pt/fr
missing them. tests/test_provider_quota_status.py also requires the two
settings_{label,desc}_previous_messaging_sessions keys in ALL 11 locales.
- tests/test_1466_sidebar_cancel_clarify.py read the first 5200 chars of
_openSessionActionMenu to find cancelSessionStream/delete actions; the
new "Hide from list" branch (17 lines for external sessions) pushed
those past the read window.
- tests/test_issue1611_session_profile_filtering.py grep'd for the
literal string `_keep_latest_messaging_session_per_source(scoped)`,
which no longer exists after the call was rewritten as a multi-line
keyword-arg form.
Fixes:
1. Translations for the 5 new i18n keys added to all 9 missing locales
(it, ja, ru, es, de, zh-CN, zh-TW, pt, fr):
- session_hide_external
- session_hide_external_desc
- session_hidden
- settings_label_previous_messaging_sessions
- settings_desc_previous_messaging_sessions
Where the locale already used the English fallback for related keys
(ru/es/de session_archive), I provided localized translations for the
new keys to match the project's general direction. Native-script
quality, not machine-translation.
2. test_1466 window bumped 5200 → 6400 with a comment explaining the
bump (mirrors the existing 3200→4400→5200 history annotations).
3. test_1611 dedupe-position check loosened to match the function name
without the `(scoped)` suffix so it tolerates both single-line and
keyword-arg call shapes.
Tests: full suite 5828 passed / 63 skipped / 0 failed (was 8 failed).
Behavioral harness verifies the toggle's claimed behavior — off (default)
hides reset/compression segments, on shows all rows in timestamp order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two non-blocking observations from the review, both addressed:
1. The bad-pattern grep listed `error_exit` as a literal token, but the
`error_exit()` function at docker_init.bash:5-10 only echoes the
strings `"!! ERROR: "` and `"!! Exiting script (ID: $$)"` — the
function name itself never appears in container logs. So
`grep -E -i "error_exit"` would only fire on stray debug prints of
the name, not on actual failures. The other patterns
(`Failed to set (UID|GID|...)`, `groupmod: cannot`, etc.) DO catch
real error_exit output, so this wasn't a coverage gap — just a dead
token.
Add `!! ERROR` and `!! Exiting script` to the bad-pattern set so the
grep actually matches the function's output. Keep the literal
`error_exit` token as belt-and-suspenders for any debug/echo of the
name.
2. `test_docker_init_excludes_egg_info_during_staging` was a single
`assert "egg-info" in src` check. That passes if any occurrence
appears — including the explanatory comment block above the staging
logic. A maintainer removing the `--exclude='*.egg-info'` from
rsync but keeping the comment would slip past the test.
Tighten to:
- scope to the staging block (between `_stage_src=` and the
`uv pip install` line) so comments outside that window can't
satisfy the assertion;
- require the literal `--exclude='*.egg-info'` rsync flag;
- require `*.egg-info` in the block so the cp-fallback cleanup is
also pinned;
- additionally require `--exclude='build'`, `--exclude='dist'`,
`--exclude='__pycache__'` so all four setuptools-touchable
artifact dirs stay excluded.
Verified:
- tests/test_docker_docs_and_readonly.py — 11/11 pass.
- YAML parses cleanly via `yaml.safe_load`.
- Full suite: 5770 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Docker smoke gate added in this same PR caught a real production
regression on its very first CI run. v0.51.84 (PR #2470) mounted
hermes-agent-src read-only on the WebUI side and widened the chown
prune to keep the read-only walk happy, but missed that the WebUI's
startup also runs:
uv pip install "$_agent_src[all]"
against the same now-read-only mount. setuptools' egg_info step writes
hermes_agent.egg-info/ inside the source tree even under PEP 517 build
isolation (this is by design -- PEP 517 isolates the BUILD environment,
not the source tree's metadata directory). On a :ro mount this returns
EROFS, the install fails, error_exit fires, and every multi-container
deploy dies at startup. The smoke gate flagged it on both the
two-container and three-container variants.
The fix
-------
Stage the agent source into a writable build dir under /tmp BEFORE
invoking pip install, then point pip at the staged copy.
_stage_src="/tmp/hermes-agent-build"
rm -rf "$_stage_src" && mkdir -p "$_stage_src"
rsync -a --exclude='*.egg-info' --exclude='build' --exclude='dist' \
--exclude='__pycache__' --exclude='.git' \
"$_agent_src"/ "$_stage_src"/
uv pip install "$_stage_src[all]" ...
rm -rf "$_stage_src"
The exclusion list matters: when setuptools sees a pre-baked *.egg-info,
build, or dist directory, it takes a timestamp-update code path that
also reads/writes inside that directory -- which itself fails on a :ro
source. Excluding them keeps the build on the fresh-build path
unconditionally.
rsync is in the production image (Dockerfile line 41-44). For users
running custom WebUI images without rsync, the script falls back to
cp -a + post-copy rm -rf of the same artifacts.
Tests
-----
Two new source-level invariants in tests/test_docker_docs_and_readonly.py:
test_docker_init_stages_agent_source_for_writable_install
-- asserts _stage_src=... is declared
-- asserts every `uv pip install ...[all]` line uses _stage_src,
NOT raw $_agent_src
test_docker_init_excludes_egg_info_during_staging
-- asserts the staging path excludes *.egg-info (rsync exclude
form or cp-fallback's explicit rm -rf both pass)
These would have caught the v0.51.84 regression at the source level
(once written; they're new). The Docker runtime smoke gate is the
durable defence for the broader class of :ro x init-script
interactions, since source-level invariants only catch what they're
written to catch.
Verification
------------
- pytest tests/test_docker_docs_and_readonly.py: 11 passed (9 existing
+ 2 new)
- pytest tests/ -q --timeout=60: 5891 passed, 6 skipped (was 5889;
delta is exactly the 2 new tests)
- bash -n docker_init.bash: clean
Once this lands, the Docker smoke gate's two/three-container variants
should go green, completing the self-validating loop.