fix(tests): auto-derive unique port+state-dir per worktree (fixes parallel pytest)

This commit is contained in:
Hermes Agent
2026-04-14 19:04:48 +00:00
parent 924c833878
commit c3251ea97d
43 changed files with 130 additions and 60 deletions

42
tests/_pytest_port.py Normal file
View File

@@ -0,0 +1,42 @@
"""
Shared test server constants for use in individual test files.
Instead of hardcoding ``BASE = "http://127.0.0.1:8788"`` in every test file,
import from here so the port and state dir are always consistent with
what conftest.py computed for this worktree.
Usage::
from tests._pytest_port import BASE
conftest.py publishes ``HERMES_WEBUI_TEST_PORT`` and
``HERMES_WEBUI_TEST_STATE_DIR`` to ``os.environ`` at module level
(before any test file is imported), so this module always reads the
correct values. The auto-derivation fallback matches conftest's logic
exactly, so standalone imports also work correctly.
"""
import hashlib
import os
import pathlib
def _auto_test_port(repo_root: pathlib.Path) -> int:
h = int(hashlib.md5(str(repo_root).encode()).hexdigest(), 16)
return 20000 + (h % 10000)
def _auto_state_dir_name(repo_root: pathlib.Path) -> str:
h = hashlib.md5(str(repo_root).encode()).hexdigest()[:8]
return f"webui-test-{h}"
_TESTS_DIR = pathlib.Path(__file__).parent.resolve()
_REPO_ROOT = _TESTS_DIR.parent.resolve()
_HERMES_HOME = pathlib.Path(os.getenv('HERMES_HOME',
str(pathlib.Path.home() / '.hermes')))
TEST_PORT = int(os.environ.get('HERMES_WEBUI_TEST_PORT',
str(_auto_test_port(_REPO_ROOT))))
BASE = f"http://127.0.0.1:{TEST_PORT}"
TEST_STATE_DIR = pathlib.Path(os.environ.get(
'HERMES_WEBUI_TEST_STATE_DIR',
str(_HERMES_HOME / _auto_state_dir_name(_REPO_ROOT))
))

View File

@@ -31,14 +31,37 @@ HOME = pathlib.Path.home()
HERMES_HOME = pathlib.Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))) HERMES_HOME = pathlib.Path(os.getenv('HERMES_HOME', str(HOME / '.hermes')))
# ── Test server config ──────────────────────────────────────────────────── # ── Test server config ────────────────────────────────────────────────────
TEST_PORT = int(os.getenv('HERMES_WEBUI_TEST_PORT', '8788')) # Port and state dir auto-derive from the repo path when no env var is set,
# giving every worktree its own isolated port (8800-8899) and state directory.
# Override with HERMES_WEBUI_TEST_PORT / HERMES_WEBUI_TEST_STATE_DIR to pin.
def _auto_test_port(repo_root) -> int:
"""Map repo path to a unique port in 20000-29999 (10k range = near-zero collisions).
Far from system port ranges and Linux ephemeral ports (32768+).
Override with HERMES_WEBUI_TEST_PORT to use a specific port."""
import hashlib
h = int(hashlib.md5(str(repo_root).encode()).hexdigest(), 16)
return 20000 + (h % 10000)
def _auto_state_dir_name(repo_root) -> str:
import hashlib
h = hashlib.md5(str(repo_root).encode()).hexdigest()[:8]
return f"webui-test-{h}"
TEST_PORT = int(os.getenv('HERMES_WEBUI_TEST_PORT',
str(_auto_test_port(REPO_ROOT))))
TEST_BASE = f"http://127.0.0.1:{TEST_PORT}" TEST_BASE = f"http://127.0.0.1:{TEST_PORT}"
TEST_STATE_DIR = pathlib.Path(os.getenv( TEST_STATE_DIR = pathlib.Path(os.getenv(
'HERMES_WEBUI_TEST_STATE_DIR', 'HERMES_WEBUI_TEST_STATE_DIR',
str(HERMES_HOME / 'webui-mvp-test') str(HERMES_HOME / _auto_state_dir_name(REPO_ROOT))
)) ))
TEST_WORKSPACE = TEST_STATE_DIR / 'test-workspace' TEST_WORKSPACE = TEST_STATE_DIR / 'test-workspace'
# Publish at module level so _pytest_port.py (imported at collection time)
# and any test file using os.environ sees the right values immediately.
os.environ.setdefault('HERMES_WEBUI_TEST_PORT', str(TEST_PORT))
os.environ.setdefault('HERMES_WEBUI_TEST_STATE_DIR', str(TEST_STATE_DIR))
# ── Server script: always relative to repo root ─────────────────────────── # ── Server script: always relative to repo root ───────────────────────────
SERVER_SCRIPT = REPO_ROOT / 'server.py' SERVER_SCRIPT = REPO_ROOT / 'server.py'
if not SERVER_SCRIPT.exists(): if not SERVER_SCRIPT.exists():
@@ -245,7 +268,10 @@ def test_server():
# as the server. Other test files (test_auth_sessions.py) may override # as the server. Other test files (test_auth_sessions.py) may override
# HERMES_WEBUI_STATE_DIR for their own purposes, but HERMES_WEBUI_TEST_STATE_DIR # HERMES_WEBUI_STATE_DIR for their own purposes, but HERMES_WEBUI_TEST_STATE_DIR
# is reserved for this mapping and is never overridden by individual test files. # is reserved for this mapping and is never overridden by individual test files.
os.environ.setdefault('HERMES_WEBUI_TEST_STATE_DIR', str(TEST_STATE_DIR)) # Export both port and state-dir as env vars so individual test files
# can read them without importing conftest (avoids circular imports).
os.environ.setdefault('HERMES_WEBUI_TEST_PORT', str(TEST_PORT))
# os.environ already set at module level above; no-op here.
env = os.environ.copy() env = os.environ.copy()
env.update({ env.update({

View File

@@ -41,7 +41,7 @@ pytestmark = pytest.mark.skipif(
reason="tools.approval not available in this environment" reason="tools.approval not available in this environment"
) )
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -18,7 +18,7 @@ import urllib.error
import urllib.request import urllib.request
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):
@@ -49,11 +49,9 @@ def _get_test_state_dir():
set (e.g. when running this file standalone), fall back to the conftest set (e.g. when running this file standalone), fall back to the conftest
formula: HERMES_HOME/webui-mvp-test. formula: HERMES_HOME/webui-mvp-test.
""" """
explicit = os.getenv('HERMES_WEBUI_TEST_STATE_DIR') # Use _pytest_port which applies the same auto-derivation as conftest.py
if explicit: from tests._pytest_port import TEST_STATE_DIR as _ptsd
return pathlib.Path(explicit) return _ptsd
hermes_home = pathlib.Path(os.getenv('HERMES_HOME', str(pathlib.Path.home() / '.hermes')))
return hermes_home / 'webui-mvp-test' # matches conftest.py TEST_STATE_DIR formula
def _get_state_db_path(): def _get_state_db_path():

View File

@@ -34,7 +34,7 @@ STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text()
INDEX_HTML = (REPO_ROOT / "static" / "index.html").read_text() INDEX_HTML = (REPO_ROOT / "static" / "index.html").read_text()
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text() I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text()
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def _get(path): def _get(path):
@@ -261,7 +261,7 @@ class TestBubbleLayoutI18N(unittest.TestCase):
) )
# ── Integration tests (require live server on port 8788) ───────────────── # ── Integration tests (require live server on test server port) ─────────────────
class TestBubbleLayoutSettingsAPI(unittest.TestCase): class TestBubbleLayoutSettingsAPI(unittest.TestCase):
@@ -272,7 +272,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase):
try: try:
d, status = _get("/api/settings") d, status = _get("/api/settings")
except OSError: except OSError:
self.skipTest("Server not running on port 8788") self.skipTest("Server not running on test server port")
self.assertEqual(status, 200) self.assertEqual(status, 200)
self.assertIn( self.assertIn(
"bubble_layout", "bubble_layout",
@@ -289,7 +289,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase):
try: try:
_, status = _post("/api/settings", {"bubble_layout": True}) _, status = _post("/api/settings", {"bubble_layout": True})
except OSError: except OSError:
self.skipTest("Server not running on port 8788") self.skipTest("Server not running on test server port")
self.assertEqual(status, 200) self.assertEqual(status, 200)
d, _ = _get("/api/settings") d, _ = _get("/api/settings")
self.assertTrue(d["bubble_layout"], "bubble_layout=True must persist after POST") self.assertTrue(d["bubble_layout"], "bubble_layout=True must persist after POST")
@@ -302,7 +302,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase):
_post("/api/settings", {"bubble_layout": True}) _post("/api/settings", {"bubble_layout": True})
_post("/api/settings", {"bubble_layout": False}) _post("/api/settings", {"bubble_layout": False})
except OSError: except OSError:
self.skipTest("Server not running on port 8788") self.skipTest("Server not running on test server port")
d, _ = _get("/api/settings") d, _ = _get("/api/settings")
self.assertFalse(d["bubble_layout"], "bubble_layout=False must persist after POST") self.assertFalse(d["bubble_layout"], "bubble_layout=False must persist after POST")
@@ -311,7 +311,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase):
try: try:
_post("/api/settings", {"bubble_layout": "1"}) _post("/api/settings", {"bubble_layout": "1"})
except OSError: except OSError:
self.skipTest("Server not running on port 8788") self.skipTest("Server not running on test server port")
d, _ = _get("/api/settings") d, _ = _get("/api/settings")
self.assertIsInstance( self.assertIsInstance(
d["bubble_layout"], d["bubble_layout"],

View File

@@ -3,7 +3,7 @@ import urllib.error
import urllib.request import urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -12,6 +12,7 @@ Covers:
from __future__ import annotations from __future__ import annotations
import json import json
import os
import pathlib import pathlib
import urllib.error import urllib.error
import urllib.request import urllib.request
@@ -187,7 +188,7 @@ class TestApplyOnboardingSetupGuard:
# Integration tests — require the live test server on port 8788 # Integration tests — require the live test server on port 8788
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def _http_get(path): def _http_get(path):
@@ -213,7 +214,7 @@ def _server_hermes_home() -> pathlib.Path:
env_path = data.get("system", {}).get("env_path", "") env_path = data.get("system", {}).get("env_path", "")
if env_path: if env_path:
return pathlib.Path(env_path).parent return pathlib.Path(env_path).parent
return pathlib.Path.home() / ".hermes" / "webui-mvp-test" return pathlib.Path(os.environ.get("HERMES_WEBUI_TEST_STATE_DIR", str(pathlib.Path.home() / ".hermes" / "webui-mvp-test")))
def _server_reachable() -> bool: def _server_reachable() -> bool:

View File

@@ -13,7 +13,7 @@ import urllib.request
import pytest import pytest
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
# Check if pyyaml is available — onboarding setup tests need it on the server # Check if pyyaml is available — onboarding setup tests need it on the server
try: try:

View File

@@ -24,7 +24,7 @@ import urllib.request
import pytest import pytest
REPO = pathlib.Path(__file__).parent.parent REPO = pathlib.Path(__file__).parent.parent
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Unit tests — directly test the IP-resolution + guard logic in routes.py # Unit tests — directly test the IP-resolution + guard logic in routes.py
@@ -128,14 +128,14 @@ class TestOnboardingIPLogic:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Integration tests — hit the live test server at port 8788 # Integration tests — hit the live test server at test server port
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@pytest.mark.integration @pytest.mark.integration
class TestOnboardingSetupEndpoint: class TestOnboardingSetupEndpoint:
""" """
Integration tests for /api/onboarding/setup. Integration tests for /api/onboarding/setup.
These require the test server running on port 8788. These require the test server running on test server port.
""" """
def _post(self, path: str, data: dict, headers: dict | None = None) -> tuple[int, dict]: def _post(self, path: str, data: dict, headers: dict | None = None) -> tuple[int, dict]:
@@ -157,7 +157,7 @@ class TestOnboardingSetupEndpoint:
Requests from 127.0.0.1 (which is what the test server sees) should Requests from 127.0.0.1 (which is what the test server sees) should
pass the IP check. We confirm no 403 is returned. pass the IP check. We confirm no 403 is returned.
""" """
# The test server runs on 127.0.0.1:8788 so client_address[0] is 127.0.0.1. # The test server runs on 127.0.0.1:{TEST_PORT} so client_address[0] is 127.0.0.1.
# A valid setup payload with a mock provider should not be rejected for IP reasons. # A valid setup payload with a mock provider should not be rejected for IP reasons.
# We patch apply_onboarding_setup to avoid actually writing any config. # We patch apply_onboarding_setup to avoid actually writing any config.
import unittest.mock import unittest.mock

View File

@@ -16,7 +16,7 @@ import re
import urllib.request import urllib.request
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def _read(rel_path: str) -> str: def _read(rel_path: str) -> str:

View File

@@ -5,6 +5,7 @@ These tests exist specifically to prevent those bugs from silently returning.
Each test is tagged with the sprint/commit where the bug was found and fixed. Each test is tagged with the sprint/commit where the bug was found and fixed.
""" """
import json import json
import os
import pathlib import pathlib
import time import time
import urllib.error import urllib.error
@@ -12,7 +13,7 @@ import urllib.request
import urllib.parse import urllib.parse
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:
@@ -104,7 +105,7 @@ def test_session_with_tool_calls_in_json_loads_ok(cleanup_test_sessions):
sid = make_session(cleanup_test_sessions) sid = make_session(cleanup_test_sessions)
# Manually inject tool_calls into the session's JSON file # Manually inject tool_calls into the session's JSON file
sessions_dir = pathlib.Path.home() / ".hermes" / "webui-mvp-test" / "sessions" sessions_dir = pathlib.Path(os.environ.get("HERMES_WEBUI_TEST_STATE_DIR", str(pathlib.Path.home() / ".hermes" / "webui-mvp-test"))) / "sessions"
session_file = sessions_dir / f"{sid}.json" session_file = sessions_dir / f"{sid}.json"
if session_file.exists(): if session_file.exists():
d = json.loads(session_file.read_text()) d = json.loads(session_file.read_text())

View File

@@ -33,7 +33,7 @@ def _server_is_up(port: int = 8788) -> bool:
# The skipif is evaluated lazily via the fixture, not at collection time. # The skipif is evaluated lazily via the fixture, not at collection time.
_needs_server = pytest.mark.usefixtures("test_server") _needs_server = pytest.mark.usefixtures("test_server")
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
# Sample credentials that should be masked in every API response # Sample credentials that should be masked in every API response
_FAKE_GITHUB_PAT = "ghp_TestFakeCredential1234567890ab" _FAKE_GITHUB_PAT = "ghp_TestFakeCredential1234567890ab"

View File

@@ -11,7 +11,7 @@ import pytest
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent)) sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent))
_needs_server = pytest.mark.usefixtures("test_server") _needs_server = pytest.mark.usefixtures("test_server")
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
_FULL_SECRET = "sk-" + ("B" * 24) _FULL_SECRET = "sk-" + ("B" * 24)

View File

@@ -1,7 +1,7 @@
""" """
Sprint 1 test suite for the Hermes Web UI. Sprint 1 test suite for the Hermes Web UI.
Tests use the ISOLATED test server running on http://127.0.0.1:8788. Tests use the ISOLATED test server. Port is auto-derived per worktree (see conftest.py).
Production server (port 8787) and your real conversations are never touched. Production server (port 8787) and your real conversations are never touched.
Start the server before running: Start the server before running:
<repo>/start.sh <repo>/start.sh
@@ -27,7 +27,7 @@ import pathlib
# Allow importing server modules directly for unit tests # Allow importing server modules directly for unit tests
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent)) sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent))
BASE = "http://127.0.0.1:8788" # test server (isolated from production) from tests._pytest_port import BASE
# ────────────────────────────────────────────── # ──────────────────────────────────────────────

View File

@@ -4,7 +4,7 @@ Sprint 10 Tests: server.py split, cancel endpoint, cron history, tool card polis
import json, pathlib, urllib.error, urllib.request, urllib.parse import json, pathlib, urllib.error, urllib.request, urllib.parse
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -4,7 +4,7 @@ Sprint 11 Tests: multi-provider model support, streaming smoothness, routes extr
import json, pathlib, urllib.error, urllib.request, urllib.parse import json, pathlib, urllib.error, urllib.request, urllib.parse
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -3,7 +3,7 @@ Sprint 12 Tests: settings panel, session pinning, session import, SSE reconnect.
""" """
import json, pathlib, urllib.error, urllib.request, urllib.parse import json, pathlib, urllib.error, urllib.request, urllib.parse
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -3,7 +3,7 @@ Sprint 13 Tests: cron recent endpoint, session duplicate, background alerts.
""" """
import json, pathlib, urllib.error, urllib.request import json, pathlib, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -3,7 +3,7 @@ Sprint 14 Tests: file rename, folder create, session archive, session tags, merm
""" """
import json, os, pathlib, shutil, tempfile, urllib.error, urllib.request import json, os, pathlib, shutil, tempfile, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -3,7 +3,7 @@ Sprint 15 Tests: session projects (CRUD, move, backward compat).
""" """
import json, urllib.error, urllib.request import json, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -7,7 +7,7 @@ import pathlib
import re import re
import urllib.request import urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
REPO_ROOT = pathlib.Path(__file__).parent.parent REPO_ROOT = pathlib.Path(__file__).parent.parent

View File

@@ -3,7 +3,7 @@ Sprint 17 Tests: send_key setting, commands.js static file, workspace subdir lis
""" """
import json, urllib.error, urllib.request import json, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -3,7 +3,7 @@ Sprint 19 Tests: auth/login, security headers, request size limit.
""" """
import json, urllib.error, urllib.request import json, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path, headers=None): def get(path, headers=None):

View File

@@ -1,7 +1,7 @@
"""Sprint 2 tests: image preview, file types, markdown. Uses cleanup_test_sessions fixture.""" """Sprint 2 tests: image preview, file types, markdown. Uses cleanup_test_sessions fixture."""
import io, json, uuid, urllib.request, urllib.error, pathlib import io, json, uuid, urllib.request, urllib.error, pathlib
BASE = "http://127.0.0.1:8788" # test server (isolated from production) from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -10,7 +10,7 @@ import urllib.request
import json import json
import pathlib import pathlib
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get_text(path): def get_text(path):

View File

@@ -5,7 +5,7 @@ icon-only circle design.
import re import re
import urllib.request import urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get_text(path): def get_text(path):

View File

@@ -4,7 +4,7 @@ subagent card names, skill picker in cron, skill linked files.
""" """
import json, urllib.error, urllib.request import json, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -4,7 +4,7 @@ custom theme names accepted.
""" """
import json, urllib.error, urllib.request import json, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -7,7 +7,7 @@ import json
import urllib.error import urllib.error
import urllib.request import urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -14,7 +14,7 @@ import urllib.request
sys.path.insert(0, str(pathlib.Path(__file__).parent)) sys.path.insert(0, str(pathlib.Path(__file__).parent))
from conftest import TEST_STATE_DIR from conftest import TEST_STATE_DIR
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -27,7 +27,7 @@ import urllib.request
sys.path.insert(0, str(pathlib.Path(__file__).parent)) sys.path.insert(0, str(pathlib.Path(__file__).parent))
from conftest import TEST_STATE_DIR from conftest import TEST_STATE_DIR
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path, headers=None): def get(path, headers=None):

View File

@@ -1,7 +1,7 @@
"""Sprint 3 tests: cron API, skills API, memory API, input validation.""" """Sprint 3 tests: cron API, skills API, memory API, input validation."""
import json, uuid, urllib.request, urllib.error import json, uuid, urllib.request, urllib.error
BASE = "http://127.0.0.1:8788" # test server (isolated from production) from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -19,7 +19,7 @@ import urllib.parse
import pytest import pytest
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):

View File

@@ -68,7 +68,7 @@ class TestWriteEndpointToConfig:
# ── 6-7: API integration tests ──────────────────────────────────────────────── # ── 6-7: API integration tests ────────────────────────────────────────────────
_TEST_BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE as _TEST_BASE
def _post(path, body=None): def _post(path, body=None):

View File

@@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import subprocess import subprocess
import os
from api.startup import auto_install_agent_deps from api.startup import auto_install_agent_deps
class TestAutoInstallAgentDeps: class TestAutoInstallAgentDeps:

View File

@@ -22,7 +22,7 @@ import unittest.mock
import pytest import pytest
REPO = pathlib.Path(__file__).parent.parent REPO = pathlib.Path(__file__).parent.parent
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
# ── Helpers ────────────────────────────────────────────────────────────────── # ── Helpers ──────────────────────────────────────────────────────────────────

View File

@@ -1,7 +1,7 @@
"""Sprint 4 tests: relocation, session rename, search, file ops, validation.""" """Sprint 4 tests: relocation, session rename, search, file ops, validation."""
import json, pathlib, uuid, urllib.request, urllib.error import json, pathlib, uuid, urllib.request, urllib.error
BASE = "http://127.0.0.1:8788" # test server (isolated from production) from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -14,7 +14,7 @@ import urllib.request
import os import os
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
REPO = pathlib.Path(__file__).parent.parent REPO = pathlib.Path(__file__).parent.parent
# Use HERMES_WEBUI_TEST_STATE_DIR if available (set by conftest for the test process), # Use HERMES_WEBUI_TEST_STATE_DIR if available (set by conftest for the test process),
# falling back to the conventional webui-mvp-test path. # falling back to the conventional webui-mvp-test path.

View File

@@ -1,7 +1,8 @@
"""Sprint 5 tests: workspace CRUD, file save, session index, JS serving.""" """Sprint 5 tests: workspace CRUD, file save, session index, JS serving."""
import json, pathlib, uuid, urllib.request, urllib.error import json, pathlib, uuid, urllib.request, urllib.error
import os
BASE = "http://127.0.0.1:8788" # test server (isolated from production) from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:
@@ -132,7 +133,7 @@ def test_file_save_path_traversal_blocked(cleanup_test_sessions):
def test_session_index_created_after_save(cleanup_test_sessions): def test_session_index_created_after_save(cleanup_test_sessions):
# Index is created in the TEST state dir, not the production dir # Index is created in the TEST state dir, not the production dir
test_state_dir = pathlib.Path.home() / ".hermes" / "webui-mvp-test" test_state_dir = pathlib.Path(os.environ.get("HERMES_WEBUI_TEST_STATE_DIR", str(pathlib.Path.home() / ".hermes" / "webui-mvp-test")))
index_path = test_state_dir / "sessions" / "_index.json" index_path = test_state_dir / "sessions" / "_index.json"
make_session_tracked(cleanup_test_sessions) make_session_tracked(cleanup_test_sessions)
# Index may not exist yet if cleanup already wiped it -- just check the endpoint works # Index may not exist yet if cleanup already wiped it -- just check the endpoint works

View File

@@ -2,7 +2,7 @@
import json, uuid, pathlib, urllib.request, urllib.error import json, uuid, pathlib, urllib.request, urllib.error
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
BASE = "http://127.0.0.1:8788" # isolated test server from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -3,7 +3,7 @@ Sprint 7 Tests: Cron CRUD, Skill CRUD, Memory Write, Session Content Search, Hea
""" """
import json, pathlib, urllib.error, urllib.parse, urllib.request import json, pathlib, urllib.error, urllib.parse, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -3,7 +3,7 @@ Sprint 8 Tests: Edit/regenerate, clear conversation, truncate, reconnect banner
""" """
import json, pathlib, urllib.error, urllib.parse, urllib.request import json, pathlib, urllib.error, urllib.parse, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get(path): def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r:

View File

@@ -4,7 +4,7 @@ Run: python -m pytest tests/test_sprint9.py -v
""" """
import json, pathlib, urllib.error, urllib.request import json, pathlib, urllib.error, urllib.request
BASE = "http://127.0.0.1:8788" from tests._pytest_port import BASE
def get_text(path): def get_text(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r: with urllib.request.urlopen(BASE + path, timeout=10) as r: