Merge branch 'master' of https://git.d-popov.com/popov/neuron-tai
# Conflicts: # packages/tracker/meshnet_tracker/cli.py # packages/tracker/meshnet_tracker/dashboard.html # packages/tracker/meshnet_tracker/server.py # tests/test_dashboard.py
This commit is contained in:
@@ -5,6 +5,7 @@ register/login/logout, per-account balance and usage, API-key lifecycle
|
||||
(revoked keys rejected by the OpenAI proxy), and the admin listing.
|
||||
"""
|
||||
|
||||
import http.cookies
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
@@ -68,6 +69,17 @@ def test_sessions_resolve_and_destroy():
|
||||
assert store.session_account("bogus") is None
|
||||
|
||||
|
||||
def test_sessions_persist_across_restart(tmp_path):
|
||||
db = str(tmp_path / "accounts.db")
|
||||
store = AccountStore(db_path=db)
|
||||
account = store.register(email="cookie@example.com", password="secret-123")
|
||||
token = store.create_session(account["account_id"])
|
||||
store.save_to_db()
|
||||
|
||||
reloaded = AccountStore(db_path=db)
|
||||
assert reloaded.session_account(token)["account_id"] == account["account_id"]
|
||||
|
||||
|
||||
def test_api_key_lifecycle():
|
||||
store = AccountStore()
|
||||
account = store.register(email="k@example.com", password="secret-123")
|
||||
@@ -156,6 +168,59 @@ def test_register_login_and_account_view(account_tracker):
|
||||
assert me["usage"]["requests"] == 0
|
||||
|
||||
|
||||
def test_login_sets_cookie_and_cookie_auth_survives_tracker_restart(tmp_path):
|
||||
accounts_db = str(tmp_path / "accounts.db")
|
||||
tracker = TrackerServer(
|
||||
billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02),
|
||||
accounts_db=accounts_db,
|
||||
starting_credit=0.0,
|
||||
devnet_topup_amount=0.0,
|
||||
)
|
||||
port = tracker.start()
|
||||
url = f"http://127.0.0.1:{port}"
|
||||
try:
|
||||
_call(f"{url}/v1/auth/register", "POST",
|
||||
{"email": "cookie-http@example.com", "password": "secret-123"})
|
||||
req = urllib.request.Request(
|
||||
f"{url}/v1/auth/login",
|
||||
data=json.dumps({
|
||||
"identifier": "cookie-http@example.com",
|
||||
"password": "secret-123",
|
||||
}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
assert json.loads(r.read())["session_token"]
|
||||
cookie_header = r.headers["Set-Cookie"]
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
cookie = http.cookies.SimpleCookie(cookie_header)
|
||||
session_cookie = cookie["meshnet_session"].OutputString()
|
||||
|
||||
restarted = TrackerServer(
|
||||
billing=BillingLedger(starting_credit=0.0, default_price_per_1k=0.02),
|
||||
accounts_db=accounts_db,
|
||||
starting_credit=0.0,
|
||||
devnet_topup_amount=0.0,
|
||||
)
|
||||
restarted_port = restarted.start()
|
||||
restarted_url = f"http://127.0.0.1:{restarted_port}"
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{restarted_url}/v1/account",
|
||||
headers={"Cookie": session_cookie},
|
||||
method="GET",
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
me = json.loads(r.read())
|
||||
finally:
|
||||
restarted.stop()
|
||||
|
||||
assert me["account"]["email"] == "cookie-http@example.com"
|
||||
|
||||
|
||||
def test_bad_credentials_and_missing_session_are_401(account_tracker):
|
||||
url, _ = account_tracker
|
||||
_call(f"{url}/v1/auth/register", "POST",
|
||||
|
||||
@@ -1,138 +1,153 @@
|
||||
"""US-035: tracker web dashboard — served from any tracker, embedded asset."""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
from meshnet_tracker.accounts import AccountStore
|
||||
from meshnet_tracker.billing import BillingLedger
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
PANELS = [
|
||||
"Tracker hive", "Nodes & coverage", "Client balances",
|
||||
"Node pending payouts", "Settlement history",
|
||||
"Strikes / bans / forfeitures", "Model usage", "Call wall",
|
||||
"Usage summary", "Node throughput", "Request history",
|
||||
'id="tab-chat"',
|
||||
"Console output",
|
||||
]
|
||||
|
||||
|
||||
def test_dashboard_served_with_all_panels():
|
||||
tracker = TrackerServer(billing=BillingLedger())
|
||||
port = tracker.start()
|
||||
try:
|
||||
html = urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/dashboard"
|
||||
).read().decode()
|
||||
for panel in PANELS:
|
||||
assert panel in html
|
||||
assert "<script>" in html # polling client embedded, no build step
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_dashboard_served_by_follower():
|
||||
"""A tracker that is not the leader (unreachable peers → never elected)
|
||||
still serves the dashboard from its own replicated state."""
|
||||
tracker = TrackerServer(
|
||||
billing=BillingLedger(),
|
||||
cluster_peers=["http://127.0.0.1:1", "http://127.0.0.1:2"],
|
||||
)
|
||||
port = tracker.start()
|
||||
try:
|
||||
response = urllib.request.urlopen(f"http://127.0.0.1:{port}/dashboard")
|
||||
assert response.status == 200
|
||||
assert "meshnet tracker" in response.read().decode()
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_registry_wallets_endpoint():
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-a", 100)
|
||||
contracts.registry.record_strike("wallet-a")
|
||||
accounts = AccountStore()
|
||||
admin = accounts.register(email="admin@example.com", password="admin-pass-123")
|
||||
session = accounts.create_session(admin["account_id"])
|
||||
tracker = TrackerServer(contracts=contracts, accounts=accounts)
|
||||
port = tracker.start()
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/registry/wallets",
|
||||
headers={"Authorization": f"Bearer {session}"},
|
||||
)
|
||||
data = json.loads(urllib.request.urlopen(req).read())
|
||||
assert data["wallets"]["wallet-a"]["strike_count"] == 1
|
||||
assert data["wallets"]["wallet-a"]["banned"] is False
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_console_endpoint_exposes_tracker_events():
|
||||
tracker = TrackerServer()
|
||||
port = tracker.start()
|
||||
try:
|
||||
body = json.dumps({
|
||||
"endpoint": "http://127.0.0.1:9001",
|
||||
"model": "stub-model",
|
||||
"shard_start": 0,
|
||||
"shard_end": 3,
|
||||
"hardware_profile": {},
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
urllib.request.urlopen(req).read()
|
||||
|
||||
data = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
assert any(event["message"] == "node registered" for event in data["events"])
|
||||
|
||||
|
||||
def test_console_node_lifecycle_events_include_model_health():
|
||||
tracker = TrackerServer(heartbeat_timeout=0.05)
|
||||
port = tracker.start()
|
||||
try:
|
||||
body = json.dumps({
|
||||
"endpoint": "http://127.0.0.1:9002",
|
||||
"model": "console-health-test",
|
||||
"hf_repo": "example/console-health-test",
|
||||
"num_layers": 4,
|
||||
"shard_start": 0,
|
||||
"shard_end": 1,
|
||||
"hardware_profile": {},
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
urllib.request.urlopen(req).read()
|
||||
|
||||
registered = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||
registered_event = next(
|
||||
event for event in registered["events"]
|
||||
if event["message"] == "node registered"
|
||||
)
|
||||
assert registered_event["fields"]["model_health"]["served_model_copies"] == 0.5
|
||||
assert registered_event["fields"]["model_health"]["coverage_percentage"] == 50.0
|
||||
|
||||
time.sleep(0.06)
|
||||
urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map").read()
|
||||
expired = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||
expired_event = next(
|
||||
event for event in expired["events"]
|
||||
if event["message"] == "node expired"
|
||||
)
|
||||
assert expired_event["fields"]["model_health"]["served_model_copies"] == 0.0
|
||||
assert expired_event["fields"]["model_health"]["coverage_percentage"] == 0.0
|
||||
finally:
|
||||
tracker.stop()
|
||||
"""US-035: tracker web dashboard — served from any tracker, embedded asset."""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from meshnet_contracts import LocalSolanaContracts
|
||||
from meshnet_tracker.accounts import AccountStore
|
||||
from meshnet_tracker.billing import BillingLedger
|
||||
from meshnet_tracker.server import TrackerServer
|
||||
|
||||
PANELS = [
|
||||
"Tracker hive", "Nodes & coverage", "Client balances",
|
||||
"Node pending payouts", "Settlement history",
|
||||
"Strikes / bans / forfeitures", "Model usage", "Call wall",
|
||||
"Usage summary", "Node throughput", "Request history",
|
||||
"Chat / inference",
|
||||
"Console output",
|
||||
]
|
||||
|
||||
|
||||
def test_dashboard_served_with_all_panels():
|
||||
tracker = TrackerServer(billing=BillingLedger())
|
||||
port = tracker.start()
|
||||
try:
|
||||
html = urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/dashboard"
|
||||
).read().decode()
|
||||
for panel in PANELS:
|
||||
assert panel in html
|
||||
assert "<script>" in html # polling client embedded, no build step
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_dashboard_chat_uses_streaming_fetch():
|
||||
tracker = TrackerServer(billing=BillingLedger())
|
||||
port = tracker.start()
|
||||
try:
|
||||
html = urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/dashboard"
|
||||
).read().decode()
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
assert "stream: true" in html
|
||||
assert ".body.getReader()" in html
|
||||
assert '=== "[DONE]"' in html
|
||||
|
||||
|
||||
def test_dashboard_served_by_follower():
|
||||
"""A tracker that is not the leader (unreachable peers → never elected)
|
||||
still serves the dashboard from its own replicated state."""
|
||||
tracker = TrackerServer(
|
||||
billing=BillingLedger(),
|
||||
cluster_peers=["http://127.0.0.1:1", "http://127.0.0.1:2"],
|
||||
)
|
||||
port = tracker.start()
|
||||
try:
|
||||
response = urllib.request.urlopen(f"http://127.0.0.1:{port}/dashboard")
|
||||
assert response.status == 200
|
||||
assert "meshnet tracker" in response.read().decode()
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_registry_wallets_endpoint():
|
||||
contracts = LocalSolanaContracts()
|
||||
contracts.registry.submit_stake("wallet-a", 100)
|
||||
contracts.registry.record_strike("wallet-a")
|
||||
accounts = AccountStore()
|
||||
admin = accounts.register(email="admin@example.com", password="admin-pass-123")
|
||||
session = accounts.create_session(admin["account_id"])
|
||||
tracker = TrackerServer(contracts=contracts, accounts=accounts)
|
||||
port = tracker.start()
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/registry/wallets",
|
||||
headers={"Authorization": f"Bearer {session}"},
|
||||
)
|
||||
data = json.loads(urllib.request.urlopen(req).read())
|
||||
assert data["wallets"]["wallet-a"]["strike_count"] == 1
|
||||
assert data["wallets"]["wallet-a"]["banned"] is False
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_console_endpoint_exposes_tracker_events():
|
||||
tracker = TrackerServer()
|
||||
port = tracker.start()
|
||||
try:
|
||||
body = json.dumps({
|
||||
"endpoint": "http://127.0.0.1:9001",
|
||||
"model": "stub-model",
|
||||
"shard_start": 0,
|
||||
"shard_end": 3,
|
||||
"hardware_profile": {},
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
urllib.request.urlopen(req).read()
|
||||
|
||||
data = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
assert any(event["message"] == "node registered" for event in data["events"])
|
||||
|
||||
|
||||
def test_console_node_lifecycle_events_include_model_health():
|
||||
tracker = TrackerServer(heartbeat_timeout=0.05)
|
||||
port = tracker.start()
|
||||
try:
|
||||
body = json.dumps({
|
||||
"endpoint": "http://127.0.0.1:9002",
|
||||
"model": "console-health-test",
|
||||
"hf_repo": "example/console-health-test",
|
||||
"num_layers": 4,
|
||||
"shard_start": 0,
|
||||
"shard_end": 1,
|
||||
"hardware_profile": {},
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{port}/v1/nodes/register",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
urllib.request.urlopen(req).read()
|
||||
|
||||
registered = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||
registered_event = next(
|
||||
event for event in registered["events"]
|
||||
if event["message"] == "node registered"
|
||||
)
|
||||
assert registered_event["fields"]["model_health"]["served_model_copies"] == 0.5
|
||||
assert registered_event["fields"]["model_health"]["coverage_percentage"] == 50.0
|
||||
|
||||
time.sleep(0.06)
|
||||
urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map").read()
|
||||
expired = json.loads(urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/console").read())
|
||||
expired_event = next(
|
||||
event for event in expired["events"]
|
||||
if event["message"] == "node expired"
|
||||
)
|
||||
assert expired_event["fields"]["model_health"]["served_model_copies"] == 0.0
|
||||
assert expired_event["fields"]["model_health"]["coverage_percentage"] == 0.0
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
@@ -142,6 +142,21 @@ def _send_chat_request(gateway_url: str, prompt: str) -> dict:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _send_streaming_chat_request(gateway_url: str, prompt: str):
|
||||
data = json.dumps({
|
||||
"model": GPT2_MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": True,
|
||||
}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{gateway_url}/v1/chat/completions",
|
||||
data=data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
return urllib.request.urlopen(req)
|
||||
|
||||
|
||||
def test_all_responses_valid_openai_format(tracker_node_setup):
|
||||
"""Ten requests via gateway all return valid OpenAI chat completion format."""
|
||||
gateway_url, _, _ = tracker_node_setup
|
||||
@@ -155,6 +170,30 @@ def test_all_responses_valid_openai_format(tracker_node_setup):
|
||||
assert isinstance(message.get("content"), str), f"request {i}: content must be a string"
|
||||
|
||||
|
||||
def test_streaming_head_worker_response_is_not_buffered_with_content_length(tracker_node_setup):
|
||||
"""Gateway must relay head-worker SSE as a live stream, not a buffered JSON-sized body."""
|
||||
gateway_url, _, _ = tracker_node_setup
|
||||
|
||||
with _send_streaming_chat_request(gateway_url, "stream through head worker") as resp:
|
||||
assert resp.status == 200
|
||||
assert "text/event-stream" in resp.headers["Content-Type"]
|
||||
assert "Content-Length" not in resp.headers
|
||||
data_lines = []
|
||||
while len(data_lines) < 4:
|
||||
line = resp.readline().decode().strip()
|
||||
if line.startswith("data: "):
|
||||
data_lines.append(line)
|
||||
if line == "data: [DONE]":
|
||||
break
|
||||
|
||||
assert data_lines[-1] == "data: [DONE]"
|
||||
content = "".join(
|
||||
json.loads(line[6:])["choices"][0].get("delta", {}).get("content", "")
|
||||
for line in data_lines[:-1]
|
||||
)
|
||||
assert "head-worker" in content
|
||||
|
||||
|
||||
def test_both_tracker_nodes_receive_load(tracker_node_setup):
|
||||
"""Both head workers handle at least one request each out of ten."""
|
||||
gateway_url, tracker_node_a, tracker_node_b = tracker_node_setup
|
||||
|
||||
67
tests/test_tracker_logging.py
Normal file
67
tests/test_tracker_logging.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from meshnet_tracker.logging_setup import configure_tracker_file_logging, tracker_logger
|
||||
|
||||
|
||||
def test_tracker_file_logging_writes_separate_level_files(tmp_path):
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
try:
|
||||
log_dir = configure_tracker_file_logging(tmp_path, tee_stdio=False)
|
||||
logger = tracker_logger()
|
||||
|
||||
logger.info("info-event")
|
||||
logger.warning("warning-event")
|
||||
logger.error("error-event")
|
||||
for handler in logger.handlers:
|
||||
handler.flush()
|
||||
|
||||
assert (log_dir / "info.log").read_text().count("info-event") == 1
|
||||
assert "warning-event" not in (log_dir / "info.log").read_text()
|
||||
assert "error-event" not in (log_dir / "info.log").read_text()
|
||||
|
||||
assert "warning-event" in (log_dir / "warning.log").read_text()
|
||||
assert "info-event" not in (log_dir / "warning.log").read_text()
|
||||
assert "error-event" not in (log_dir / "warning.log").read_text()
|
||||
|
||||
assert "error-event" in (log_dir / "error.log").read_text()
|
||||
assert "info-event" not in (log_dir / "error.log").read_text()
|
||||
assert "warning-event" not in (log_dir / "error.log").read_text()
|
||||
finally:
|
||||
sys.stdout = original_stdout
|
||||
sys.stderr = original_stderr
|
||||
|
||||
|
||||
def test_tracker_file_logging_tees_stdio_and_rotates(tmp_path):
|
||||
original_stdout = sys.stdout
|
||||
original_stderr = sys.stderr
|
||||
try:
|
||||
log_dir = configure_tracker_file_logging(
|
||||
tmp_path,
|
||||
max_bytes=120,
|
||||
backup_count=1,
|
||||
)
|
||||
|
||||
print("stdout goes to info", flush=True)
|
||||
print("stderr goes to error", file=sys.stderr, flush=True)
|
||||
for handler in tracker_logger().handlers:
|
||||
handler.flush()
|
||||
|
||||
assert "stdout goes to info" in (log_dir / "info.log").read_text()
|
||||
assert "stderr goes to error" in (log_dir / "error.log").read_text()
|
||||
|
||||
for index in range(12):
|
||||
tracker_logger().info("rotating-info-line-%02d", index)
|
||||
for handler in tracker_logger().handlers:
|
||||
handler.flush()
|
||||
|
||||
assert (log_dir / "info.log.1").exists()
|
||||
finally:
|
||||
sys.stdout = original_stdout
|
||||
sys.stderr = original_stderr
|
||||
logger = tracker_logger()
|
||||
for handler in logger.handlers:
|
||||
handler.close()
|
||||
logger.handlers.clear()
|
||||
logger.setLevel(logging.NOTSET)
|
||||
@@ -502,6 +502,75 @@ def test_tracker_logs_stream_progress_before_request_completes():
|
||||
node_thread.join(timeout=1.0)
|
||||
|
||||
|
||||
def test_tracker_stream_survives_idle_gap_between_sse_chunks():
|
||||
first_chunk_sent = threading.Event()
|
||||
|
||||
class IdleStreamingChatHandler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def do_POST(self):
|
||||
if self.path != "/v1/chat/completions":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
self.rfile.read(int(self.headers.get("Content-Length", 0)))
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
self.end_headers()
|
||||
first = json.dumps({
|
||||
"choices": [{"delta": {"content": "hello"}}],
|
||||
}).encode()
|
||||
second = json.dumps({
|
||||
"choices": [{"delta": {"content": " world"}}],
|
||||
}).encode()
|
||||
self.wfile.write(b"data: " + first + b"\n\n")
|
||||
self.wfile.flush()
|
||||
first_chunk_sent.set()
|
||||
time.sleep(1.0)
|
||||
self.wfile.write(b"data: " + second + b"\n\n")
|
||||
self.wfile.write(b"data: [DONE]\n\n")
|
||||
self.wfile.flush()
|
||||
|
||||
node = http.server.HTTPServer(("127.0.0.1", 0), IdleStreamingChatHandler)
|
||||
node_thread = threading.Thread(target=node.serve_forever, daemon=True)
|
||||
node_thread.start()
|
||||
tracker = TrackerServer(heartbeat_timeout=60.0)
|
||||
tracker_port = tracker.start()
|
||||
response = None
|
||||
try:
|
||||
_post_json(
|
||||
f"http://127.0.0.1:{tracker_port}/v1/nodes/register",
|
||||
{"endpoint": f"http://127.0.0.1:{node.server_address[1]}",
|
||||
"model": "idle-stream-model", "num_layers": 1,
|
||||
"shard_start": 0, "shard_end": 0,
|
||||
"hardware_profile": {}, "score": 1.0},
|
||||
)
|
||||
req = urllib.request.Request(
|
||||
f"http://127.0.0.1:{tracker_port}/v1/chat/completions",
|
||||
data=json.dumps({
|
||||
"model": "idle-stream-model",
|
||||
"stream": True,
|
||||
"messages": [{"role": "user", "content": "hi"}],
|
||||
}).encode(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
response = urllib.request.urlopen(req, timeout=3.0)
|
||||
assert response.readline().startswith(b"data:")
|
||||
assert first_chunk_sent.wait(timeout=1.0)
|
||||
|
||||
remaining = response.read().splitlines()
|
||||
assert b"data: [DONE]" in remaining
|
||||
finally:
|
||||
if response is not None:
|
||||
response.close()
|
||||
tracker.stop()
|
||||
node.shutdown()
|
||||
node.server_close()
|
||||
node_thread.join(timeout=1.0)
|
||||
|
||||
|
||||
def test_tracker_dashboard_can_cancel_inflight_proxy():
|
||||
chunk_sent = threading.Event()
|
||||
release = threading.Event()
|
||||
|
||||
Reference in New Issue
Block a user