fix(tests): auto-derive unique port+state-dir per worktree (fixes parallel pytest)
This commit is contained in:
42
tests/_pytest_port.py
Normal file
42
tests/_pytest_port.py
Normal 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))
|
||||||
|
))
|
||||||
@@ -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({
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────
|
# ──────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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 ──────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user