CI failed on stage-323 because:
1. mcp_server.py imports the 'mcp' package (optional runtime dep) — only
users who actually run the MCP integration install it. CI runs with
stdlib-only deps (pyyaml + pytest + pytest-timeout).
2. tests/test_mcp_server.py uses pytest.mark.asyncio which requires
pytest-asyncio — not installed in CI.
Fix:
- Add pytest-asyncio to CI install line.
- Try-install mcp; if it fails (Python 3.13 wheel issues, etc.) the test
module uses pytest.importorskip and skips cleanly without breaking the
matrix.
- tests/test_mcp_server.py: add module-level importorskip for both 'mcp'
and 'pytest_asyncio' as a safety net.
Local: 4947/4947 still pass after change.
Root cause: tests/test_mcp_server.py and tests/test_issue1857_usage_overwrite.py
both leaked module state into the full pytest suite, causing 20+ failures in
unrelated test files when they ran together.
Two distinct bugs:
1. test_issue1857_usage_overwrite.py used mock.patch.dict(sys.modules, {...}).
patch.dict tracks original keys at __enter__ and DELETES any keys added
during the patch on __exit__. That silently evicted lazily-imported
pydantic submodules (e.g. pydantic.root_model), producing
KeyError: 'pydantic.root_model' in test_mcp_server.py downstream.
Fix: manual save/restore of only the three keys we explicitly inject.
2. test_mcp_server.py mutated module-level constants on api.config / api.models /
mcp_server (STATE_DIR, SESSION_DIR, PROJECTS_FILE, …) without restoring,
leaving downstream tests reading deleted tmpdirs. Fix: snapshot original
values on first _reimport_mcp() call and restore in _cleanup_state_dir.
Additionally, test_profiles_match_single_source_of_truth re-imported
api.routes / api.profiles into sys.modules and only restored sys.modules,
not the parent api package's attributes. `import api.routes as r` resolves
via sys.modules['api'].routes (parent attribute), NOT directly via
sys.modules['api.routes']. So fresh modules leaked through despite the
sys.modules restore. Fix: also restore parent-package attributes.
Result: full pytest suite goes from 20 failures + 36 errors back to all green
(4947 passed, 8 skipped). Up from 4898 in v0.51.27, gain of 49 from
PR #1895 (MCP server tests) + #1866 (goal handler tests).
Maintainer review on #1895 asked for two test additions:
TestApiWireFormat — stands up a tiny http.server stub on a free port,
points WEBUI_URL at it, and captures (path, body, headers) of every
request the MCP issues:
- test_rename_session_posts_to_canonical_path: locks /api/session/rename
URL + body shape so a typo in the path or field names cannot slip
through validation-only tests.
- test_move_session_posts_to_canonical_path: same for /api/session/move
including profile pre-flight against a real local project.
- test_move_session_unassign_sends_null_project_id: explicit JSON null
in the body, not an omitted key.
- test_url_built_from_env_vars: HERMES_WEBUI_HOST/HERMES_WEBUI_PORT
flow through to WEBUI_URL — would have caught the original 8788 bug.
- test_url_default_when_env_unset: default 127.0.0.1:8787 matches the
upstream contract from api/config.py:33.
TestProfileCliOrdering — locks the --profile CLI ordering invariant
(mcp_server.py:62-64): the override of _active_profile must bind before
any consumer reads it. Today this is safe because get_active_profile_name
reads the module global lazily, but a regression that latched the value
at import time would silently make --profile foo a no-op.
50/50 mcp tests pass.
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Maintainer review on PR #1895 flagged that mcp_server.py duplicated the
visibility model from api/routes.py:75. Move the canonical helper into
api/profiles.py (next to _is_root_profile, on which it depends) so both
api/routes.py and mcp_server.py import the same function instead of
carrying parallel definitions that could drift as the model evolves.
- api/profiles.py: + _profiles_match (verbatim from former routes.py:75-97)
- api/routes.py: replace local definition with re-export to keep all
existing _profiles_match(...) call sites resolving
without per-call-site refactors
- mcp_server.py: drop local copy, import _profiles_match alongside the
existing api.profiles imports (line 59)
- tests: + test_profiles_match_single_source_of_truth asserts
identity (mcp.module._profiles_match is api.profiles._profiles_match
is api.routes._profiles_match) so any re-introduction of
a local copy trips the test
+ test_profiles_match_input_matrix parametrize across
the (None|''|'default'|'foo') x (None|''|'default'|'foo'|'bar')
visibility matrix per maintainer suggestion
Behaviour unchanged. Zero call-site changes anywhere in api/routes.py.
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Blocker fixes from maintainer review of #1895.
WEBUI_URL: replace hardcoded 'http://127.0.0.1:8788' with HERMES_WEBUI_HOST/
HERMES_WEBUI_PORT env vars defaulting to 127.0.0.1:8787, mirroring the
contract in api/config.py:32-33. The 8788 default would have failed every
fresh upstream install — 8787 is canonical, 8788 is a local-deployment
quirk on hosts where 8787 is taken by another service.
delete_project no-auth path: remove the filesystem fallback that wrote
session_data['project_id']=None directly via os.replace(). That bypassed
_write_session_index() and left _index.json holding the stale project_id,
causing a running WebUI to keep grouping sessions under the deleted
project until something else triggered a re-compact. Even calling
Session.save() in-process would not have helped because the WebUI's
SESSIONS dict cache lives in a separate process and would overwrite our
update on its next save. The HTTP API is the only cache-safe path —
without auth we now refuse the unassign and surface a 'warning' field.
Tests: + test_delete_no_auth_refuses_unassign locks the new behaviour
(project deleted, sessions and index untouched, warning surfaced).
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Per maintainer review, replace duplicated I/O with canonical helpers
for locking, profile scoping, index consistency, and validation.
Profile scoping (#1614) enforced on all CRUD via _profiles_match
matching api/routes.py:75 semantics exactly. AI-authored, human-reviewed.
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>