384 lines
14 KiB
Python
384 lines
14 KiB
Python
"""US-035: tracker web dashboard — served from any tracker, embedded asset."""
|
|
|
|
import http.client
|
|
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",
|
|
"Test run status", "Tests & suites", "Test 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 '<link rel="icon" type="image/svg+xml" href="/favicon.svg">' in html
|
|
favicon = urllib.request.urlopen(f"http://127.0.0.1:{port}/favicon.svg").read()
|
|
assert favicon.startswith(b"<svg")
|
|
assert b"meshnet" in favicon
|
|
assert "<script>" in html # polling client embedded, no build step
|
|
assert "resolveModelGroup" in html
|
|
assert "buildModelAliasMap" in html
|
|
assert "modelAliasKey(raw)" in html
|
|
finally:
|
|
tracker.stop()
|
|
|
|
|
|
def test_tracker_root_redirects_to_dashboard():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
connection = http.client.HTTPConnection("127.0.0.1", port)
|
|
connection.request("GET", "/")
|
|
response = connection.getresponse()
|
|
location = response.getheader("Location")
|
|
response.read()
|
|
connection.close()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert response.status == 302
|
|
assert location == "/dashboard"
|
|
|
|
|
|
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
|
|
assert 'console.error("chat stream failed", err)' in html
|
|
assert "preloadChatModels" in html
|
|
assert "renderChatModels(true)" in html
|
|
|
|
|
|
def test_dashboard_allows_admin_to_request_selected_model_load():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
html = urllib.request.urlopen(
|
|
f"http://127.0.0.1:{port}/dashboard"
|
|
).read().decode()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert 'id="request-model-load"' in html
|
|
assert "requestSelectedModelLoad" in html
|
|
assert '"/v1/models/load"' in html
|
|
assert '$("request-model-load").style.display = enabled ? "" : "none"' in html
|
|
|
|
|
|
def test_network_map_includes_node_friendly_name():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
body = json.dumps({
|
|
"endpoint": "http://127.0.0.1:9010",
|
|
"model": "stub-model",
|
|
"shard_start": 0,
|
|
"shard_end": 3,
|
|
"hardware_profile": {},
|
|
"friendly_name": "Kitchen GPU",
|
|
}).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()
|
|
network = json.loads(
|
|
urllib.request.urlopen(f"http://127.0.0.1:{port}/v1/network/map").read()
|
|
)
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert network["nodes"][0]["friendly_name"] == "Kitchen GPU"
|
|
|
|
|
|
def test_dashboard_chat_model_selector_shows_health_and_speed():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
html = urllib.request.urlopen(
|
|
f"http://127.0.0.1:{port}/dashboard"
|
|
).read().decode()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert "chatModelHealthHp" in html
|
|
assert "modelServedCopiesFromMap" in html
|
|
assert "nodeDisplayName" in html
|
|
assert "accountDisplayName" in html
|
|
assert "saveNickname" in html
|
|
assert "chatModelTypicalTps" in html
|
|
assert "chatStreamStatsText" in html
|
|
assert "chat-stream-stats" in html
|
|
assert "chatMessageRowHtml" in html
|
|
assert "chatModelOptionLabel" in html
|
|
assert "findRoutingForModel" in html
|
|
assert "tok/s" in html
|
|
assert "toFixed(2)}HP" in html or '${copies(v)}HP' in html
|
|
|
|
|
|
def test_dashboard_chat_sessions_use_delegated_handlers():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
html = urllib.request.urlopen(
|
|
f"http://127.0.0.1:{port}/dashboard"
|
|
).read().decode()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert "bindChatSessionList" in html
|
|
assert "dataset.sessionId" in html
|
|
assert "dataset.deleteSession" in html
|
|
assert '[data-session-id]' in html
|
|
assert 'onclick="selectChatSession(' not in html
|
|
assert 'onclick="deleteChatSession(' not in html
|
|
|
|
|
|
def test_dashboard_incremental_refresh_helpers():
|
|
tracker = TrackerServer()
|
|
port = tracker.start()
|
|
try:
|
|
html = urllib.request.urlopen(
|
|
f"http://127.0.0.1:{port}/dashboard"
|
|
).read().decode()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
assert "renderIfChanged" in html
|
|
assert "syncKeyedList" in html
|
|
assert "refreshBlocked" in html
|
|
assert "patchAccountPanelView" in html
|
|
assert "buildAccountPanelShell" in html
|
|
assert "refreshActiveTab" in html
|
|
assert "TAB_FETCHERS" in html
|
|
assert "loadAccountSummary" in html
|
|
assert "loadAccountUsage" in html
|
|
assert "fetchOverviewTab" in html
|
|
assert "pollCallWallIfIdle" in html
|
|
assert "pendingChatModelRefresh" in html
|
|
assert 'renderChatHistory(true)' in html
|
|
assert "renderChatHistory();" not in html
|
|
assert "refreshIfIdle" not in html
|
|
assert "refreshAccountIfIdle" not in html
|
|
assert "setInterval(refreshIfIdle" not 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()
|
|
|
|
|
|
def _dashboard_html(**kwargs) -> str:
|
|
tracker = TrackerServer(**kwargs)
|
|
port = tracker.start()
|
|
try:
|
|
return urllib.request.urlopen(
|
|
f"http://127.0.0.1:{port}/dashboard"
|
|
).read().decode()
|
|
finally:
|
|
tracker.stop()
|
|
|
|
|
|
def test_dashboard_testing_tab_is_admin_only():
|
|
"""US-002: the Testing tab ships hidden and is only revealed for admins."""
|
|
html = _dashboard_html()
|
|
|
|
# Tab button exists but is hidden until setAdminMode(true) reveals it.
|
|
assert '<button id="tab-testing" style="display:none"' in html
|
|
assert "switchDashboardTab('testing')" in html
|
|
assert '$("tab-testing").style.display = enabled ? "" : "none"' in html
|
|
|
|
# Every Testing panel is data-admin-only, so updateSectionVisibility()
|
|
# (adminOnly && !isAdmin -> hidden) keeps them hidden for non-admins.
|
|
for panel in ("testing-status", "testing-targets", "testing-log"):
|
|
assert f'id="{panel}"' in html
|
|
assert html.count('data-tab="testing" data-admin-only') == 3
|
|
|
|
# Selecting the tab without admin rights falls back to overview, and losing
|
|
# admin rights while on it kicks the user out.
|
|
assert 'if ((name === "admin" || name === "testing") && !isAdmin) name = "overview";' in html
|
|
assert 'if (!enabled && (dashboardTab === "admin" || dashboardTab === "testing"))' in html
|
|
|
|
|
|
def test_dashboard_testing_tab_uses_dynamic_api_and_run_controls():
|
|
"""US-002: targets come from the API, never a hardcoded list."""
|
|
html = _dashboard_html()
|
|
|
|
# Dynamic collection + run + status endpoints from US-001.
|
|
assert 'apiCall(`/v1/tests${refresh ? "?refresh=1" : ""}`)' in html
|
|
assert 'apiCall("/v1/tests/run", "POST", { target })' in html
|
|
assert 'apiCall("/v1/tests/status")' in html
|
|
assert "fetchTestingTab" in html
|
|
assert "testing: fetchTestingTab" in html # registered in TAB_FETCHERS
|
|
|
|
# Targets are rendered from the API payload (tests + suites), not literals.
|
|
assert "testCollection.suites" in html
|
|
assert "testCollection.tests" in html
|
|
assert "renderTestTargets" in html
|
|
|
|
# Run buttons are per-target and delegated; disabled while a run is active.
|
|
assert 'data-test-target="${esc(t.id)}"' in html
|
|
assert "const disabled = testRunActive() ? \" disabled\" : \"\";" in html
|
|
assert "if (testRunActive()) return;" in html # runTest() guards re-entry
|
|
assert 'button = event.target.closest("[data-test-target]")' in html
|
|
|
|
# API errors are surfaced rather than swallowed.
|
|
assert "showTestingError" in html
|
|
assert "testingErrorText" in html
|
|
assert 'id="testing-error"' in html
|
|
|
|
|
|
def test_dashboard_testing_tab_renders_status_and_bounded_log():
|
|
"""US-002: state, timings, exit code, outcome and an auto-refreshing log."""
|
|
html = _dashboard_html()
|
|
|
|
assert "renderTestRunStatus" in html
|
|
assert "renderTestLog" in html
|
|
for field in ("target", "state", "outcome", "started", "ended", "elapsed", "exit code"):
|
|
assert f'"{field}"' in html
|
|
assert "testRun.exit_code" in html
|
|
assert "testRun.started_at" in html
|
|
assert "testRun.finished_at" in html
|
|
assert "testRun.elapsed_seconds" in html
|
|
assert "fmtElapsed" in html
|
|
|
|
# Success/failure is derived from the run status, not guessed from the log.
|
|
assert '<span class="ok">success</span>' in html
|
|
assert '<span class="bad">failure</span>' in html
|
|
|
|
# Bounded log view + auto-refresh while the run is in flight.
|
|
assert "TEST_LOG_MAX_LINES" in html
|
|
assert "lines.slice(-TEST_LOG_MAX_LINES)" in html
|
|
assert "pollTestRunIfActive" in html
|
|
assert "setInterval(pollTestRunIfActive, TEST_RUN_POLL_MS)" in html
|