[verified] feat: complete Ralph task workstreams
This commit is contained in:
525
tests/test_tracker_capability_admission.py
Normal file
525
tests/test_tracker_capability_admission.py
Normal file
@@ -0,0 +1,525 @@
|
||||
"""NCA-004 tests: the tracker records a node's capability proof and routes only
|
||||
to nodes whose proof admits what they advertise (ADR-0023).
|
||||
|
||||
Model ids here are arbitrary and made up on purpose: nothing in the admission or
|
||||
routing path may branch on a vendor, model or kernel name.
|
||||
"""
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import pytest
|
||||
|
||||
from meshnet_tracker.capability import (
|
||||
MIN_CATALOGUE_VERSION,
|
||||
POLICY_COMPAT,
|
||||
POLICY_ENFORCE,
|
||||
STATE_ABSENT,
|
||||
STATE_ADMITTED,
|
||||
STATE_CATALOGUE_INCOMPATIBLE,
|
||||
STATE_FAILED,
|
||||
STATE_INVALID,
|
||||
STATE_MODEL_MISMATCH,
|
||||
STATE_RECIPE_MISMATCH,
|
||||
STATE_SHARD_MISMATCH,
|
||||
STATE_STALE,
|
||||
evaluate_report,
|
||||
policy_from_env,
|
||||
)
|
||||
from meshnet_tracker.server import (
|
||||
TrackerServer,
|
||||
_NodeEntry,
|
||||
_capability_routable,
|
||||
_node_admission,
|
||||
_select_route,
|
||||
)
|
||||
|
||||
MODEL = "arbitrary-labs/oracle-9b"
|
||||
SHORT = "oracle-9b"
|
||||
LAYERS = 32
|
||||
|
||||
|
||||
def _post_json(url: str, payload: dict) -> dict:
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
url, data=data, headers={"Content-Type": "application/json"}, method="POST"
|
||||
)
|
||||
with urllib.request.urlopen(req) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _get_json(url: str) -> dict:
|
||||
with urllib.request.urlopen(url) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def _report(
|
||||
*,
|
||||
model_id: str = MODEL,
|
||||
start: int = 0,
|
||||
end: int = 15,
|
||||
status: str = "passed",
|
||||
validated_at: float | None = None,
|
||||
recipe_id: str = "baseline",
|
||||
recipe_version: str = "1",
|
||||
catalogue_version: str = MIN_CATALOGUE_VERSION,
|
||||
schema_version: int = 1,
|
||||
device: str = "cpu",
|
||||
diagnostics: list | None = None,
|
||||
) -> dict:
|
||||
"""A capability report shaped exactly as `meshnet_node.capability` emits it."""
|
||||
return {
|
||||
"schema_version": schema_version,
|
||||
"model": {"model_id": model_id, "revision": None, "config_fingerprint": None},
|
||||
"shard": {"start": start, "end": end},
|
||||
"recipe": {
|
||||
"recipe_id": recipe_id,
|
||||
"recipe_version": recipe_version,
|
||||
"catalogue_version": catalogue_version,
|
||||
},
|
||||
"backend": {
|
||||
"backend_id": "torch-transformers",
|
||||
"device": device,
|
||||
"device_name": None,
|
||||
"quantization": "bfloat16",
|
||||
"runtime": {},
|
||||
},
|
||||
"status": status,
|
||||
"validated_at": time.time() if validated_at is None else validated_at,
|
||||
"duration_ms": 42,
|
||||
"diagnostics": list(diagnostics or []),
|
||||
}
|
||||
|
||||
|
||||
def _registration(
|
||||
port: int,
|
||||
*,
|
||||
start: int = 0,
|
||||
end: int = 15,
|
||||
report: dict | None = "default", # type: ignore[assignment]
|
||||
recipe_id: str | None = "baseline",
|
||||
recipe_version: str | None = "1",
|
||||
benchmark_tokens_per_sec: float = 10.0,
|
||||
) -> dict:
|
||||
payload: dict = {
|
||||
"endpoint": f"http://127.0.0.1:{port}",
|
||||
"model": SHORT,
|
||||
"hf_repo": MODEL,
|
||||
"num_layers": LAYERS,
|
||||
"shard_start": start,
|
||||
"shard_end": end,
|
||||
"hardware_profile": {},
|
||||
"score": 1.0,
|
||||
"tracker_mode": start == 0,
|
||||
"benchmark_tokens_per_sec": benchmark_tokens_per_sec,
|
||||
}
|
||||
if report == "default":
|
||||
report = _report(start=start, end=end)
|
||||
if report is not None:
|
||||
payload["capability_report"] = report
|
||||
if recipe_id is not None:
|
||||
payload["recipe_id"] = recipe_id
|
||||
if recipe_version is not None:
|
||||
payload["recipe_version"] = recipe_version
|
||||
return payload
|
||||
|
||||
|
||||
def _always(_reported: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _evaluate(report, **kwargs) -> object:
|
||||
kwargs.setdefault("model_matches", _always)
|
||||
kwargs.setdefault("advertised_model", MODEL)
|
||||
kwargs.setdefault("shard_start", 0)
|
||||
kwargs.setdefault("shard_end", 15)
|
||||
return evaluate_report(report, **kwargs)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- report verdicts
|
||||
|
||||
|
||||
def test_a_passing_report_that_covers_the_registration_is_admitted():
|
||||
"A proof matching the advertised model and shard admits the node.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report())
|
||||
assert state.state == STATE_ADMITTED
|
||||
assert state.proven
|
||||
assert state.model_id == MODEL
|
||||
assert (state.shard_start, state.shard_end) == (0, 15)
|
||||
|
||||
|
||||
def test_a_missing_report_is_absent_not_admitted():
|
||||
"No proof is recorded as `absent` — never silently treated as proven.\n\nTags: routing, tracker"
|
||||
state = _evaluate(None)
|
||||
assert state.state == STATE_ABSENT
|
||||
assert not state.proven
|
||||
|
||||
|
||||
def test_a_failed_report_is_recorded_as_failed():
|
||||
"A node that failed its own validation is not routable.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report(status="failed", diagnostics=["out of memory on device"]))
|
||||
assert state.state == STATE_FAILED
|
||||
assert not state.proven
|
||||
assert "out of memory" in state.detail
|
||||
|
||||
|
||||
def test_a_report_for_a_different_model_is_a_model_mismatch():
|
||||
"A proof for another artifact proves nothing about this one.\n\nTags: routing, tracker"
|
||||
state = evaluate_report(
|
||||
_report(model_id="other-org/unrelated-3b"),
|
||||
model_matches=lambda reported: reported == MODEL,
|
||||
advertised_model=MODEL,
|
||||
shard_start=0,
|
||||
shard_end=15,
|
||||
)
|
||||
assert state.state == STATE_MODEL_MISMATCH
|
||||
|
||||
|
||||
def test_a_report_for_a_different_shard_is_a_shard_mismatch():
|
||||
"A proof for layers 0–15 does not admit a node advertising 16–31.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report(start=0, end=15), shard_start=16, shard_end=31)
|
||||
assert state.state == STATE_SHARD_MISMATCH
|
||||
|
||||
|
||||
def test_a_report_for_a_different_recipe_than_the_node_declares_is_a_recipe_mismatch():
|
||||
"The proof must be for the recipe the node says it serves with.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report(recipe_id="eager-attention"), declared_recipe_id="baseline")
|
||||
assert state.state == STATE_RECIPE_MISMATCH
|
||||
|
||||
versioned = _evaluate(
|
||||
_report(recipe_version="1"),
|
||||
declared_recipe_id="baseline",
|
||||
declared_recipe_version="2",
|
||||
)
|
||||
assert versioned.state == STATE_RECIPE_MISMATCH
|
||||
|
||||
|
||||
def test_an_older_recipe_catalogue_is_incompatible():
|
||||
"Recipe ids from a catalogue older than the tracker's minimum cannot be matched.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report(catalogue_version="2025.01.1"))
|
||||
assert state.state == STATE_CATALOGUE_INCOMPATIBLE
|
||||
assert MIN_CATALOGUE_VERSION in state.detail
|
||||
|
||||
newer = _evaluate(_report(catalogue_version="2099.12.9"))
|
||||
assert newer.state == STATE_ADMITTED
|
||||
|
||||
|
||||
def test_an_unparseable_catalogue_version_is_incompatible():
|
||||
"A catalogue version that cannot be compared cannot be shown to be new enough.\n\nTags: routing, tracker"
|
||||
assert _evaluate(_report(catalogue_version="rolling")).state == STATE_CATALOGUE_INCOMPATIBLE
|
||||
|
||||
|
||||
def test_a_stale_report_is_not_admitted():
|
||||
"A proof older than the freshness bound must be re-validated before routing.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report(validated_at=time.time() - 3600), max_age_seconds=900.0)
|
||||
assert state.state == STATE_STALE
|
||||
|
||||
|
||||
def test_a_future_dated_report_is_not_admitted():
|
||||
"A proof from the future is a broken clock, not a fresh proof.\n\nTags: routing, tracker"
|
||||
state = _evaluate(_report(validated_at=time.time() + 3600))
|
||||
assert state.state == STATE_STALE
|
||||
assert "clock" in state.detail
|
||||
|
||||
|
||||
def test_a_report_from_an_unknown_schema_version_is_invalid():
|
||||
"The tracker refuses to interpret a report layout it does not read.\n\nTags: routing, tracker"
|
||||
assert _evaluate(_report(schema_version=99)).state == STATE_INVALID
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"payload",
|
||||
[
|
||||
"not-an-object",
|
||||
{},
|
||||
{"schema_version": 1},
|
||||
{**_report(), "model": {"model_id": ""}},
|
||||
{**_report(), "shard": {"start": -1, "end": 3}},
|
||||
{**_report(), "validated_at": "yesterday"},
|
||||
{**_report(), "status": None},
|
||||
],
|
||||
ids=["not-object", "empty", "header-only", "blank-model", "negative-shard",
|
||||
"bad-timestamp", "missing-status"],
|
||||
)
|
||||
def test_a_malformed_report_is_invalid_and_never_admitted(payload):
|
||||
"Malformed proof is rejected by the schema check, not by a later coincidence.\n\nTags: routing, tracker"
|
||||
state = _evaluate(payload)
|
||||
assert state.state == STATE_INVALID
|
||||
assert not state.proven
|
||||
|
||||
|
||||
def test_recorded_detail_carries_no_credentials_from_node_diagnostics():
|
||||
"Operator-facing admission detail is sanitized; a leaked token never reaches it.\n\nTags: routing, security, tracker"
|
||||
state = _evaluate(
|
||||
_report(
|
||||
status="failed",
|
||||
diagnostics=["download failed: authorization=hf_abcdefghijklmnopqrstuvwxyz01"],
|
||||
)
|
||||
)
|
||||
assert state.state == STATE_FAILED
|
||||
assert "hf_abcdefghijklmnopqrstuvwxyz01" not in state.detail
|
||||
assert "hf_abcdefghijklmnopqrstuvwxyz01" not in json.dumps(state.to_dict())
|
||||
assert "[redacted]" in state.detail
|
||||
|
||||
|
||||
# ---------------------------------------------------------- compatibility policy
|
||||
|
||||
|
||||
def test_compat_policy_routes_a_legacy_node_but_never_a_broken_proof():
|
||||
"Older nodes (no proof) keep routing under `compat`; a bad proof never does.\n\nTags: routing, tracker"
|
||||
absent = _evaluate(None)
|
||||
failed = _evaluate(_report(status="failed"))
|
||||
|
||||
assert absent.routable_under(POLICY_COMPAT)
|
||||
assert not absent.routable_under(POLICY_ENFORCE)
|
||||
assert not failed.routable_under(POLICY_COMPAT)
|
||||
assert not failed.routable_under(POLICY_ENFORCE)
|
||||
|
||||
|
||||
def test_the_policy_is_read_from_the_environment_and_defaults_to_compat(monkeypatch):
|
||||
"The rollout switch is explicit and defaults to the compatible behaviour.\n\nTags: config, routing, tracker"
|
||||
monkeypatch.delenv("MESHNET_TRACKER_CAPABILITY_POLICY", raising=False)
|
||||
assert policy_from_env() == POLICY_COMPAT
|
||||
monkeypatch.setenv("MESHNET_TRACKER_CAPABILITY_POLICY", "enforce")
|
||||
assert policy_from_env() == POLICY_ENFORCE
|
||||
monkeypatch.setenv("MESHNET_TRACKER_CAPABILITY_POLICY", "nonsense")
|
||||
assert policy_from_env() == POLICY_COMPAT
|
||||
|
||||
|
||||
# ------------------------------------------------------------- the routing gate
|
||||
|
||||
|
||||
def _entry(node_id: str, start: int, end: int, report: dict | None, **kwargs) -> _NodeEntry:
|
||||
from meshnet_tracker.capability import evaluate_report as _eval
|
||||
|
||||
entry = _NodeEntry(
|
||||
node_id=node_id,
|
||||
endpoint=f"http://127.0.0.1:{9000 + int(node_id[-1])}",
|
||||
shard_start=start,
|
||||
shard_end=end,
|
||||
model=SHORT,
|
||||
shard_checksum=None,
|
||||
hardware_profile={},
|
||||
wallet_address=None,
|
||||
score=1.0,
|
||||
hf_repo=MODEL,
|
||||
num_layers=LAYERS,
|
||||
capability=_eval(
|
||||
report,
|
||||
model_matches=lambda reported: reported == MODEL,
|
||||
advertised_model=MODEL,
|
||||
shard_start=start,
|
||||
shard_end=end,
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
return entry
|
||||
|
||||
|
||||
def test_route_selection_drops_every_unadmitted_candidate_under_enforce():
|
||||
"Absent, failed, stale and mismatched candidates are all excluded.\n\nTags: routing, tracker"
|
||||
good = _entry("node-1", 0, 31, _report(start=0, end=31))
|
||||
unproven = _entry("node-2", 0, 31, None)
|
||||
failed = _entry("node-3", 0, 31, _report(start=0, end=31, status="failed"))
|
||||
stale = _entry("node-4", 0, 31, _report(start=0, end=31, validated_at=time.time() - 86400))
|
||||
wrong_model = _entry("node-5", 0, 31, _report(start=0, end=31, model_id="someone/else-1b"))
|
||||
|
||||
route, error = _select_route(
|
||||
[unproven, failed, stale, wrong_model, good], 0, 31, policy=POLICY_ENFORCE
|
||||
)
|
||||
assert not error
|
||||
assert [n.node_id for n in route] == ["node-1"]
|
||||
|
||||
only_bad, error = _select_route([unproven, failed, stale], 0, 31, policy=POLICY_ENFORCE)
|
||||
assert only_bad == []
|
||||
assert "no route available" in error
|
||||
|
||||
|
||||
def test_a_node_reassigned_to_a_shard_it_never_proved_stops_routing():
|
||||
"The proof does not travel with a tracker reassignment.\n\nTags: routing, tracker"
|
||||
node = _entry("node-1", 0, 15, _report(start=0, end=15))
|
||||
assert _node_admission(node).state == STATE_ADMITTED
|
||||
|
||||
node.shard_start, node.shard_end = 16, 31 # tracker rebalanced it
|
||||
assert _node_admission(node).state == STATE_SHARD_MISMATCH
|
||||
assert not _node_admission(node).routable_under(POLICY_COMPAT)
|
||||
|
||||
|
||||
def test_admitted_candidates_keep_coverage_first_and_throughput_routing():
|
||||
"Gating removes candidates; among the survivors routing is unchanged.\n\nTags: routing, tracker"
|
||||
head = _entry("node-1", 0, 15, _report(start=0, end=15))
|
||||
slow_tail = _entry(
|
||||
"node-2", 16, 31, _report(start=16, end=31), benchmark_tokens_per_sec=5.0
|
||||
)
|
||||
fast_tail = _entry(
|
||||
"node-3", 16, 31, _report(start=16, end=31), benchmark_tokens_per_sec=50.0
|
||||
)
|
||||
|
||||
route, error = _select_route(
|
||||
[head, slow_tail, fast_tail], 0, 31, policy=POLICY_ENFORCE
|
||||
)
|
||||
assert not error
|
||||
# Coverage first (head, then a tail), and the faster of the two tied tails.
|
||||
assert [n.node_id for n in route] == ["node-1", "node-3"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- over the wire
|
||||
|
||||
|
||||
def test_an_enforcing_tracker_routes_a_proven_node_and_excludes_an_unproven_one():
|
||||
"End to end: a proof is required to appear in a route.\n\nTags: http, routing, tracker"
|
||||
tracker = TrackerServer(capability_policy=POLICY_ENFORCE)
|
||||
port = tracker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
_post_json(f"{base}/v1/nodes/register", _registration(9101, start=0, end=15))
|
||||
# A tail that presents no proof at all: the route cannot complete.
|
||||
_post_json(
|
||||
f"{base}/v1/nodes/register",
|
||||
_registration(9102, start=16, end=31, report=None),
|
||||
)
|
||||
|
||||
with pytest.raises(urllib.error.HTTPError) as exc:
|
||||
_get_json(f"{base}/v1/route?model={SHORT}")
|
||||
assert exc.value.code == 503
|
||||
|
||||
# Now the tail proves itself and the same route resolves.
|
||||
_post_json(f"{base}/v1/nodes/register", _registration(9102, start=16, end=31))
|
||||
route = _get_json(f"{base}/v1/route?model={SHORT}")["route"]
|
||||
assert route == ["http://127.0.0.1:9101", "http://127.0.0.1:9102"]
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"bad_report",
|
||||
[
|
||||
_report(start=16, end=31), # proves the wrong shard
|
||||
_report(model_id="unrelated/other-7b"), # proves the wrong model
|
||||
_report(status="failed"),
|
||||
_report(validated_at=time.time() - 86400), # stale
|
||||
{"schema_version": 1, "model": {}}, # malformed
|
||||
],
|
||||
ids=["shard-mismatch", "model-mismatch", "failed", "stale", "invalid"],
|
||||
)
|
||||
def test_an_enforcing_tracker_never_routes_a_node_whose_proof_does_not_cover_it(bad_report):
|
||||
"A proof for something else is worth exactly as much as no proof.\n\nTags: http, routing, tracker"
|
||||
tracker = TrackerServer(capability_policy=POLICY_ENFORCE)
|
||||
port = tracker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
resp = _post_json(
|
||||
f"{base}/v1/nodes/register",
|
||||
_registration(9111, start=0, end=31, report=bad_report),
|
||||
)
|
||||
# Registration still succeeds — the operator must be able to see the node.
|
||||
assert resp["node_id"]
|
||||
assert resp["capability"]["routable"] is False
|
||||
assert resp["capability"]["state"] != STATE_ADMITTED
|
||||
|
||||
with pytest.raises(urllib.error.HTTPError) as exc:
|
||||
_get_json(f"{base}/v1/route?model={SHORT}")
|
||||
assert exc.value.code in (404, 503)
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_a_compat_tracker_routes_a_legacy_node_that_sends_no_report():
|
||||
"Documented rollout policy: pre-capability nodes keep working under `compat`.\n\nTags: http, routing, tracker"
|
||||
tracker = TrackerServer(capability_policy=POLICY_COMPAT)
|
||||
port = tracker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
_post_json(
|
||||
f"{base}/v1/nodes/register",
|
||||
_registration(9121, start=0, end=31, report=None, recipe_id=None, recipe_version=None),
|
||||
)
|
||||
route = _get_json(f"{base}/v1/route?model={SHORT}")["route"]
|
||||
assert route == ["http://127.0.0.1:9121"]
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_a_compat_tracker_still_refuses_a_node_that_presents_a_failed_proof():
|
||||
"Compatibility grandfathers silence, not a proof of failure.\n\nTags: http, routing, tracker"
|
||||
tracker = TrackerServer(capability_policy=POLICY_COMPAT)
|
||||
port = tracker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
_post_json(
|
||||
f"{base}/v1/nodes/register",
|
||||
_registration(9131, start=0, end=31, report=_report(start=0, end=31, status="failed")),
|
||||
)
|
||||
with pytest.raises(urllib.error.HTTPError) as exc:
|
||||
_get_json(f"{base}/v1/route?model={SHORT}")
|
||||
assert exc.value.code == 503
|
||||
finally:
|
||||
tracker.stop()
|
||||
|
||||
|
||||
def test_a_replicated_registration_carries_its_verdict_to_a_follower():
|
||||
"A proven node must not be routable on the leader and dark on every follower.\n\nTags: cluster, routing, tracker"
|
||||
tracker = TrackerServer(capability_policy=POLICY_ENFORCE)
|
||||
proven = _registration(9151, start=0, end=31, report=_report(start=0, end=31))
|
||||
proven["node_id"] = "follower-node-1"
|
||||
unproven = _registration(9152, start=0, end=31, report=None)
|
||||
unproven["node_id"] = "follower-node-2"
|
||||
|
||||
tracker._raft_apply("register", proven)
|
||||
tracker._raft_apply("register", unproven)
|
||||
|
||||
admitted = tracker._registry["follower-node-1"]
|
||||
assert admitted.capability.state == STATE_ADMITTED
|
||||
assert _capability_routable(admitted, POLICY_ENFORCE)
|
||||
|
||||
absent = tracker._registry["follower-node-2"]
|
||||
assert absent.capability.state == STATE_ABSENT
|
||||
assert not _capability_routable(absent, POLICY_ENFORCE)
|
||||
|
||||
|
||||
def test_the_network_map_exposes_the_admission_state_of_every_node():
|
||||
"The operator view answers 'why is my node not routing' without raw internals.\n\nTags: http, routing, tracker"
|
||||
tracker = TrackerServer(capability_policy=POLICY_ENFORCE)
|
||||
port = tracker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{port}"
|
||||
_post_json(f"{base}/v1/nodes/register", _registration(9141, start=0, end=15))
|
||||
_post_json(
|
||||
f"{base}/v1/nodes/register",
|
||||
_registration(
|
||||
9142,
|
||||
start=16,
|
||||
end=31,
|
||||
report=_report(
|
||||
start=16,
|
||||
end=31,
|
||||
status="failed",
|
||||
diagnostics=["load failed: token=hf_abcdefghijklmnopqrstuvwx1234"],
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
network = _get_json(f"{base}/v1/network/map")
|
||||
assert network["capability_policy"] == POLICY_ENFORCE
|
||||
by_endpoint = {n["endpoint"]: n["capability"] for n in network["nodes"]}
|
||||
|
||||
proven = by_endpoint["http://127.0.0.1:9141"]
|
||||
assert proven["state"] == STATE_ADMITTED
|
||||
assert proven["routable"] is True
|
||||
assert proven["model_id"] == MODEL
|
||||
assert (proven["shard_start"], proven["shard_end"]) == (0, 15)
|
||||
assert proven["recipe_id"] == "baseline"
|
||||
assert proven["device"] == "cpu"
|
||||
|
||||
broken = by_endpoint["http://127.0.0.1:9142"]
|
||||
assert broken["state"] == STATE_FAILED
|
||||
assert broken["routable"] is False
|
||||
assert broken["detail"]
|
||||
|
||||
raw = json.dumps(network)
|
||||
assert "hf_abcdefghijklmnopqrstuvwx1234" not in raw
|
||||
assert "Traceback" not in raw
|
||||
finally:
|
||||
tracker.stop()
|
||||
Reference in New Issue
Block a user