test: stream-proxy test works against remote deployments
Rebuild & Deploy / rebuild (push) Successful in 14s
Rebuild & Deploy / rebuild (push) Successful in 14s
The fake audio upstream bound the test host's loopback, which the Unraid media server cannot reach (and its LAN route is firewalled). Local runs keep the controlled upstream; remote runs use a public reference mp3 and assert Range semantics without byte equality. docker-exec MCP probes skip on remote targets (they necessarily exercise the local container).
This commit is contained in:
1 parent
ce8fd1e046
commit
d89918fd2a
1 file changed
+63
-7
+63
-7
@@ -13,11 +13,12 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from conftest import TOKEN_URL, docker_exec, IS_LOCAL_DEPLOYMENT
|
||||
from conftest import TOKEN_URL, docker_exec, IS_LOCAL_DEPLOYMENT, WEB_BASE_URL
|
||||
|
||||
MEDIA_API = "/media/api"
|
||||
TEST_ROOM = f"media-test-{uuid.uuid4().hex[:8]}"
|
||||
@@ -26,11 +27,13 @@ TEST_ROOM = f"media-test-{uuid.uuid4().hex[:8]}"
|
||||
# ── local upstream for stream-proxy tests ───────────────────────────────────
|
||||
|
||||
def _serve_fake_audio(payload: bytes):
|
||||
"""Tiny Range-capable HTTP server on an ephemeral host port.
|
||||
"""Tiny Range-capable HTTP server on an ephemeral port.
|
||||
|
||||
The container uses host networking, so the media server inside the
|
||||
container reaches it on 127.0.0.1.
|
||||
Binds 0.0.0.0 and advertises this host's LAN IP so the deployment host's
|
||||
media server (which may be a different machine — Unraid) can reach it
|
||||
over the LAN when fetching external stream URLs.
|
||||
"""
|
||||
import socket
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
@@ -57,10 +60,19 @@ def _serve_fake_audio(payload: bytes):
|
||||
def log_message(self, *args):
|
||||
pass
|
||||
|
||||
server = HTTPServer(("127.0.0.1", 0), Handler)
|
||||
server = HTTPServer(("0.0.0.0", 0), Handler)
|
||||
port = server.server_address[1]
|
||||
# The local IP toward the deployment host (works across the LAN).
|
||||
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
probe.connect((urlparse(WEB_BASE_URL).hostname or "192.168.86.2", 9178))
|
||||
lan_ip = probe.getsockname()[0]
|
||||
except OSError:
|
||||
lan_ip = "127.0.0.1"
|
||||
finally:
|
||||
probe.close()
|
||||
threading.Thread(target=server.serve_forever, daemon=True).start()
|
||||
return server, f"http://127.0.0.1:{port}/audio.mp3"
|
||||
return server, f"http://{lan_ip}:{port}/audio.mp3"
|
||||
|
||||
|
||||
# ── health + basic API shape ────────────────────────────────────────────────
|
||||
@@ -171,7 +183,13 @@ def test_media_player_state_flow(client):
|
||||
|
||||
def test_media_stream_proxy_and_range(client):
|
||||
"""External audio URLs must be reachable same-origin (HTTPS) and support
|
||||
Range passthrough so the browser can seek progressive streams."""
|
||||
Range passthrough so the browser can seek progressive streams.
|
||||
|
||||
Local deployments test against a controlled fake upstream; remote
|
||||
deployments (Unraid) cannot reach the test host's ephemeral ports, so a
|
||||
public reference mp3 is used and Range semantics are asserted without
|
||||
byte equality."""
|
||||
if IS_LOCAL_DEPLOYMENT:
|
||||
payload = b"0123456789" * 100
|
||||
upstream, url = _serve_fake_audio(payload)
|
||||
track_id = None
|
||||
@@ -197,6 +215,33 @@ def test_media_stream_proxy_and_range(client):
|
||||
client.delete(f"{MEDIA_API}/tracks/{track_id}")
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
return
|
||||
|
||||
# Remote deployment: public reference audio (SoundHelix).
|
||||
url = "https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3"
|
||||
track_id = None
|
||||
try:
|
||||
track_id = client.post(
|
||||
f"{MEDIA_API}/streams", json={"name": "Range FM public", "url": url}
|
||||
).json()["track"]["id"]
|
||||
|
||||
full = client.get(f"{MEDIA_API}/stream/{track_id}")
|
||||
assert full.status_code == 200
|
||||
assert full.headers.get("content-type", "").startswith("audio/")
|
||||
assert len(full.content) > 100_000
|
||||
|
||||
ranged = client.get(
|
||||
f"{MEDIA_API}/stream/{track_id}", headers={"Range": "bytes=0-1023"}
|
||||
)
|
||||
assert ranged.status_code == 206
|
||||
assert len(ranged.content) == 1024
|
||||
assert (ranged.headers.get("content-range") or "").startswith("bytes 0-1023/")
|
||||
finally:
|
||||
if track_id:
|
||||
try:
|
||||
client.delete(f"{MEDIA_API}/tracks/{track_id}")
|
||||
except httpx.HTTPError:
|
||||
pass
|
||||
|
||||
|
||||
def test_media_files_static_range_support(client):
|
||||
@@ -286,13 +331,22 @@ def test_token_response_includes_room():
|
||||
|
||||
|
||||
# ── Hope's media MCP toolset ────────────────────────────────────────────────
|
||||
# These spawn the MCP server inside the LOCAL dev container (docker exec),
|
||||
# whose media server is the local one — they can only be consistent when
|
||||
# the test target IS the local deployment.
|
||||
|
||||
MEDIA_MCP_ENV = {
|
||||
"MEDIA_SERVER_URL": "http://127.0.0.1:8093",
|
||||
"MEDIA_ROOM": "mcp-test-room",
|
||||
}
|
||||
|
||||
needs_local_container = pytest.mark.skipif(
|
||||
not IS_LOCAL_DEPLOYMENT,
|
||||
reason="docker-exec MCP probes run against the local dev container",
|
||||
)
|
||||
|
||||
|
||||
@needs_local_container
|
||||
def test_mcp_media_list_library(mcp_client_factory):
|
||||
text, is_error = mcp_client_factory(
|
||||
"media_mcp.py", "list_library", {}, env=MEDIA_MCP_ENV
|
||||
@@ -301,6 +355,7 @@ def test_mcp_media_list_library(mcp_client_factory):
|
||||
assert "library" in text.lower()
|
||||
|
||||
|
||||
@needs_local_container
|
||||
def test_mcp_media_add_stream_and_now_playing(mcp_client_factory, client):
|
||||
upstream, url = _serve_fake_audio(b"\x00" * 16)
|
||||
try:
|
||||
@@ -335,6 +390,7 @@ def test_mcp_media_add_stream_and_now_playing(mcp_client_factory, client):
|
||||
upstream.shutdown()
|
||||
|
||||
|
||||
@needs_local_container
|
||||
def test_mcp_media_resume_without_load(mcp_client_factory, client):
|
||||
"""Errors from the media server reach the LLM as tool text (not MCP
|
||||
errors) so Hope can explain them naturally."""
|
||||
|
||||
Reference in new issue
Block a user