From c3251ea97d8a4da505a7843c0564b39f074d67e2 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 14 Apr 2026 19:04:48 +0000 Subject: [PATCH] fix(tests): auto-derive unique port+state-dir per worktree (fixes parallel pytest) --- tests/_pytest_port.py | 42 ++++++++++++++++++++++++ tests/conftest.py | 32 ++++++++++++++++-- tests/test_approval_unblock.py | 2 +- tests/test_gateway_sync.py | 10 +++--- tests/test_issue336.py | 12 +++---- tests/test_login_locale.py | 2 +- tests/test_onboarding_existing_config.py | 5 +-- tests/test_onboarding_mvp.py | 2 +- tests/test_onboarding_network.py | 8 ++--- tests/test_provider_mismatch.py | 2 +- tests/test_regressions.py | 5 +-- tests/test_security_redaction.py | 2 +- tests/test_session_summary_redaction.py | 2 +- tests/test_sprint1.py | 4 +-- tests/test_sprint10.py | 2 +- tests/test_sprint11.py | 2 +- tests/test_sprint12.py | 2 +- tests/test_sprint13.py | 2 +- tests/test_sprint14.py | 2 +- tests/test_sprint15.py | 2 +- tests/test_sprint16.py | 2 +- tests/test_sprint17.py | 2 +- tests/test_sprint19.py | 2 +- tests/test_sprint2.py | 2 +- tests/test_sprint20.py | 2 +- tests/test_sprint20b.py | 2 +- tests/test_sprint23.py | 2 +- tests/test_sprint26.py | 2 +- tests/test_sprint27.py | 2 +- tests/test_sprint28.py | 2 +- tests/test_sprint29.py | 2 +- tests/test_sprint3.py | 2 +- tests/test_sprint30.py | 2 +- tests/test_sprint31.py | 2 +- tests/test_sprint32.py | 1 + tests/test_sprint34.py | 2 +- tests/test_sprint4.py | 2 +- tests/test_sprint45.py | 2 +- tests/test_sprint5.py | 5 +-- tests/test_sprint6.py | 2 +- tests/test_sprint7.py | 2 +- tests/test_sprint8.py | 2 +- tests/test_sprint9.py | 2 +- 43 files changed, 130 insertions(+), 60 deletions(-) create mode 100644 tests/_pytest_port.py diff --git a/tests/_pytest_port.py b/tests/_pytest_port.py new file mode 100644 index 0000000..19afc6a --- /dev/null +++ b/tests/_pytest_port.py @@ -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)) +)) diff --git a/tests/conftest.py b/tests/conftest.py index 910add5..b45d4f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,14 +31,37 @@ HOME = pathlib.Path.home() HERMES_HOME = pathlib.Path(os.getenv('HERMES_HOME', str(HOME / '.hermes'))) # ── 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_STATE_DIR = pathlib.Path(os.getenv( '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' +# 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 = REPO_ROOT / 'server.py' if not SERVER_SCRIPT.exists(): @@ -245,7 +268,10 @@ def test_server(): # 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 # 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.update({ diff --git a/tests/test_approval_unblock.py b/tests/test_approval_unblock.py index 9fa10e9..2f6f2c7 100644 --- a/tests/test_approval_unblock.py +++ b/tests/test_approval_unblock.py @@ -41,7 +41,7 @@ pytestmark = pytest.mark.skipif( reason="tools.approval not available in this environment" ) -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_gateway_sync.py b/tests/test_gateway_sync.py index 1f38bf5..39adc5c 100644 --- a/tests/test_gateway_sync.py +++ b/tests/test_gateway_sync.py @@ -18,7 +18,7 @@ import urllib.error import urllib.request REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE 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 formula: HERMES_HOME/webui-mvp-test. """ - explicit = os.getenv('HERMES_WEBUI_TEST_STATE_DIR') - if explicit: - return pathlib.Path(explicit) - 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 + # Use _pytest_port which applies the same auto-derivation as conftest.py + from tests._pytest_port import TEST_STATE_DIR as _ptsd + return _ptsd def _get_state_db_path(): diff --git a/tests/test_issue336.py b/tests/test_issue336.py index 25322ad..48b8d28 100644 --- a/tests/test_issue336.py +++ b/tests/test_issue336.py @@ -34,7 +34,7 @@ STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text() INDEX_HTML = (REPO_ROOT / "static" / "index.html").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): @@ -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): @@ -272,7 +272,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase): try: d, status = _get("/api/settings") except OSError: - self.skipTest("Server not running on port 8788") + self.skipTest("Server not running on test server port") self.assertEqual(status, 200) self.assertIn( "bubble_layout", @@ -289,7 +289,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase): try: _, status = _post("/api/settings", {"bubble_layout": True}) except OSError: - self.skipTest("Server not running on port 8788") + self.skipTest("Server not running on test server port") self.assertEqual(status, 200) d, _ = _get("/api/settings") 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": False}) except OSError: - self.skipTest("Server not running on port 8788") + self.skipTest("Server not running on test server port") d, _ = _get("/api/settings") self.assertFalse(d["bubble_layout"], "bubble_layout=False must persist after POST") @@ -311,7 +311,7 @@ class TestBubbleLayoutSettingsAPI(unittest.TestCase): try: _post("/api/settings", {"bubble_layout": "1"}) except OSError: - self.skipTest("Server not running on port 8788") + self.skipTest("Server not running on test server port") d, _ = _get("/api/settings") self.assertIsInstance( d["bubble_layout"], diff --git a/tests/test_login_locale.py b/tests/test_login_locale.py index 5cdea58..09829c5 100644 --- a/tests/test_login_locale.py +++ b/tests/test_login_locale.py @@ -3,7 +3,7 @@ import urllib.error import urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_onboarding_existing_config.py b/tests/test_onboarding_existing_config.py index 2484063..e644b66 100644 --- a/tests/test_onboarding_existing_config.py +++ b/tests/test_onboarding_existing_config.py @@ -12,6 +12,7 @@ Covers: from __future__ import annotations import json +import os import pathlib import urllib.error import urllib.request @@ -187,7 +188,7 @@ class TestApplyOnboardingSetupGuard: # 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): @@ -213,7 +214,7 @@ def _server_hermes_home() -> pathlib.Path: env_path = data.get("system", {}).get("env_path", "") if env_path: 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: diff --git a/tests/test_onboarding_mvp.py b/tests/test_onboarding_mvp.py index 39134d5..82ac607 100644 --- a/tests/test_onboarding_mvp.py +++ b/tests/test_onboarding_mvp.py @@ -13,7 +13,7 @@ import urllib.request 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 try: diff --git a/tests/test_onboarding_network.py b/tests/test_onboarding_network.py index 13eab5e..2535212 100644 --- a/tests/test_onboarding_network.py +++ b/tests/test_onboarding_network.py @@ -24,7 +24,7 @@ import urllib.request import pytest 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 @@ -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 class TestOnboardingSetupEndpoint: """ 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]: @@ -157,7 +157,7 @@ class TestOnboardingSetupEndpoint: Requests from 127.0.0.1 (which is what the test server sees) should 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. # We patch apply_onboarding_setup to avoid actually writing any config. import unittest.mock diff --git a/tests/test_provider_mismatch.py b/tests/test_provider_mismatch.py index f0e9ef9..fb72b94 100644 --- a/tests/test_provider_mismatch.py +++ b/tests/test_provider_mismatch.py @@ -16,7 +16,7 @@ import re import urllib.request 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: diff --git a/tests/test_regressions.py b/tests/test_regressions.py index ce06ba1..fba311f 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -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. """ import json +import os import pathlib import time import urllib.error @@ -12,7 +13,7 @@ import urllib.request import urllib.parse REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): 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) # 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" if session_file.exists(): d = json.loads(session_file.read_text()) diff --git a/tests/test_security_redaction.py b/tests/test_security_redaction.py index fc24f94..09154d3 100644 --- a/tests/test_security_redaction.py +++ b/tests/test_security_redaction.py @@ -33,7 +33,7 @@ def _server_is_up(port: int = 8788) -> bool: # The skipif is evaluated lazily via the fixture, not at collection time. _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 _FAKE_GITHUB_PAT = "ghp_TestFakeCredential1234567890ab" diff --git a/tests/test_session_summary_redaction.py b/tests/test_session_summary_redaction.py index dfa6728..e28645a 100644 --- a/tests/test_session_summary_redaction.py +++ b/tests/test_session_summary_redaction.py @@ -11,7 +11,7 @@ import pytest sys.path.insert(0, str(pathlib.Path(__file__).parent.parent.parent)) _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) diff --git a/tests/test_sprint1.py b/tests/test_sprint1.py index e746328..664a65e 100644 --- a/tests/test_sprint1.py +++ b/tests/test_sprint1.py @@ -1,7 +1,7 @@ """ 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. Start the server before running: /start.sh @@ -27,7 +27,7 @@ import pathlib # Allow importing server modules directly for unit tests 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 # ────────────────────────────────────────────── diff --git a/tests/test_sprint10.py b/tests/test_sprint10.py index 00c5aa6..a976d3f 100644 --- a/tests/test_sprint10.py +++ b/tests/test_sprint10.py @@ -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 REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint11.py b/tests/test_sprint11.py index a02a2de..3a4ecaa 100644 --- a/tests/test_sprint11.py +++ b/tests/test_sprint11.py @@ -4,7 +4,7 @@ Sprint 11 Tests: multi-provider model support, streaming smoothness, routes extr import json, pathlib, urllib.error, urllib.request, urllib.parse REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve() -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint12.py b/tests/test_sprint12.py index 4426687..d5ea3a6 100644 --- a/tests/test_sprint12.py +++ b/tests/test_sprint12.py @@ -3,7 +3,7 @@ Sprint 12 Tests: settings panel, session pinning, session import, SSE reconnect. """ 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): diff --git a/tests/test_sprint13.py b/tests/test_sprint13.py index 9c3126a..5d54f35 100644 --- a/tests/test_sprint13.py +++ b/tests/test_sprint13.py @@ -3,7 +3,7 @@ Sprint 13 Tests: cron recent endpoint, session duplicate, background alerts. """ import json, pathlib, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint14.py b/tests/test_sprint14.py index 8826e75..074ca32 100644 --- a/tests/test_sprint14.py +++ b/tests/test_sprint14.py @@ -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 -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint15.py b/tests/test_sprint15.py index 1a22656..8fb0678 100644 --- a/tests/test_sprint15.py +++ b/tests/test_sprint15.py @@ -3,7 +3,7 @@ Sprint 15 Tests: session projects (CRUD, move, backward compat). """ import json, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint16.py b/tests/test_sprint16.py index 0299bb3..897713e 100644 --- a/tests/test_sprint16.py +++ b/tests/test_sprint16.py @@ -7,7 +7,7 @@ import pathlib import re import urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE REPO_ROOT = pathlib.Path(__file__).parent.parent diff --git a/tests/test_sprint17.py b/tests/test_sprint17.py index 61e4776..9188f34 100644 --- a/tests/test_sprint17.py +++ b/tests/test_sprint17.py @@ -3,7 +3,7 @@ Sprint 17 Tests: send_key setting, commands.js static file, workspace subdir lis """ import json, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint19.py b/tests/test_sprint19.py index 8cb3cbd..b3ce658 100644 --- a/tests/test_sprint19.py +++ b/tests/test_sprint19.py @@ -3,7 +3,7 @@ Sprint 19 Tests: auth/login, security headers, request size limit. """ import json, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path, headers=None): diff --git a/tests/test_sprint2.py b/tests/test_sprint2.py index 0be15a6..aa5c5f2 100644 --- a/tests/test_sprint2.py +++ b/tests/test_sprint2.py @@ -1,7 +1,7 @@ """Sprint 2 tests: image preview, file types, markdown. Uses cleanup_test_sessions fixture.""" 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): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint20.py b/tests/test_sprint20.py index 2e00beb..b523447 100644 --- a/tests/test_sprint20.py +++ b/tests/test_sprint20.py @@ -10,7 +10,7 @@ import urllib.request import json import pathlib -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get_text(path): diff --git a/tests/test_sprint20b.py b/tests/test_sprint20b.py index 4c2b85d..3c3ad85 100644 --- a/tests/test_sprint20b.py +++ b/tests/test_sprint20b.py @@ -5,7 +5,7 @@ icon-only circle design. import re import urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get_text(path): diff --git a/tests/test_sprint23.py b/tests/test_sprint23.py index 8d6a1d9..43909d5 100644 --- a/tests/test_sprint23.py +++ b/tests/test_sprint23.py @@ -4,7 +4,7 @@ subagent card names, skill picker in cron, skill linked files. """ import json, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint26.py b/tests/test_sprint26.py index edb20f5..7463bb5 100644 --- a/tests/test_sprint26.py +++ b/tests/test_sprint26.py @@ -4,7 +4,7 @@ custom theme names accepted. """ import json, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint27.py b/tests/test_sprint27.py index bd5386e..4f5d594 100644 --- a/tests/test_sprint27.py +++ b/tests/test_sprint27.py @@ -7,7 +7,7 @@ import json import urllib.error import urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint28.py b/tests/test_sprint28.py index b64eb48..96f79bd 100644 --- a/tests/test_sprint28.py +++ b/tests/test_sprint28.py @@ -14,7 +14,7 @@ import urllib.request sys.path.insert(0, str(pathlib.Path(__file__).parent)) from conftest import TEST_STATE_DIR -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint29.py b/tests/test_sprint29.py index 5a1ecd5..f486e31 100644 --- a/tests/test_sprint29.py +++ b/tests/test_sprint29.py @@ -27,7 +27,7 @@ import urllib.request sys.path.insert(0, str(pathlib.Path(__file__).parent)) from conftest import TEST_STATE_DIR -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path, headers=None): diff --git a/tests/test_sprint3.py b/tests/test_sprint3.py index 10e1165..0184854 100644 --- a/tests/test_sprint3.py +++ b/tests/test_sprint3.py @@ -1,7 +1,7 @@ """Sprint 3 tests: cron API, skills API, memory API, input validation.""" 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): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint30.py b/tests/test_sprint30.py index b590efb..93dc823 100644 --- a/tests/test_sprint30.py +++ b/tests/test_sprint30.py @@ -19,7 +19,7 @@ import urllib.parse import pytest -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): diff --git a/tests/test_sprint31.py b/tests/test_sprint31.py index fe325c8..64907d5 100644 --- a/tests/test_sprint31.py +++ b/tests/test_sprint31.py @@ -68,7 +68,7 @@ class TestWriteEndpointToConfig: # ── 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): diff --git a/tests/test_sprint32.py b/tests/test_sprint32.py index 63754b6..8430ba3 100644 --- a/tests/test_sprint32.py +++ b/tests/test_sprint32.py @@ -1,6 +1,7 @@ from pathlib import Path from unittest.mock import MagicMock, patch import subprocess +import os from api.startup import auto_install_agent_deps class TestAutoInstallAgentDeps: diff --git a/tests/test_sprint34.py b/tests/test_sprint34.py index ced931b..9b662f0 100644 --- a/tests/test_sprint34.py +++ b/tests/test_sprint34.py @@ -22,7 +22,7 @@ import unittest.mock import pytest REPO = pathlib.Path(__file__).parent.parent -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE # ── Helpers ────────────────────────────────────────────────────────────────── diff --git a/tests/test_sprint4.py b/tests/test_sprint4.py index 11bbb1b..cb5ca42 100644 --- a/tests/test_sprint4.py +++ b/tests/test_sprint4.py @@ -1,7 +1,7 @@ """Sprint 4 tests: relocation, session rename, search, file ops, validation.""" 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): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint45.py b/tests/test_sprint45.py index 8c9da45..031bcc2 100644 --- a/tests/test_sprint45.py +++ b/tests/test_sprint45.py @@ -14,7 +14,7 @@ import urllib.request import os -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE REPO = pathlib.Path(__file__).parent.parent # Use HERMES_WEBUI_TEST_STATE_DIR if available (set by conftest for the test process), # falling back to the conventional webui-mvp-test path. diff --git a/tests/test_sprint5.py b/tests/test_sprint5.py index c15d578..620f11c 100644 --- a/tests/test_sprint5.py +++ b/tests/test_sprint5.py @@ -1,7 +1,8 @@ """Sprint 5 tests: workspace CRUD, file save, session index, JS serving.""" 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): 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): # 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" make_session_tracked(cleanup_test_sessions) # Index may not exist yet if cleanup already wiped it -- just check the endpoint works diff --git a/tests/test_sprint6.py b/tests/test_sprint6.py index ececde0..4c53201 100644 --- a/tests/test_sprint6.py +++ b/tests/test_sprint6.py @@ -2,7 +2,7 @@ import json, uuid, pathlib, urllib.request, urllib.error 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): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint7.py b/tests/test_sprint7.py index 1865653..92a9986 100644 --- a/tests/test_sprint7.py +++ b/tests/test_sprint7.py @@ -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 -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint8.py b/tests/test_sprint8.py index 2841a3d..7be0b66 100644 --- a/tests/test_sprint8.py +++ b/tests/test_sprint8.py @@ -3,7 +3,7 @@ Sprint 8 Tests: Edit/regenerate, clear conversation, truncate, reconnect banner """ 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): with urllib.request.urlopen(BASE + path, timeout=10) as r: diff --git a/tests/test_sprint9.py b/tests/test_sprint9.py index c5482bc..d2ed7d9 100644 --- a/tests/test_sprint9.py +++ b/tests/test_sprint9.py @@ -4,7 +4,7 @@ Run: python -m pytest tests/test_sprint9.py -v """ import json, pathlib, urllib.error, urllib.request -BASE = "http://127.0.0.1:8788" +from tests._pytest_port import BASE def get_text(path): with urllib.request.urlopen(BASE + path, timeout=10) as r: