adf23550f5
Under `gateway.multiplex_profiles` one gateway process serves every profile
under ~/.hermes/profiles/NAME/; each routed turn runs with a context-local
HERMES_HOME override while `os.environ` still holds the DEFAULT profile's
values. Anything evaluated once at import, or memoised in a single unkeyed
module slot, therefore freezes the LAUNCH profile's value and leaks it into
every other profile's turns. This lands the tools-side half of that class:
- tools/process_registry.py, tools/environments/{modal,singularity}.py:
`_checkpoint_path()` / `_snapshot_store()` resolve `get_hermes_home()` at
call time (same seam as `tools/skills_tool._skills_dir`, so the existing
`monkeypatch.setattr(CHECKPOINT_PATH)` test sites keep working). Completes
the checkpoint_manager / sticker_cache half cherry-picked from #56315.
- plugins/platforms/feishu/feishu_comment_rules.py: `_MtimeCache` is now
path-keyed (accepts a Path or a zero-arg resolver, one (mtime, data) slot
per resolved path) with `invalidate()`; `_rules_file()` / `_pairing_file()`
resolve the routed profile's files. Proposed in #63962.
- tools/tool_output_limits.py, tools/browser_tool.py, tools/browser_camofox.py:
the process-lifetime config caches are dicts keyed by `hermes_home_key()`;
the `_X_resolved` flags and the lifecycle reset keep their shape.
tools/file_tools.py drops its private `file_read_max_chars` memo and reads
the already mtime+path-cached `load_config_readonly()`.
- hermes_time.py: `get_timezone_name()`; when `is_multiplex_active()` the
env `HERMES_TIMEZONE` (bridged from the default profile's config at gateway
startup) is ignored in favour of the routed profile's config.yaml. Both
sandbox TZ sites (code_execution_env/_tool) now use it.
- tools/cronjob_tools.py, tools/tts_tool.py, tools/skill_manager_tool.py:
the static schema text is profile-neutral and `dynamic_schema_overrides=`
rebuilds the `display_hermes_home()` / create-dir hint per
`get_definitions()`, so a routed profile's model sees its own paths.
Refs #95685.
Co-authored-by: Nathan Shan <nathanielcrush51@gmail.com>
(cherry picked from commit 6d3fc6b07b3155c6196b1fd61a829283f1d7855c)
69 lines
2.6 KiB
Python
69 lines
2.6 KiB
Python
"""Tests for terminal truncation spill + metadata (deferred retrieval)."""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tools.terminal_tool import terminal_tool
|
|
|
|
|
|
@pytest.fixture
|
|
def small_cap(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
|
from hermes_constants import hermes_home_key
|
|
import tools.tool_output_limits as lim
|
|
monkeypatch.setattr(lim, "_cached_limits", {hermes_home_key(): {
|
|
"max_bytes": 2000, "max_lines": 2000, "max_line_length": 2000,
|
|
}})
|
|
return tmp_path
|
|
|
|
|
|
class TestTruncationSpill:
|
|
def test_truncated_output_has_metadata_and_spill(self, small_cap):
|
|
r = json.loads(terminal_tool(
|
|
"python3 -c \"print('marker_head'); [print(f'row_{i}', 'x'*80) for i in range(200)]; print('marker_tail')\"",
|
|
task_id="t-spill-1"))
|
|
assert r["exit_code"] == 0
|
|
assert "OUTPUT TRUNCATED" in r["output"]
|
|
assert r["output_total_chars"] > 2000
|
|
p = Path(r["full_output_path"])
|
|
assert p.exists()
|
|
full = p.read_text()
|
|
assert "marker_head" in full and "marker_tail" in full
|
|
# The spill contains rows that were cut from the visible window.
|
|
assert "row_100 " in full
|
|
assert "read_file" in r["truncation_note"]
|
|
|
|
def test_small_output_has_no_metadata(self, small_cap):
|
|
r = json.loads(terminal_tool("echo tiny", task_id="t-spill-2"))
|
|
assert r["exit_code"] == 0
|
|
assert "full_output_path" not in r
|
|
assert "output_total_chars" not in r
|
|
|
|
def test_spill_is_redacted(self, small_cap):
|
|
r = json.loads(terminal_tool(
|
|
"python3 -c \"print('sk-proj-' + 'a1B2c3D4e5F6g7H8i9J0' * 3); [print('pad', 'y'*90) for i in range(200)]\"",
|
|
task_id="t-spill-3"))
|
|
p = Path(r["full_output_path"])
|
|
full = p.read_text()
|
|
assert "a1B2c3D4e5F6g7H8i9J0a1B2c3D4e5F6g7H8i9J0" not in full
|
|
|
|
def test_old_spills_cleaned(self, small_cap, tmp_path):
|
|
spill_dir = tmp_path / ".hermes" / "cache" / "terminal-output"
|
|
spill_dir.mkdir(parents=True, exist_ok=True)
|
|
stale = spill_dir / "out-1-2-dead.log"
|
|
stale.write_text("old")
|
|
os.utime(stale, (1, 1))
|
|
json.loads(terminal_tool(
|
|
"python3 -c \"[print('z'*90) for i in range(200)]\"", task_id="t-spill-4"))
|
|
assert not stale.exists()
|
|
|
|
def test_failed_command_still_gets_spill(self, small_cap):
|
|
r = json.loads(terminal_tool(
|
|
"python3 -c \"[print('e'*90) for i in range(200)]; import sys; sys.exit(3)\"",
|
|
task_id="t-spill-5"))
|
|
assert r["exit_code"] == 3
|
|
assert Path(r["full_output_path"]).exists()
|