test: make tests/ mirror the source tree; drop issue numbers from filenames

`scripts/run_tests.sh tests/<dir>/` is how a change gets its regression
coverage run, so a test filed under the wrong directory is a test nobody
runs when that code changes. Two kinds of drift had accumulated.

Parallel directories for one source package, folded into the mirror:
  tests/acp        -> tests/acp_adapter   (its __init__/conftest move with it)
  tests/cli        -> tests/hermes_cli    (prompt_toolkit fixture merged into
                                           hermes_cli/conftest.py)
  tests/run_agent  -> tests/agent         (backoff fixture becomes
                                           agent/conftest.py)
  tests/relay      -> tests/gateway/relay
  tests/state      -> tests/hermes_state

246 loose files at tests/ root, routed by the package they import/patch:
hermes_cli, hermes_state, agent, gateway, tools, plugins, tui_gateway, cron.
Installer and desktop-update script tests go to tests/scripts/{install,
desktop_update}/. 43 tests of root-level modules (batch_runner, utils,
hermes_constants, packaging) stay at the root.

Filenames drop their issue numbers (95 files: test_89315_x.py -> test_x.py);
the number stays in the module docstring where it has context.

Collisions: test_cli_skin_integration.py existed in both tests/ and tests/cli
with different subsets — merged into one (10 tests, all kept);
run_agent/test_pre_compress_memory_context.py -> agent/..._handoff.py;
tests/test_account_usage.py -> agent/test_account_usage_fetch.py;
tests/test_web_server.py -> hermes_cli/test_web_server_ws_ping.py.
Deleted: test_minisweagent_path.py (empty since PR #2804),
test_model_picker_scroll.py (tested a private copy of the logic, imported
nothing), test_process_loop_event_loop_warning.py (asserted asyncio behaviour,
imported nothing from Hermes).

Repo-root path arithmetic (Path(__file__).parents[N], dirname chains) is
bumped for the 202 files that changed depth and verified by evaluating every
such expression against the new location. classify_changes' desktop-updater
lane prefix, tests-os.yml's ignore glob and every in-tree path comment follow
the moves. tests/test_tests_tree_layout.py keeps the tree from drifting back.
This commit is contained in:
teknium1
2026-09-13 06:27:46 -07:00
committed by Teknium
parent 422bc9bde9
commit d10bb2ab6f
697 changed files with 312 additions and 537 deletions
-256
View File
@@ -1,256 +0,0 @@
"""Tests for Honcho client configuration."""
import json
import os
import stat
from pathlib import Path
import pytest
from plugins.memory.honcho.client import HonchoClientConfig
from plugins.memory.honcho import HonchoMemoryProvider
class TestHonchoClientConfigAutoEnable:
"""Test auto-enable behavior when API key is present."""
def test_auto_enables_when_api_key_present_no_explicit_enabled(self, tmp_path):
"""When API key exists and enabled is not set, should auto-enable."""
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({
"apiKey": "test-api-key-12345",
# Note: no "enabled" field
}))
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.api_key == "test-api-key-12345"
assert cfg.enabled is True # Auto-enabled because API key exists
def test_respects_explicit_enabled_false(self, tmp_path):
"""When enabled is explicitly False, should stay disabled even with API key."""
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({
"apiKey": "test-api-key-12345",
"enabled": False, # Explicitly disabled
}))
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.api_key == "test-api-key-12345"
assert cfg.enabled is False # Respects explicit setting
def test_disabled_when_no_api_key_and_no_explicit_enabled(self, tmp_path):
"""When no API key and enabled not set, should be disabled."""
config_path = tmp_path / "config.json"
config_path.write_text(json.dumps({
"workspace": "test",
# No apiKey, no enabled
}))
# Clear env var if set
env_key = os.environ.pop("HONCHO_API_KEY", None)
try:
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.api_key is None
assert cfg.enabled is False # No API key = not enabled
finally:
if env_key:
os.environ["HONCHO_API_KEY"] = env_key
def test_from_env_always_enabled(self, monkeypatch):
"""from_env() should always set enabled=True."""
monkeypatch.setenv("HONCHO_API_KEY", "env-test-key")
cfg = HonchoClientConfig.from_env()
assert cfg.api_key == "env-test-key"
assert cfg.enabled is True
@pytest.mark.skipif(os.name == "nt", reason="POSIX mode bits not enforced on Windows")
def test_save_config_sets_owner_only_permissions(tmp_path, monkeypatch):
"""honcho.json is created atomically with 0o600, not chmod-after-write."""
import utils
calls = []
real_atomic = utils.atomic_json_write
def spy(path, data, **kwargs):
calls.append(kwargs.get("mode"))
return real_atomic(path, data, **kwargs)
monkeypatch.setattr(utils, "atomic_json_write", spy)
provider = HonchoMemoryProvider()
provider.save_config({"api_key": "hc-test-key"}, str(tmp_path))
assert calls == [0o600]
config_file = tmp_path / "honcho.json"
assert config_file.exists()
mode = stat.S_IMODE(config_file.stat().st_mode)
assert mode == 0o600, f"Expected 0o600 (owner-only), got {oct(mode)}"
class TestLatencyFlagResolution:
def test_host_block_wins(self, tmp_path, monkeypatch):
monkeypatch.delenv('HONCHO_BASE_URL', raising=False)
config_path = tmp_path / 'config.json'
config_path.write_text(json.dumps({
'apiKey': 'k',
'queryRewrite': False,
'firstTurnBaseWait': 3,
'hosts': {'hermes': {
'queryRewrite': True,
'firstTurnBaseWait': 0,
'firstTurnDialecticWait': 0.5,
}},
}))
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.query_rewrite is True
assert cfg.first_turn_base_wait == 0.0
assert cfg.first_turn_dialectic_wait == 0.5
def test_per_host_timeout_wins_over_global(self, tmp_path, monkeypatch):
monkeypatch.delenv('HONCHO_TIMEOUT', raising=False)
config_path = tmp_path / 'config.json'
config_path.write_text(json.dumps({
'apiKey': 'k',
'timeout': 30,
'hosts': {'hermes': {'timeout': 5}},
}))
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.timeout == 5.0
class TestHonchoBaseUrlSanitize:
def test_clean_base_url_accepted(self, tmp_path, monkeypatch):
monkeypatch.delenv('HONCHO_BASE_URL', raising=False)
config_path = tmp_path / 'config.json'
config_path.write_text(json.dumps({
'apiKey': 'k',
'baseUrl': 'https://honcho.example.com',
}))
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.base_url == 'https://honcho.example.com'
def test_nonprintable_base_url_dropped(self, tmp_path, monkeypatch):
monkeypatch.delenv('HONCHO_BASE_URL', raising=False)
config_path = tmp_path / 'config.json'
bad = 'https://honcho.example.com\x1b'
config_path.write_text(json.dumps({
'apiKey': 'k',
'baseUrl': bad,
}))
cfg = HonchoClientConfig.from_global_config(config_path=config_path)
assert cfg.base_url is None
def test_env_nonprintable_dropped(self, monkeypatch):
monkeypatch.setenv('HONCHO_BASE_URL', 'https://x.example\x1b')
monkeypatch.delenv('HONCHO_API_KEY', raising=False)
cfg = HonchoClientConfig.from_env()
assert cfg.base_url is None
class TestProfileKeyIsolationWarning:
"""#36098 / #66125: a named-profile host block without apiKey does NOT
inherit the default host's key (isolation by design), but the failure
must be loud, not silent."""
def test_keyless_profile_block_warns_when_default_has_key(self, tmp_path, monkeypatch, caplog):
import logging
monkeypatch.delenv('HONCHO_API_KEY', raising=False)
config_path = tmp_path / 'config.json'
config_path.write_text(json.dumps({
'hosts': {
'hermes': {'apiKey': 'shared-key'},
'hermes_coder': {'baseUrl': 'http://192.168.1.50:8000'},
},
}))
with caplog.at_level(logging.WARNING, logger='plugins.memory.honcho.client'):
cfg = HonchoClientConfig.from_global_config(
host='hermes_coder', config_path=config_path,
)
assert cfg.api_key is None # isolation preserved — no silent inheritance
assert any('NOT inherited' in r.message for r in caplog.records)
def test_no_warning_when_profile_block_has_key(self, tmp_path, monkeypatch, caplog):
import logging
monkeypatch.delenv('HONCHO_API_KEY', raising=False)
config_path = tmp_path / 'config.json'
config_path.write_text(json.dumps({
'hosts': {
'hermes': {'apiKey': 'shared-key'},
'hermes_coder': {'apiKey': 'coder-key'},
},
}))
with caplog.at_level(logging.WARNING, logger='plugins.memory.honcho.client'):
cfg = HonchoClientConfig.from_global_config(
host='hermes_coder', config_path=config_path,
)
assert cfg.api_key == 'coder-key'
assert not any('NOT inherited' in r.message for r in caplog.records)
def test_no_warning_for_default_host(self, tmp_path, monkeypatch, caplog):
import logging
monkeypatch.delenv('HONCHO_API_KEY', raising=False)
config_path = tmp_path / 'config.json'
config_path.write_text(json.dumps({
'hosts': {'hermes': {'baseUrl': 'http://localhost:8000'}},
}))
with caplog.at_level(logging.WARNING, logger='plugins.memory.honcho.client'):
HonchoClientConfig.from_global_config(
host='hermes', config_path=config_path,
)
assert not any('NOT inherited' in r.message for r in caplog.records)
def test_save_config_refuses_to_replace_a_file_that_does_not_parse(tmp_path):
"""A corrupt honcho.json must not be rewritten from the new values alone."""
config_path = tmp_path / "honcho.json"
config_path.write_text("{ not json")
with pytest.raises(ValueError):
HonchoMemoryProvider().save_config({"api_key": "hc-test-key"}, str(tmp_path))
assert config_path.read_text() == "{ not json"
def test_save_config_merges_into_a_parseable_file(tmp_path):
config_path = tmp_path / "honcho.json"
config_path.write_text(json.dumps({"hosts": {"other": {"apiKey": "keep-me"}}}))
HonchoMemoryProvider().save_config({"api_key": "hc-test-key"}, str(tmp_path))
data = json.loads(config_path.read_text())
assert data["hosts"]["other"]["apiKey"] == "keep-me" and data["api_key"] == "hc-test-key"
def test_save_config_holds_the_refresh_locks_so_a_rotation_survives(tmp_path, monkeypatch):
"""A refresh thread that wants the locks while save_config reads must land after its write, not under it."""
import threading
from plugins.memory.honcho import oauth
config_path = tmp_path / "honcho.json"
config_path.write_text(json.dumps({"hosts": {"hermes": {"apiKey": "hch-at-old", "oauth": {"refreshToken": "hch-rt-old"}}}}))
rotated = oauth.OAuthCredential("hch-at-new", "hch-rt-new", 10_000, "hermes-desktop", "http://localhost:8000/oauth/token")
save_read, rotation_done = threading.Event(), threading.Event()
real_read = oauth._read_config_strict
def read_then_wait(path):
raw = real_read(path)
if not save_read.is_set():
save_read.set()
rotation_done.wait(0.5) # the refresh gets this window; only a held lock keeps it out
return raw
def rotate():
save_read.wait(2)
with oauth._refresh_lock, oauth._config_refresh_lock(config_path):
oauth._persist_credential(config_path, "hermes", rotated)
rotation_done.set()
monkeypatch.setattr(oauth, "_read_config_strict", read_then_wait)
thread = threading.Thread(target=rotate)
thread.start()
HonchoMemoryProvider().save_config({"logging": True}, str(tmp_path))
thread.join(2)
assert rotation_done.is_set()
data = json.loads(config_path.read_text())
assert data["hosts"]["hermes"]["apiKey"] == "hch-at-new" and data["logging"] is True