Closes #1695. @Patrick-81 reported the bare "AIAgent not available -- check that hermes-agent is on sys.path" error on a symlinked install (~/Programmes/hermes-agent linked to ~/hermes-agent). The maintainer's response — three diagnostic commands plus `pip install -e .` in the agent dir — fixed it for them. This PR captures both halves of that learning so the next user with the same shape doesn't have to file a new issue: 1. **Error message diagnostic block.** New helper `_aiagent_import_error_detail()` in api/streaming.py builds a multi-line diagnostic when the import fails, including: - the running Python interpreter - HERMES_WEBUI_AGENT_DIR (set value, or "(not set)") - sys.path entries that mention hermes/agent (or "no entries mention..." — itself a strong diagnostic signal) - the most-common fix (`pip install -e .` in the agent dir) - a pointer to docs/troubleshooting.md The original error message string is preserved as the FIRST line so existing log scrapers and docs-search keep matching. Helper is kept as a separate function so it stays out of the hot path until we actually need to raise — building it on every successful import would be wasted work. 2. **New docs/troubleshooting.md.** Symptom → Why → Diagnostic commands → Fix → When-to-file-a-bug template, with one entry to start: the "AIAgent not available" flow Patrick-81 walked through. Future recurring failure modes follow the same template. Required a one-line addition to .gitignore — docs/* is gitignored with an allowlist, and the new file needed `!docs/troubleshooting.md` to be tracked. 3. **README link.** docs/troubleshooting.md added to the `## Docs` section so users know where to look first. 13 regression tests in tests/test_1695_aiagent_import_error_detail.py: 9 for the helper output shape (preserves original message line, includes running python, shows HERMES_WEBUI_AGENT_DIR set/unset both ways, includes pip-install-e hint, points at troubleshooting doc, lists relevant sys.path entries when present, says "no entries..." when absent, output is multi-line) plus 4 for the docs-presence regression (file exists, has the AIAgent section, includes pip install -e ., describes the diagnostic chain with readlink + agent/__init__.py verification). 190 streaming/aiagent tests pass after the change. ast.parse on api/streaming.py clean. CI failure on prior push was due to the docs/* gitignore swallowing the new troubleshooting.md file silently — this commit adds the allowlist entry so the file is tracked.
185 lines
7.7 KiB
Python
185 lines
7.7 KiB
Python
"""Tests for #1695 — diagnostic detail in the "AIAgent not available" ImportError.
|
|
|
|
Patrick-81 reported a symlinked hermes-agent install that produced a bare
|
|
"AIAgent not available -- check that hermes-agent is on sys.path" error with
|
|
no information about which Python was running, where it was looking, or what
|
|
to do next. The maintainer's response (which Patrick confirmed worked)
|
|
amounted to: run three diagnostic commands, then `pip install -e .` in the
|
|
agent dir.
|
|
|
|
This test suite locks the diagnostic shape of the new error message:
|
|
|
|
- The original message string is preserved (so existing log scrapers /
|
|
monitoring / docs-search keep working).
|
|
- The running python interpreter path is included.
|
|
- HERMES_WEBUI_AGENT_DIR is shown if set, "(not set)" otherwise.
|
|
- The relevant sys.path entries are shown.
|
|
- A pip install -e . hint is included.
|
|
- A pointer to docs/troubleshooting.md is included.
|
|
|
|
Behavioural test for the actual raise path lives in the streaming integration
|
|
suite; this file only exercises the helper.
|
|
"""
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _import_helper():
|
|
"""Import _aiagent_import_error_detail without triggering the full streaming
|
|
module side-effects.
|
|
|
|
api/streaming.py imports a lot at top-level (gateway routing, model resolver,
|
|
session DB, ...). For a focused unit test we just need the helper. Importing
|
|
the module is fine — it stays cached for the rest of the suite.
|
|
"""
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
from api import streaming # noqa: F401
|
|
return streaming._aiagent_import_error_detail
|
|
|
|
|
|
class TestAIAgentImportErrorDetail:
|
|
"""Unit tests for the diagnostic helper."""
|
|
|
|
def test_preserves_original_message_for_log_scrapers(self):
|
|
"""The original error string must remain the FIRST line so existing
|
|
scrapers, alerting, and docs-search keep matching.
|
|
"""
|
|
helper = _import_helper()
|
|
out = helper()
|
|
first = out.splitlines()[0]
|
|
assert first == "AIAgent not available -- check that hermes-agent is on sys.path", (
|
|
f"first line must be the original error message verbatim, got: {first!r}"
|
|
)
|
|
|
|
def test_includes_running_python_interpreter(self):
|
|
"""The diagnostic must include the running python so the user knows
|
|
which interpreter is missing the agent (most common cause of the bug).
|
|
"""
|
|
helper = _import_helper()
|
|
out = helper()
|
|
assert "python:" in out
|
|
assert sys.executable in out, (
|
|
f"running python ({sys.executable}) must appear in the diagnostic"
|
|
)
|
|
|
|
def test_shows_agent_dir_env_when_set(self, monkeypatch):
|
|
"""If HERMES_WEBUI_AGENT_DIR is set, the diagnostic must show its value
|
|
so the user can confirm whether the override is pointing at the right
|
|
directory.
|
|
"""
|
|
helper = _import_helper()
|
|
monkeypatch.setenv("HERMES_WEBUI_AGENT_DIR", "/custom/agent/path")
|
|
out = helper()
|
|
assert "HERMES_WEBUI_AGENT_DIR: /custom/agent/path" in out
|
|
|
|
def test_shows_agent_dir_env_unset_marker(self, monkeypatch):
|
|
"""If HERMES_WEBUI_AGENT_DIR is NOT set, the diagnostic must say so
|
|
explicitly — silence is ambiguous (could be empty string, could be unset).
|
|
"""
|
|
helper = _import_helper()
|
|
monkeypatch.delenv("HERMES_WEBUI_AGENT_DIR", raising=False)
|
|
out = helper()
|
|
assert "HERMES_WEBUI_AGENT_DIR: (not set)" in out
|
|
|
|
def test_includes_pip_install_editable_hint(self):
|
|
"""The most common fix (per #1695) is `pip install -e .` in the agent dir.
|
|
The diagnostic must surface this as the first-line remediation.
|
|
"""
|
|
helper = _import_helper()
|
|
out = helper()
|
|
assert "pip install -e ." in out, (
|
|
"diagnostic must surface `pip install -e .` as the most common fix"
|
|
)
|
|
|
|
def test_points_at_troubleshooting_doc(self):
|
|
"""The diagnostic must point at the docs/troubleshooting.md entry so
|
|
users with edge-case failures know where to look next.
|
|
"""
|
|
helper = _import_helper()
|
|
out = helper()
|
|
assert "troubleshooting" in out.lower(), (
|
|
"diagnostic must point at docs/troubleshooting.md for further help"
|
|
)
|
|
|
|
def test_lists_sys_path_entries_when_relevant(self, monkeypatch):
|
|
"""If sys.path contains entries mentioning hermes/agent, the diagnostic
|
|
must list them (helps the user confirm the agent dir is or isn't
|
|
actually present on the import path).
|
|
"""
|
|
helper = _import_helper()
|
|
# Force at least one relevant entry into sys.path for the test.
|
|
monkeypatch.syspath_prepend("/fake/hermes-agent")
|
|
out = helper()
|
|
assert "/fake/hermes-agent" in out
|
|
|
|
def test_handles_no_relevant_sys_path_entries(self, monkeypatch):
|
|
"""If sys.path has NO hermes/agent-related entries, the diagnostic must
|
|
say so explicitly — this is itself a strong diagnostic signal.
|
|
"""
|
|
helper = _import_helper()
|
|
# Replace sys.path with entries that mention neither hermes nor agent.
|
|
# Use monkeypatch.setattr so the change reverts cleanly.
|
|
clean_path = ["/usr/lib/python3.11", "/usr/local/lib/python3.11", "/tmp"]
|
|
monkeypatch.setattr(sys, "path", clean_path)
|
|
out = helper()
|
|
assert "no entries mention hermes or agent" in out, (
|
|
"diagnostic must explicitly call out empty-path case (it's a strong signal)"
|
|
)
|
|
|
|
def test_output_is_multiline_string(self):
|
|
"""The diagnostic must be a multi-line string (newline-joined), not a
|
|
single long line — log-readability matters when this surfaces in a
|
|
traceback.
|
|
"""
|
|
helper = _import_helper()
|
|
out = helper()
|
|
assert "\n" in out, "diagnostic must be multi-line for log readability"
|
|
assert len(out.splitlines()) >= 5, (
|
|
f"diagnostic must have at least 5 lines, got {len(out.splitlines())}"
|
|
)
|
|
|
|
|
|
class TestAIAgentImportErrorDocsPresence:
|
|
"""Regression: the docs/troubleshooting.md file must exist with the
|
|
"AIAgent not available" entry the diagnostic links to.
|
|
"""
|
|
|
|
def test_troubleshooting_md_exists(self):
|
|
path = REPO_ROOT / "docs" / "troubleshooting.md"
|
|
assert path.exists(), "docs/troubleshooting.md must exist (referenced by streaming.py)"
|
|
|
|
def test_troubleshooting_md_has_aiagent_section(self):
|
|
path = REPO_ROOT / "docs" / "troubleshooting.md"
|
|
content = path.read_text(encoding="utf-8")
|
|
assert "AIAgent not available" in content, (
|
|
"docs/troubleshooting.md must have an entry titled \"AIAgent not available\""
|
|
)
|
|
|
|
def test_troubleshooting_md_includes_pip_install_editable(self):
|
|
"""The doc must surface the `pip install -e .` fix."""
|
|
path = REPO_ROOT / "docs" / "troubleshooting.md"
|
|
content = path.read_text(encoding="utf-8")
|
|
assert "pip install -e ." in content, (
|
|
"docs/troubleshooting.md must include the pip install -e . fix"
|
|
)
|
|
|
|
def test_troubleshooting_md_describes_diagnostic_steps(self):
|
|
"""The doc must walk through diagnostic commands (readlink, ls, etc.)
|
|
before jumping to the fix — that ordering is what worked for #1695.
|
|
"""
|
|
path = REPO_ROOT / "docs" / "troubleshooting.md"
|
|
content = path.read_text(encoding="utf-8")
|
|
# Look for the symlink-resolution diagnostic chain.
|
|
assert "readlink" in content, (
|
|
"diagnostic flow must include `readlink` for the symlink-typo failure mode"
|
|
)
|
|
assert "/agent/__init__.py" in content, (
|
|
"diagnostic flow must verify the agent module file is reachable"
|
|
)
|